feat: imgproxy
This commit is contained in:
parent
02800defd5
commit
c16d5d19e5
@ -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
17
src/Element/ProxyImg.tsx
Normal 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} />
|
||||
}
|
@ -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
39
src/Feed/ImgProxy.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user