feat: imgproxy

This commit is contained in:
Kieran 2023-01-31 14:41:21 +00:00
parent 02800defd5
commit c16d5d19e5
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
8 changed files with 154 additions and 44 deletions

View File

@ -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<string>(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 (
<div
{...rest}

17
src/Element/ProxyImg.tsx Normal file
View File

@ -0,0 +1,17 @@
import useImgProxy from "Feed/ImgProxy";
import { useEffect, useState } from "react";
export const ProxyImg = ({ src, ...rest }: { src?: string }) => {
const [url, setUrl] = useState<string>();
const { proxy } = useImgProxy();
useEffect(() => {
if (src) {
proxy(src)
.then(a => setUrl(a))
.catch(console.warn);
}
}, [src]);
return <img src={url} {...rest} />
}

View File

@ -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 <img key={url.toString()} src={url.toString()} />;
return <ProxyImg key={url.toString()} src={url.toString()} />;
}
case "wav":
case "mp3":
@ -81,9 +82,9 @@ function transformHttpLink(a: string, pref: UserPreferences) {
)
} else if (tidalId) {
return <TidalEmbed link={a} />
} else if (soundcloundId){
} else if (soundcloundId) {
return <SoundCloudEmbed link={a} />
} else if (mixcloudId){
} else if (mixcloudId) {
return <MixCloudEmbed link={a} />
} else {
return <a href={a} onClick={(e) => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">{a}</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]);

39
src/Feed/ImgProxy.ts Normal file
View File

@ -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}`;
}
}
}

View File

@ -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 = () => {
<div>Theme</div>
</div>
<div>
<select value={perf.theme} onChange={e => dispatch(setPreferences({ ...perf, theme: e.target.value} as UserPreferences))}>
<select value={perf.theme} onChange={e => dispatch(setPreferences({ ...perf, theme: e.target.value } as UserPreferences))}>
<option value="system">System (Default)</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
@ -32,6 +32,43 @@ const PreferencesPage = () => {
<input type="checkbox" checked={perf.autoLoadMedia} onChange={e => dispatch(setPreferences({ ...perf, autoLoadMedia: e.target.checked }))} />
</div>
</div>
<div className="card flex f-col">
<div className="flex w-max">
<div className="flex f-col f-grow">
<div>Image proxy service</div>
<small>Use imgproxy to compress images</small>
</div>
<div>
<input type="checkbox" checked={perf.imgProxyConfig !== null} onChange={e => dispatch(setPreferences({ ...perf, imgProxyConfig: e.target.checked ? InitState.preferences.imgProxyConfig : null }))} />
</div>
</div>
{perf.imgProxyConfig && (<div className="w-max mt10 form">
<div className="form-group">
<div>
Service Url
</div>
<div className="w-max">
<input type="text" value={perf.imgProxyConfig?.url} placeholder="Url.." onChange={e => dispatch(setPreferences({ ...perf, imgProxyConfig: { ...perf.imgProxyConfig!, url: e.target.value } }))} />
</div>
</div>
<div className="form-group">
<div>
Service Key
</div>
<div className="w-max">
<input type="password" value={perf.imgProxyConfig?.key} placeholder="Hex key.." onChange={e => dispatch(setPreferences({ ...perf, imgProxyConfig: { ...perf.imgProxyConfig!, key: e.target.value } }))} />
</div>
</div>
<div className="form-group">
<div>
Service Salt
</div>
<div className="w-max">
<input type="password" value={perf.imgProxyConfig?.salt} placeholder="Hex salt.." onChange={e => dispatch(setPreferences({ ...perf, imgProxyConfig: { ...perf.imgProxyConfig!, salt: e.target.value } }))} />
</div>
</div>
</div>)}
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>Enable reactions</div>
@ -65,21 +102,12 @@ const PreferencesPage = () => {
<small>Pick which upload service you want to upload attachments to</small>
</div>
<div>
<select value={perf.fileUploader} onChange={e => dispatch(setPreferences({ ...perf, fileUploader: e.target.value} as UserPreferences))}>
<select value={perf.fileUploader} onChange={e => dispatch(setPreferences({ ...perf, fileUploader: e.target.value } as UserPreferences))}>
<option value="void.cat">void.cat (Default)</option>
<option value="nostr.build">nostr.build</option>
</select>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>Image proxy</div>
<small>Use the caching image proxy to load avatars</small>
</div>
<div>
<input type="checkbox" checked={perf.useImageProxy} onChange={e => dispatch(setPreferences({ ...perf, useImageProxy: e.target.checked }))} />
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>Debug Menus</div>

View File

@ -102,7 +102,7 @@ export default function ProfileSettings() {
function editor() {
return (
<div className="editor">
<div className="editor form">
<div className="form-group">
<div>Name:</div>
<div>
@ -115,7 +115,7 @@ export default function ProfileSettings() {
<input type="text" value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
</div>
</div>
<div className="form-group f-col">
<div className="form-group form-col">
<div>About:</div>
<div className="w-max">
<textarea className="w-max" onChange={(e) => setAbout(e.target.value)} value={about}></textarea>

View File

@ -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;

View File

@ -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;
}
}