Merge branch 'main' into dbfix

This commit is contained in:
Alejandro Gomez 2023-01-27 22:30:42 +01:00
commit beeb6e6c8b
No known key found for this signature in database
GPG Key ID: 4DF39E566658C817
20 changed files with 149 additions and 42 deletions

View File

@ -8,7 +8,7 @@
<meta name="theme-color" content="#000000" />
<meta name="description" content="Fast nostr web ui" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; child-src 'none'; worker-src 'self'; frame-src youtube.com www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src wss://* 'self' https://*; img-src * data:; font-src https://fonts.gstatic.com; media-src *; script-src 'self' https://static.cloudflareinsights.com https://platform.twitter.com https://embed.tidal.com;" />
content="default-src 'self'; child-src 'none'; worker-src 'self'; frame-src youtube.com www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src wss://* 'self' https://*; img-src * data:; font-src https://fonts.gstatic.com; media-src *; script-src 'self' https://static.cloudflareinsights.com https://platform.twitter.com https://embed.tidal.com;" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/nostrich_512.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

View File

@ -5,6 +5,11 @@ import { RelaySettings } from "Nostr/Connection";
*/
export const ApiHost = "https://api.snort.social";
/**
* Void.cat file upload service url
*/
export const VoidCatHost = "https://void.cat";
/**
* Websocket re-connect timeout
*/
@ -19,9 +24,7 @@ export const ProfileCacheExpire = (1_000 * 60 * 5);
* Default bootstrap relays
*/
export const DefaultRelays = new Map<string, RelaySettings>([
["wss://relay.snort.social", { read: true, write: true }],
["wss://relay.damus.io", { read: true, write: true }],
["wss://nostr-pub.wellorder.net", { read: true, write: true }],
["wss://relay.snort.social", { read: true, write: true }]
]);
/**
@ -99,3 +102,9 @@ export const TidalRegex = /tidal\.com\/(?:browse\/)?(\w+)\/([a-z0-9-]+)/i;
* SoundCloud regex
*/
export const SoundCloudRegex = /soundcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/
/**
* Mixcloud regex
*/
export const MixCloudRegex = /mixcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/

View File

View File

@ -4,13 +4,14 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUserMinus, faUserPlus } from "@fortawesome/free-solid-svg-icons";
import { HexKey } from "Nostr";
import { RootState } from "State/Store";
import { parseId } from "Util";
export interface FollowButtonProps {
pubkey: HexKey,
className?: string,
}
export default function FollowButton(props: FollowButtonProps) {
const pubkey = props.pubkey;
const pubkey = parseId(props.pubkey);
const publiser = useEventPublisher();
const isFollowing = useSelector<RootState, boolean>(s => s.login.follows?.includes(pubkey) ?? false);
const baseClassName = isFollowing ? `btn btn-warn follow-button` : `btn btn-success follow-button`

View File

@ -0,0 +1,27 @@
import { MixCloudRegex } from "Const";
import { useSelector } from "react-redux";
import { RootState } from "State/Store";
const MixCloudEmbed = ({link}: {link: string}) => {
const feedPath = (MixCloudRegex.test(link) && RegExp.$1) + "%2F" + ( MixCloudRegex.test(link) && RegExp.$2)
const lightTheme = useSelector<RootState, boolean>(s => s.login.preferences.theme === "light");
const lightParams = lightTheme ? "light=1" : "light=0";
return(
<>
<br/>
<iframe
title="SoundCloud player"
width="100%"
height="120"
frameBorder="0"
src={`https://www.mixcloud.com/widget/iframe/?hide_cover=1&${lightParams}&feed=%2F${feedPath}%2F`}
/>
</>
)
}
export default MixCloudEmbed;

View File

@ -7,7 +7,7 @@ import "./NoteCreator.css";
import useEventPublisher from "Feed/EventPublisher";
import { openFile } from "Util";
import VoidUpload from "Feed/VoidUpload";
import { FileExtensionRegex } from "Const";
import { FileExtensionRegex, VoidCatHost } from "Const";
import Textarea from "Element/Textarea";
import Event, { default as NEvent } from "Nostr/Event";
@ -46,7 +46,7 @@ export function NoteCreator(props: NoteCreatorProps) {
let ext = file.name.match(FileExtensionRegex);
// extension tricks note parser to embed the content
let url = rx.file.meta?.url ?? `https://void.cat/d/${rx.file.id}${ext ? `.${ext[1]}` : ""}`;
let url = rx.file.meta?.url ?? `${VoidCatHost}/d/${rx.file.id}${ext ? `.${ext[1]}` : ""}`;
setNote(n => `${n}\n${url}`);
} else if (rx?.errorMessage) {

View File

@ -63,7 +63,7 @@ export default function Relay(props: RelayProps) {
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
</div>
<div>
<span className="icon-btn" onClick={() => navigate(name)}>
<span className="icon-btn" onClick={() => navigate(state!.id)}>
<FontAwesomeIcon icon={faGear} />
</span>
</div>

View File

@ -52,7 +52,7 @@
margin: 20px;
}
.text img, .text video, .text iframe {
.text img, .text video, .text iframe, .text audio {
max-width: 100%;
max-height: 500px;
margin: 10px auto;

View File

@ -4,7 +4,7 @@ import { Link } from "react-router-dom";
import ReactMarkdown from "react-markdown";
import { TwitterTweetEmbed } from "react-twitter-embed";
import { UrlRegex, FileExtensionRegex, MentionRegex, InvoiceRegex, YoutubeUrlRegex, TweetUrlRegex, HashtagRegex, TidalRegex, SoundCloudRegex } from "Const";
import { UrlRegex, FileExtensionRegex, MentionRegex, InvoiceRegex, YoutubeUrlRegex, TweetUrlRegex, HashtagRegex, TidalRegex, SoundCloudRegex, MixCloudRegex } from "Const";
import { eventLink, hexToBech32 } from "Util";
import Invoice from "Element/Invoice";
import Hashtag from "Element/Hashtag";
@ -17,6 +17,7 @@ import { useSelector } from 'react-redux';
import { RootState } from 'State/Store';
import { UserPreferences } from 'State/Login';
import SoundCloudEmbed from 'Element/SoundCloudEmded'
import MixCloudEmbed from './MixCloudEmbed';
function transformHttpLink(a: string, pref: UserPreferences) {
try {
@ -28,6 +29,7 @@ function transformHttpLink(a: string, pref: UserPreferences) {
const tweetId = TweetUrlRegex.test(a) && RegExp.$2;
const tidalId = TidalRegex.test(a) && RegExp.$1;
const soundcloundId = SoundCloudRegex.test(a) && RegExp.$1;
const mixcloudId = MixCloudRegex.test(a) && RegExp.$1;
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
if (extension) {
switch (extension) {
@ -39,6 +41,11 @@ function transformHttpLink(a: string, pref: UserPreferences) {
case "webp": {
return <img key={url.toString()} src={url.toString()} />;
}
case "wav":
case "mp3":
case "ogg": {
return <audio key={url.toString()} src={url.toString()} controls />
}
case "mp4":
case "mov":
case "mkv":
@ -75,6 +82,8 @@ function transformHttpLink(a: string, pref: UserPreferences) {
return <TidalEmbed link={a} />
} else if (soundcloundId){
return <SoundCloudEmbed link={a} />
} else if (mixcloudId){
return <MixCloudEmbed link={a} />
} else {
return <a href={a} onClick={(e) => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">{a}</a>
}

View File

@ -13,7 +13,7 @@ declare global {
nostr: {
getPublicKey: () => Promise<HexKey>,
signEvent: (event: RawEvent) => Promise<RawEvent>,
getRelays: () => Promise<[[string, { read: boolean, write: boolean }]]>,
getRelays: () => Promise<Record<string, { read: boolean, write: boolean }>>,
nip04: {
encrypt: (pubkey: HexKey, content: string) => Promise<string>,
decrypt: (pubkey: HexKey, content: string) => Promise<string>
@ -71,8 +71,8 @@ export default function useEventPublisher() {
return match;
}
const content = msg.replace(/@npub[a-z0-9]+/g, replaceNpub)
.replace(/note[a-z0-9]+/g, replaceNoteId)
.replace(HashtagRegex, replaceHashtag);
.replace(/note[a-z0-9]+/g, replaceNoteId)
.replace(HashtagRegex, replaceHashtag);
ev.Content = content;
}
@ -89,8 +89,8 @@ export default function useEventPublisher() {
* When they open the site again we wont see that updated relay list and so it will appear to reset back to the previous state
*/
broadcastForBootstrap: (ev: NEvent | undefined) => {
if(ev) {
for(let [k, _] of DefaultRelays) {
if (ev) {
for (let [k, _] of DefaultRelays) {
System.WriteOnceToRelay(k, ev);
}
}
@ -182,6 +182,9 @@ export default function useEventPublisher() {
temp.add(pkAdd);
}
for (let pk of temp) {
if (pk.length !== 64) {
continue;
}
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
}
@ -194,7 +197,7 @@ export default function useEventPublisher() {
ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(relays);
for (let pk of follows) {
if (pk === pkRemove) {
if (pk === pkRemove || pk.length !== 64) {
continue;
}
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));

View File

@ -1,4 +1,5 @@
import * as secp from "@noble/secp256k1";
import { VoidCatHost } from "Const";
/**
* Upload file to void.cat
@ -8,7 +9,7 @@ export default async function VoidUpload(file: File | Blob, filename: string) {
const buf = await file.arrayBuffer();
const digest = await crypto.subtle.digest("SHA-256", buf);
let req = await fetch(`https://void.cat/upload`, {
let req = await fetch(`${VoidCatHost}/upload`, {
mode: "cors",
method: "POST",
body: buf,
@ -17,7 +18,8 @@ export default async function VoidUpload(file: File | Blob, filename: string) {
"V-Content-Type": file.type,
"V-Filename": filename,
"V-Full-Digest": secp.utils.bytesToHex(new Uint8Array(digest)),
"V-Description": "Upload from https://snort.social"
"V-Description": "Upload from https://snort.social",
"V-Strip-Metadata": "true"
}
});

View File

@ -29,10 +29,12 @@ export type StateSnapshot = {
received: number,
send: number
},
info?: RelayInfo
info?: RelayInfo,
id: string
};
export default class Connection {
Id: string;
Address: string;
Socket: WebSocket | null;
Pending: Subscriptions[];
@ -50,6 +52,7 @@ export default class Connection {
EventsCallback: Map<u256, () => void>;
constructor(addr: string, options: RelaySettings) {
this.Id = uuid();
this.Address = addr;
this.Socket = null;
this.Pending = [];
@ -285,6 +288,7 @@ export default class Connection {
this.CurrentState.avgLatency = this.Stats.Latency.length > 0 ? (this.Stats.Latency.reduce((acc, v) => acc + v, 0) / this.Stats.Latency.length) : 0;
this.CurrentState.disconnects = this.Stats.Disconnects;
this.CurrentState.info = this.Info;
this.CurrentState.id = this.Id;
this.Stats.Latency = this.Stats.Latency.slice(-20); // trim
this.HasStateChange = true;
this._NotifyState();

View File

@ -21,18 +21,28 @@ interface Splits {
split: number
}
interface TotalToday {
donations: number,
nip5: number
}
const DonatePage = () => {
const [splits, setSplits] = useState<Splits[]>([]);
const [today, setSumToday] = useState<TotalToday>();
async function loadSplits() {
async function loadData() {
let rsp = await fetch(`${ApiHost}/api/v1/revenue/splits`);
if(rsp.ok) {
setSplits(await rsp.json());
}
let rsp2 = await fetch(`${ApiHost}/api/v1/revenue/today`);
if(rsp2.ok) {
setSumToday(await rsp2.json());
}
}
useEffect(() => {
loadSplits().catch(console.warn);
loadData().catch(console.warn);
}, []);
function actions(pk: HexKey) {
@ -62,6 +72,7 @@ const DonatePage = () => {
<div className="mr10">Lightning Donation: </div>
<ZapButton svc={"donate@snort.social"} />
</div>
{today && (<small>Total today (UTC): {today.donations.toLocaleString()} sats</small>)}
<h3>Primary Developers</h3>
{Developers.map(a => <ProfilePreview pubkey={a} key={a} actions={actions(a)} />)}
<h4>Contributors</h4>

View File

@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom";
import * as secp from '@noble/secp256k1';
import { RootState } from "State/Store";
import { setPrivateKey, setPublicKey } from "State/Login";
import { setPrivateKey, setPublicKey, setRelays } from "State/Login";
import { EmailRegex } from "Const";
import { bech32ToHex } from "Util";
import { HexKey } from "Nostr";
@ -73,6 +73,14 @@ export default function LoginPage() {
async function doNip07Login() {
let pubKey = await window.nostr.getPublicKey();
dispatch(setPublicKey(pubKey));
if ("getRelays" in window.nostr) {
let relays = await window.nostr.getRelays();
dispatch(setRelays({
relays: relays,
createdAt: 1
}));
}
}
function altLogins() {

View File

@ -57,8 +57,9 @@ export default function MessagesPage() {
<div className="btn" onClick={() => markAllRead()}>Mark All Read</div>
</div>
{chats.sort((a, b) => {
if(b.pubkey === myPubKey) return 1
return b.newestMessage - a.newestMessage
return a.pubkey === myPubKey ? -1 :
b.pubkey === myPubKey ? 1 :
b.newestMessage - a.newestMessage
}).map(person)}
</>
)

View File

@ -31,14 +31,21 @@ export default function NewUserPage() {
setError("");
try {
let rsp = await fetch(`${TwitterFollowsApi}?username=${twitterUsername}`);
let data = await rsp.json();
if (rsp.ok) {
setFollows(await rsp.json());
if (Array.isArray(data) && data.length === 0) {
setError(`No nostr users found for "${twitterUsername}"`);
} else {
setFollows(data);
}
} else if ("error" in data) {
setError(data.error);
} else {
setError("Failed to load follows, is your profile public?");
setError("Failed to load follows, please try again later");
}
} catch (e) {
console.warn(e);
setError("Failed to load follows, is your profile public?");
setError("Failed to load follows, please try again later");
}
}

View File

@ -30,7 +30,7 @@ export const SettingsRoutes: RouteObject[] = [
element: <Relay />,
},
{
path: "relays/:addr",
path: "relays/:id",
element: <RelayInfo />
},
{

View File

@ -15,6 +15,7 @@ import { hexToBech32, openFile } from "Util";
import Copy from "Element/Copy";
import { RootState } from "State/Store";
import { HexKey } from "Nostr";
import { VoidCatHost } from "Const";
export default function ProfileSettings() {
const navigate = useNavigate();
@ -64,6 +65,7 @@ export default function ProfileSettings() {
delete userCopy["loaded"];
delete userCopy["created"];
delete userCopy["pubkey"];
delete userCopy["npub"];
console.debug(userCopy);
let ev = await publisher.metadata(userCopy);
@ -86,14 +88,14 @@ export default function ProfileSettings() {
async function setNewAvatar() {
const rsp = await uploadFile();
if (rsp) {
setPicture(rsp.meta?.url ?? `https://void.cat/d/${rsp.id}`);
setPicture(rsp.meta?.url ?? `${VoidCatHost}/d/${rsp.id}`);
}
}
async function setNewBanner() {
const rsp = await uploadFile();
if (rsp) {
setBanner(rsp.meta?.url ?? `https://void.cat/d/${rsp.id}`);
setBanner(rsp.meta?.url ?? `${VoidCatHost}/d/${rsp.id}`);
}
}

View File

@ -10,16 +10,16 @@ const RelayInfo = () => {
const params = useParams();
const navigate = useNavigate();
const dispatch = useDispatch();
const addr: string = `wss://${params.addr}`;
const con = System.Sockets.get(addr) ?? System.Sockets.get(`${addr}/`);
const stats = useRelayState(con?.Address ?? addr);
const conn = Array.from(System.Sockets.values()).find(a => a.Id === params.id);
console.debug(conn);
const stats = useRelayState(conn?.Address ?? "");
return (
<>
<h3 className="pointer" onClick={() => navigate("/settings/relays")}>Relays</h3>
<div className="card">
<h3>{stats?.info?.name ?? addr}</h3>
<h3>{stats?.info?.name}</h3>
<p>{stats?.info?.description}</p>
{stats?.info?.pubkey && (<>
@ -45,7 +45,7 @@ const RelayInfo = () => {
</>)}
<div className="flex mt10 f-end">
<div className="btn error" onClick={() => {
dispatch(removeRelay(con!.Address));
dispatch(removeRelay(conn!.Address));
navigate("/settings/relays")
}}>Remove</div>
</div>

View File

@ -8,6 +8,8 @@ const PrivateKeyItem = "secret";
const PublicKeyItem = "pubkey";
const NotificationsReadItem = "notifications-read";
const UserPreferencesKey = "preferences";
const RelayListKey = "last-relays";
const FollowList = "last-follows";
export interface UserPreferences {
/**
@ -38,7 +40,7 @@ export interface UserPreferences {
/**
* Show debugging menus to help diagnose issues
*/
showDebugMenus: boolean
showDebugMenus: boolean
}
export interface LoginStore {
@ -110,7 +112,7 @@ const InitState = {
dms: [],
dmInteraction: 0,
preferences: {
enableReactions: false,
enableReactions: true,
autoLoadMedia: true,
theme: "system",
confirmReposts: false,
@ -138,8 +140,6 @@ const LoginSlice = createSlice({
state.loggedOut = true;
}
state.relays = Object.fromEntries(DefaultRelays.entries());
// check pub key only
let pubKey = window.localStorage.getItem(PublicKeyItem);
if (pubKey && !state.privateKey) {
@ -147,6 +147,18 @@ const LoginSlice = createSlice({
state.loggedOut = false;
}
let lastRelayList = window.localStorage.getItem(RelayListKey);
if (lastRelayList) {
state.relays = JSON.parse(lastRelayList);
} else {
state.relays = Object.fromEntries(DefaultRelays.entries());
}
let lastFollows = window.localStorage.getItem(FollowList);
if (lastFollows) {
state.follows = JSON.parse(lastFollows);
}
// notifications
let readNotif = parseInt(window.localStorage.getItem(NotificationsReadItem) ?? "0");
if (!isNaN(readNotif)) {
@ -187,25 +199,36 @@ const LoginSlice = createSlice({
state.relays = Object.fromEntries(filtered.entries());
state.latestRelays = createdAt;
window.localStorage.setItem(RelayListKey, JSON.stringify(state.relays));
},
removeRelay: (state, action: PayloadAction<string>) => {
delete state.relays[action.payload];
state.relays = { ...state.relays };
window.localStorage.setItem(RelayListKey, JSON.stringify(state.relays));
},
setFollows: (state, action: PayloadAction<string | string[]>) => {
setFollows: (state, action: PayloadAction<HexKey | HexKey[]>) => {
let existing = new Set(state.follows);
let update = Array.isArray(action.payload) ? action.payload : [action.payload];
let changes = false;
for (let pk of update) {
for (let pk of update.filter(a => a.length === 64)) {
if (!existing.has(pk)) {
existing.add(pk);
changes = true;
}
}
for (let pk of existing) {
if (!update.includes(pk)) {
existing.delete(pk);
changes = true;
}
}
if (changes) {
state.follows = Array.from(existing);
}
window.localStorage.setItem(FollowList, JSON.stringify(state.follows));
},
addNotifications: (state, action: PayloadAction<TaggedRawEvent | TaggedRawEvent[]>) => {
let n = action.payload;
@ -249,10 +272,10 @@ const LoginSlice = createSlice({
state.dmInteraction += 1;
},
logout: (state) => {
window.localStorage.clear();
Object.assign(state, InitState);
state.loggedOut = true;
state.relays = Object.fromEntries(DefaultRelays.entries());
window.localStorage.clear();
},
markNotificationsRead: (state) => {
state.readNotifications = new Date().getTime();