From c16d5d19e5d2b98cbb8f3345c865bc38d7a5d51e Mon Sep 17 00:00:00 2001 From: Kieran Date: Tue, 31 Jan 2023 14:41:21 +0000 Subject: [PATCH] feat: imgproxy --- src/Element/Avatar.tsx | 23 +++++++------ src/Element/ProxyImg.tsx | 17 ++++++++++ src/Element/Text.tsx | 25 +++++++------- src/Feed/ImgProxy.ts | 39 ++++++++++++++++++++++ src/Pages/settings/Preferences.tsx | 52 +++++++++++++++++++++++------- src/Pages/settings/Profile.tsx | 4 +-- src/State/Login.ts | 13 +++++--- src/index.css | 25 +++++++++++--- 8 files changed, 154 insertions(+), 44 deletions(-) create mode 100644 src/Element/ProxyImg.tsx create mode 100644 src/Feed/ImgProxy.ts diff --git a/src/Element/Avatar.tsx b/src/Element/Avatar.tsx index 553f6d8e..2f78cf1d 100644 --- a/src/Element/Avatar.tsx +++ b/src/Element/Avatar.tsx @@ -1,19 +1,24 @@ import "./Avatar.css"; import Nostrich from "nostrich.webp"; -import { CSSProperties } from "react"; +import { CSSProperties, useEffect, useState } from "react"; import type { UserMetadata } from "Nostr"; -import { useSelector } from "react-redux"; -import { RootState } from "State/Store"; -import { ApiHost } from "Const"; +import useImgProxy from "Feed/ImgProxy"; const Avatar = ({ user, ...rest }: { user?: UserMetadata, onClick?: () => void }) => { - const useImageProxy = useSelector((s: RootState) => s.login.preferences.useImageProxy); + const [url, setUrl] = useState(Nostrich); + const { proxy } = useImgProxy(); - const avatarUrl = (user?.picture?.length ?? 0) === 0 ? Nostrich : - (useImageProxy ? `${ApiHost}/api/v1/imgproxy/${window.btoa(user!.picture!)}` : user?.picture) - const backgroundImage = `url(${avatarUrl})` - const domain = user?.nip05 && user.nip05.split('@')[1] + useEffect(() => { + if (user?.picture) { + proxy(user.picture) + .then(a => setUrl(a)) + .catch(console.warn); + } + }, [user]); + + const backgroundImage = `url(${url})` const style = { '--img-url': backgroundImage } as CSSProperties + const domain = user?.nip05 && user.nip05.split('@')[1] return (
{ + const [url, setUrl] = useState(); + const { proxy } = useImgProxy(); + + useEffect(() => { + if (src) { + proxy(src) + .then(a => setUrl(a)) + .catch(console.warn); + } + }, [src]); + + return +} \ No newline at end of file diff --git a/src/Element/Text.tsx b/src/Element/Text.tsx index e7200980..0917db2b 100644 --- a/src/Element/Text.tsx +++ b/src/Element/Text.tsx @@ -17,8 +17,9 @@ import TidalEmbed from "Element/TidalEmbed"; import { useSelector } from 'react-redux'; import { RootState } from 'State/Store'; import { UserPreferences } from 'State/Login'; -import SoundCloudEmbed from 'Element/SoundCloudEmded' -import MixCloudEmbed from './MixCloudEmbed'; +import SoundCloudEmbed from 'Element/SoundCloudEmded' +import MixCloudEmbed from 'Element/MixCloudEmbed'; +import { ProxyImg } from 'Element/ProxyImg'; function transformHttpLink(a: string, pref: UserPreferences) { try { @@ -40,7 +41,7 @@ function transformHttpLink(a: string, pref: UserPreferences) { case "png": case "bmp": case "webp": { - return ; + return ; } case "wav": case "mp3": @@ -81,9 +82,9 @@ function transformHttpLink(a: string, pref: UserPreferences) { ) } else if (tidalId) { return - } else if (soundcloundId){ + } else if (soundcloundId) { return - } else if (mixcloudId){ + } else if (mixcloudId) { return } else { return e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">{a} @@ -223,14 +224,14 @@ export default function Text({ content, tags, users }: TextProps) { parent && typeof index === 'number' && (node.type === 'link' || - node.type === 'linkReference' || - node.type === 'image' || - node.type === 'imageReference' || - node.type === 'definition') + node.type === 'linkReference' || + node.type === 'image' || + node.type === 'imageReference' || + node.type === 'definition') ) { - node.type = 'text'; - node.value = content.slice(node.position.start.offset, node.position.end.offset).replace(/\)$/, ' )'); - return SKIP; + node.type = 'text'; + node.value = content.slice(node.position.start.offset, node.position.end.offset).replace(/\)$/, ' )'); + return SKIP; } }) }, [content]); diff --git a/src/Feed/ImgProxy.ts b/src/Feed/ImgProxy.ts new file mode 100644 index 00000000..91b01408 --- /dev/null +++ b/src/Feed/ImgProxy.ts @@ -0,0 +1,39 @@ +import * as secp from "@noble/secp256k1" +import * as base64 from "@protobufjs/base64" +import { useSelector } from "react-redux"; +import { RootState } from "State/Store"; + +export interface ImgProxySettings { + url: string, + key: string, + salt: string +} + +export default function useImgProxy() { + const settings = useSelector((s: RootState) => s.login.preferences.imgProxyConfig); + const te = new TextEncoder(); + + function urlSafe(s: string) { + return s.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); + } + + async function signUrl(u: string) { + const result = await secp.utils.hmacSha256( + secp.utils.hexToBytes(settings!.key), + secp.utils.hexToBytes(settings!.salt), + te.encode(u)); + return urlSafe(base64.encode(result, 0, result.byteLength)); + } + + return { + proxy: async (url: string, resize?: number) => { + if (!settings) return url; + const opt = resize ? `rs:fit:${resize}:${resize}` : ""; + const urlBytes = te.encode(url); + const urlEncoded = urlSafe(base64.encode(urlBytes, 0, urlBytes.byteLength)); + const path = `/${opt}/${urlEncoded}`; + const sig = await signUrl(path); + return `${new URL(settings.url).toString()}${sig}${path}`; + } + } +} \ No newline at end of file diff --git a/src/Pages/settings/Preferences.tsx b/src/Pages/settings/Preferences.tsx index f0acbe68..93e3bfd0 100644 --- a/src/Pages/settings/Preferences.tsx +++ b/src/Pages/settings/Preferences.tsx @@ -1,5 +1,5 @@ import { useDispatch, useSelector } from "react-redux"; -import { setPreferences, UserPreferences } from "State/Login"; +import { InitState, setPreferences, UserPreferences } from "State/Login"; import { RootState } from "State/Store"; import "./Preferences.css"; @@ -16,7 +16,7 @@ const PreferencesPage = () => {
Theme
- dispatch(setPreferences({ ...perf, theme: e.target.value } as UserPreferences))}> @@ -32,6 +32,43 @@ const PreferencesPage = () => { dispatch(setPreferences({ ...perf, autoLoadMedia: e.target.checked }))} />
+
+
+
+
Image proxy service
+ Use imgproxy to compress images +
+
+ dispatch(setPreferences({ ...perf, imgProxyConfig: e.target.checked ? InitState.preferences.imgProxyConfig : null }))} /> +
+
+ {perf.imgProxyConfig && (
+
+
+ Service Url +
+
+ dispatch(setPreferences({ ...perf, imgProxyConfig: { ...perf.imgProxyConfig!, url: e.target.value } }))} /> +
+
+
+
+ Service Key +
+
+ dispatch(setPreferences({ ...perf, imgProxyConfig: { ...perf.imgProxyConfig!, key: e.target.value } }))} /> +
+
+
+
+ Service Salt +
+
+ dispatch(setPreferences({ ...perf, imgProxyConfig: { ...perf.imgProxyConfig!, salt: e.target.value } }))} /> +
+
+
)} +
Enable reactions
@@ -65,21 +102,12 @@ const PreferencesPage = () => { Pick which upload service you want to upload attachments to
- dispatch(setPreferences({ ...perf, fileUploader: e.target.value } as UserPreferences))}>
-
-
-
Image proxy
- Use the caching image proxy to load avatars -
-
- dispatch(setPreferences({ ...perf, useImageProxy: e.target.checked }))} /> -
-
Debug Menus
diff --git a/src/Pages/settings/Profile.tsx b/src/Pages/settings/Profile.tsx index a47b3fd5..814d6450 100644 --- a/src/Pages/settings/Profile.tsx +++ b/src/Pages/settings/Profile.tsx @@ -102,7 +102,7 @@ export default function ProfileSettings() { function editor() { return ( -
+
Name:
@@ -115,7 +115,7 @@ export default function ProfileSettings() { setDisplayName(e.target.value)} />
-
+
About:
diff --git a/src/State/Login.ts b/src/State/Login.ts index 59bcd7db..e5640227 100644 --- a/src/State/Login.ts +++ b/src/State/Login.ts @@ -4,6 +4,7 @@ import { DefaultRelays } from 'Const'; import { HexKey, TaggedRawEvent } from 'Nostr'; import { RelaySettings } from 'Nostr/Connection'; import type { AppDispatch, RootState } from "State/Store"; +import { ImgProxySettings } from 'Feed/ImgProxy'; const PrivateKeyItem = "secret"; const PublicKeyItem = "pubkey"; @@ -56,9 +57,9 @@ export interface UserPreferences { fileUploader: "void.cat" | "nostr.build", /** - * Use image proxy service to compress avatars + * Use imgproxy to optimize images */ - useImageProxy: boolean, + imgProxyConfig: ImgProxySettings | null } export interface LoginStore { @@ -138,7 +139,7 @@ export interface LoginStore { preferences: UserPreferences }; -const InitState = { +export const InitState = { loggedOut: undefined, publicKey: undefined, privateKey: undefined, @@ -161,7 +162,11 @@ const InitState = { showDebugMenus: false, autoShowLatest: false, fileUploader: "void.cat", - useImageProxy: true + imgProxyConfig: { + url: "https://imgproxy.snort.social", + key: "a82fcf26aa0ccb55dfc6b4bd6a1c90744d3be0f38429f21a8828b43449ce7cebe6bdc2b09a827311bef37b18ce35cb1e6b1c60387a254541afa9e5b4264ae942", + salt: "a897770d9abf163de055e9617891214e75a9016d748f8ef865e6ffbcb9ed932295659549773a22a019a5f06d0b440c320be411e3fddfe784e199e4f03d74bd9b" + } } } as LoginStore; diff --git a/src/index.css b/src/index.css index 419060ef..b3e4939c 100644 --- a/src/index.css +++ b/src/index.css @@ -327,18 +327,24 @@ a.ext { white-space: initial; } +div.form { + display: grid; + grid-auto-flow: row; +} + div.form-group { - display: flex; + display: grid; align-items: center; + grid-template-columns: 1fr 2fr; } div.form-group>div { padding: 3px 5px; - word-break: break-word; } div.form-group>div:nth-child(1) { - min-width: 100px; + display: flex; + align-self: center; } div.form-group>div:nth-child(2) { @@ -351,6 +357,11 @@ div.form-group>div:nth-child(2) input { flex-grow: 1; } +div.form-col { + grid-auto-flow: row; + grid-template-columns: auto; +} + .modal { position: absolute; width: 100vw; @@ -377,7 +388,7 @@ body.scroll-lock { height: 100vh; } -.pointer { +.pointer { cursor: pointer; } @@ -485,6 +496,10 @@ body.scroll-lock { } @media(max-width: 720px) { + div.form { + grid-auto-flow: dense; + } + div.form-group { flex-direction: column; align-items: flex-start; @@ -507,4 +522,4 @@ body.scroll-lock { .bold { font-weight: 700; -} +} \ No newline at end of file