Optimize subs and many other things
This commit is contained in:
parent
560d827f87
commit
75a6a34900
@ -13,7 +13,6 @@
|
|||||||
"qr-code-styling": "^1.6.0-rc.1",
|
"qr-code-styling": "^1.6.0-rc.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-modal": "^3.16.1",
|
|
||||||
"react-redux": "^8.0.5",
|
"react-redux": "^8.0.5",
|
||||||
"react-router-dom": "^6.5.0",
|
"react-router-dom": "^6.5.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="Fast nostr web ui" />
|
<meta name="description" content="Fast nostr web ui" />
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<meta http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; child-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src wss://* 'self'; img-src *; font-src https://fonts.gstatic.com;" />
|
content="default-src 'self'; child-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src wss://* 'self'; img-src * data:; font-src https://fonts.gstatic.com;" />
|
||||||
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/nostrich_512.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/nostrich_512.png" />
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
11
src/element/Modal.css
Normal file
11
src/element/Modal.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.modal {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgba(0,0,0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
18
src/element/Modal.js
Normal file
18
src/element/Modal.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import "./Modal.css";
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
|
export default function Modal(props) {
|
||||||
|
const onClose = props.onClose || (() => {});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
document.body.classList.add("scroll-lock");
|
||||||
|
return () => document.body.classList.remove("scroll-lock");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal" onClick={(e) => onClose(e)}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import "./Note.css";
|
import "./Note.css";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
@ -11,9 +11,9 @@ import ProfileImage from "./ProfileImage";
|
|||||||
import useEventPublisher from "../feed/EventPublisher";
|
import useEventPublisher from "../feed/EventPublisher";
|
||||||
import { NoteCreator } from "./NoteCreator";
|
import { NoteCreator } from "./NoteCreator";
|
||||||
|
|
||||||
const UrlRegex = /((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/;
|
const UrlRegex = /((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/i;
|
||||||
const FileExtensionRegex = /\.([\w]+)$/;
|
const FileExtensionRegex = /\.([\w]+)$/i;
|
||||||
const MentionRegex = /(#\[\d+\])/g;
|
const MentionRegex = /(#\[\d+\])/gi;
|
||||||
|
|
||||||
export default function Note(props) {
|
export default function Note(props) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -21,22 +21,10 @@ export default function Note(props) {
|
|||||||
const dataEvent = props["data-ev"];
|
const dataEvent = props["data-ev"];
|
||||||
const reactions = props.reactions;
|
const reactions = props.reactions;
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const [sig, setSig] = useState(false);
|
|
||||||
const [showReply, setShowReply] = useState(false);
|
const [showReply, setShowReply] = useState(false);
|
||||||
const users = useSelector(s => s.users?.users);
|
const users = useSelector(s => s.users?.users);
|
||||||
const ev = dataEvent ?? Event.FromObject(data);
|
const ev = dataEvent ?? Event.FromObject(data);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (sig === false) {
|
|
||||||
verifyEvent();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function verifyEvent() {
|
|
||||||
let res = await ev.Verify();
|
|
||||||
setSig(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToEvent(e, id) {
|
function goToEvent(e, id) {
|
||||||
if (!window.location.pathname.startsWith("/e/")) {
|
if (!window.location.pathname.startsWith("/e/")) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -66,7 +54,7 @@ export default function Note(props) {
|
|||||||
return urlBody.map(a => {
|
return urlBody.map(a => {
|
||||||
if (a.startsWith("http")) {
|
if (a.startsWith("http")) {
|
||||||
let url = new URL(a);
|
let url = new URL(a);
|
||||||
let ext = url.pathname.match(FileExtensionRegex);
|
let ext = url.pathname.toLowerCase().match(FileExtensionRegex);
|
||||||
if (ext) {
|
if (ext) {
|
||||||
switch (ext[1]) {
|
switch (ext[1]) {
|
||||||
case "gif":
|
case "gif":
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { useContext } from "react";
|
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { NostrContext } from "..";
|
import { System } from "..";
|
||||||
import Event from "../nostr/Event";
|
import Event from "../nostr/Event";
|
||||||
import EventKind from "../nostr/EventKind";
|
import EventKind from "../nostr/EventKind";
|
||||||
import Tag from "../nostr/Tag";
|
import Tag from "../nostr/Tag";
|
||||||
|
|
||||||
export default function useEventPublisher() {
|
export default function useEventPublisher() {
|
||||||
const system = useContext(NostrContext);
|
|
||||||
const pubKey = useSelector(s => s.login.publicKey);
|
const pubKey = useSelector(s => s.login.publicKey);
|
||||||
const privKey = useSelector(s => s.login.privateKey);
|
const privKey = useSelector(s => s.login.privateKey);
|
||||||
const nip07 = useSelector(s => s.login.nip07);
|
const nip07 = useSelector(s => s.login.nip07);
|
||||||
@ -33,7 +31,7 @@ export default function useEventPublisher() {
|
|||||||
return {
|
return {
|
||||||
broadcast: (ev) => {
|
broadcast: (ev) => {
|
||||||
console.debug("Sending event: ", ev);
|
console.debug("Sending event: ", ev);
|
||||||
system.BroadcastEvent(ev);
|
System.BroadcastEvent(ev);
|
||||||
},
|
},
|
||||||
metadata: async (obj) => {
|
metadata: async (obj) => {
|
||||||
let ev = Event.ForPubKey(pubKey);
|
let ev = Event.ForPubKey(pubKey);
|
||||||
|
@ -1,37 +1,53 @@
|
|||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { NostrContext } from "..";
|
import { System } from "..";
|
||||||
import Event from "../nostr/Event";
|
import Event from "../nostr/Event";
|
||||||
import EventKind from "../nostr/EventKind";
|
import EventKind from "../nostr/EventKind";
|
||||||
import { Subscriptions } from "../nostr/Subscriptions";
|
import { Subscriptions } from "../nostr/Subscriptions";
|
||||||
import { setFollows, setRelays } from "../state/Login";
|
import { addNotifications, setFollows, setRelays } from "../state/Login";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Managed loading data for the current logged in user
|
* Managed loading data for the current logged in user
|
||||||
*/
|
*/
|
||||||
export default function useLoginFeed() {
|
export default function useLoginFeed() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const system = useContext(NostrContext);
|
|
||||||
const pubKey = useSelector(s => s.login.publicKey);
|
const pubKey = useSelector(s => s.login.publicKey);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (system && pubKey) {
|
if (pubKey) {
|
||||||
let sub = new Subscriptions();
|
let sub = new Subscriptions();
|
||||||
|
sub.Id = "login";
|
||||||
sub.Authors.add(pubKey);
|
sub.Authors.add(pubKey);
|
||||||
sub.Kinds.add(EventKind.ContactList);
|
sub.Kinds.add(EventKind.ContactList);
|
||||||
|
|
||||||
|
let notifications = new Subscriptions();
|
||||||
|
notifications.Kinds.add(EventKind.TextNote);
|
||||||
|
notifications.Kinds.add(EventKind.Reaction);
|
||||||
|
notifications.PTags.add(pubKey);
|
||||||
|
sub.AddSubscription(notifications);
|
||||||
|
|
||||||
sub.OnEvent = (e) => {
|
sub.OnEvent = (e) => {
|
||||||
let ev = Event.FromObject(e);
|
let ev = Event.FromObject(e);
|
||||||
if (ev.Content !== "") {
|
switch (ev.Kind) {
|
||||||
let relays = JSON.parse(ev.Content);
|
case EventKind.ContactList: {
|
||||||
dispatch(setRelays(relays));
|
if (ev.Content !== "") {
|
||||||
|
let relays = JSON.parse(ev.Content);
|
||||||
|
dispatch(setRelays(relays));
|
||||||
|
}
|
||||||
|
let pTags = ev.Tags.filter(a => a.Key === "p").map(a => a.PubKey);
|
||||||
|
dispatch(setFollows(pTags));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
dispatch(addNotifications(ev.ToObject()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let pTags = ev.Tags.filter(a => a.Key === "p").map(a => a.PubKey);
|
|
||||||
dispatch(setFollows(pTags));
|
|
||||||
}
|
}
|
||||||
system.AddSubscription(sub);
|
System.AddSubscription(sub);
|
||||||
return () => system.RemoveSubscription(sub.Id);
|
return () => System.RemoveSubscription(sub.Id);
|
||||||
}
|
}
|
||||||
}, [system, pubKey]);
|
}, [pubKey]);
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
@ -1,19 +1,17 @@
|
|||||||
import { useContext, useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { NostrContext } from "..";
|
|
||||||
import { addPubKey } from "../state/Users";
|
import { addPubKey } from "../state/Users";
|
||||||
|
|
||||||
export default function useProfile(pubKey) {
|
export default function useProfile(pubKey) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const system = useContext(NostrContext);
|
|
||||||
const user = useSelector(s => s.users.users[pubKey]);
|
const user = useSelector(s => s.users.users[pubKey]);
|
||||||
const pubKeys = useSelector(s => s.users.pubKeys);
|
const pubKeys = useSelector(s => s.users.pubKeys);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (system && pubKey !== "" && !pubKeys.includes(pubKey)) {
|
if (pubKey !== "" && !pubKeys.includes(pubKey)) {
|
||||||
dispatch(addPubKey(pubKey));
|
dispatch(addPubKey(pubKey));
|
||||||
}
|
}
|
||||||
}, [system]);
|
}, [pubKey]);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
51
src/feed/Subscription.js
Normal file
51
src/feed/Subscription.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { System } from "..";
|
||||||
|
import { Subscriptions } from "../nostr/Subscriptions";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Subscriptions} sub
|
||||||
|
* @param {any} opt
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function useSubscription(sub, opt) {
|
||||||
|
const [notes, setNotes] = useState([]);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
leaveOpen: false,
|
||||||
|
...opt
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sub) {
|
||||||
|
sub.OnEvent = (e) => {
|
||||||
|
setNotes(n => {
|
||||||
|
if (Array.isArray(n) && !n.some(a => a.id === e.id)) {
|
||||||
|
return [
|
||||||
|
...n,
|
||||||
|
e
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!options.leaveOpen) {
|
||||||
|
sub.OnEnd = (c) => {
|
||||||
|
c.RemoveSubscription(sub.Id);
|
||||||
|
if (sub.IsFinished()) {
|
||||||
|
System.RemoveSubscription(sub.Id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
System.AddSubscription(sub);
|
||||||
|
return () => {
|
||||||
|
System.RemoveSubscription(sub.Id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [sub]);
|
||||||
|
|
||||||
|
return { notes, sub };
|
||||||
|
}
|
@ -1,79 +1,56 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import EventKind from "../nostr/EventKind";
|
||||||
import { NostrContext } from "..";
|
|
||||||
import Event from "../nostr/Event";
|
|
||||||
import { Subscriptions } from "../nostr/Subscriptions";
|
import { Subscriptions } from "../nostr/Subscriptions";
|
||||||
import { addNote, reset } from "../state/Thread";
|
import useSubscription from "./Subscription";
|
||||||
import { addPubKey } from "../state/Users";
|
|
||||||
|
|
||||||
export default function useThreadFeed(id) {
|
export default function useThreadFeed(id) {
|
||||||
const dispatch = useDispatch();
|
const sub = useMemo(() => {
|
||||||
const system = useContext(NostrContext);
|
const thisSub = new Subscriptions();
|
||||||
const notes = useSelector(s => s.thread.notes);
|
thisSub.Id = "thread";
|
||||||
const [thisLoaded, setThisLoaded] = useState(false);
|
thisSub.Ids.add(id);
|
||||||
|
|
||||||
// track profiles
|
// get replies to this event
|
||||||
useEffect(() => {
|
const subRelated = new Subscriptions();
|
||||||
let keys = [];
|
subRelated.Kinds.add(EventKind.Reaction);
|
||||||
for (let n of notes) {
|
subRelated.Kinds.add(EventKind.TextNote);
|
||||||
if (n.pubkey) {
|
subRelated.ETags = thisSub.Ids;
|
||||||
keys.push(n.pubkey);
|
thisSub.AddSubscription(subRelated);
|
||||||
|
|
||||||
|
return thisSub;
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const main = useSubscription(sub, { leaveOpen: true });
|
||||||
|
|
||||||
|
const relatedThisSub = useMemo(() => {
|
||||||
|
let thisNote = main.notes.find(a => a.id === id);
|
||||||
|
|
||||||
|
if (thisNote) {
|
||||||
|
let otherSubs = new Subscriptions();
|
||||||
|
otherSubs.Id = "thread-related";
|
||||||
|
for (let e of thisNote.tags.filter(a => a[0] === "e")) {
|
||||||
|
otherSubs.Ids.add(e[1]);
|
||||||
}
|
}
|
||||||
for (let t of n.tags) {
|
// no #e skip related
|
||||||
if (t[0] === "p" && t[1]) {
|
if (otherSubs.Ids.size === 0) {
|
||||||
keys.push(t[1]);
|
return null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let relatedSubs = new Subscriptions();
|
||||||
|
relatedSubs.Kinds.add(EventKind.Reaction);
|
||||||
|
relatedSubs.Kinds.add(EventKind.TextNote);
|
||||||
|
relatedSubs.ETags = otherSubs.Ids;
|
||||||
|
|
||||||
|
otherSubs.AddSubscription(relatedSubs);
|
||||||
|
return otherSubs;
|
||||||
}
|
}
|
||||||
|
}, [main.notes]);
|
||||||
|
|
||||||
dispatch(addPubKey(keys));
|
const others = useSubscription(relatedThisSub, { leaveOpen: true });
|
||||||
}, [notes]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
return {
|
||||||
if (system) {
|
notes: [
|
||||||
let sub = new Subscriptions();
|
...main.notes,
|
||||||
let thisNote = notes.find(a => a.id === id);
|
...others.notes
|
||||||
if (thisNote && !thisLoaded) {
|
]
|
||||||
console.debug(notes);
|
};
|
||||||
setThisLoaded(true);
|
|
||||||
|
|
||||||
let thisNote = Event.FromObject(notes[0]);
|
|
||||||
let thread = thisNote.GetThread();
|
|
||||||
if (thread !== null) {
|
|
||||||
if (thread.ReplyTo) {
|
|
||||||
sub.Ids.add(thread.ReplyTo.Event);
|
|
||||||
}
|
|
||||||
if (thread.Root) {
|
|
||||||
sub.Ids.add(thread.Root.Event);
|
|
||||||
}
|
|
||||||
for (let m of thread.Mentions) {
|
|
||||||
sub.Ids.add(m.Event);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// this event is a root note, no other notes need to be loaded
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (notes.length === 0) {
|
|
||||||
sub.Ids.add(id);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get replies to this event
|
|
||||||
let subRelated = new Subscriptions();
|
|
||||||
subRelated.ETags = sub.Ids;
|
|
||||||
sub.AddSubscription(subRelated);
|
|
||||||
|
|
||||||
sub.OnEvent = (e) => {
|
|
||||||
dispatch(addNote(e));
|
|
||||||
};
|
|
||||||
system.AddSubscription(sub);
|
|
||||||
}
|
|
||||||
}, [system, notes]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(reset());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { notes };
|
|
||||||
}
|
}
|
@ -1,38 +1,21 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { NostrContext } from "..";
|
|
||||||
import EventKind from "../nostr/EventKind";
|
import EventKind from "../nostr/EventKind";
|
||||||
import { Subscriptions } from "../nostr/Subscriptions";
|
import { Subscriptions } from "../nostr/Subscriptions";
|
||||||
|
import useSubscription from "./Subscription";
|
||||||
|
|
||||||
export default function useTimelineFeed(pubKeys) {
|
export default function useTimelineFeed(pubKeys) {
|
||||||
const system = useContext(NostrContext);
|
const sub = useMemo(() => {
|
||||||
const [notes, setNotes] = useState([]);
|
let sub = new Subscriptions();
|
||||||
|
sub.Id = "timeline";
|
||||||
|
sub.Authors = new Set(pubKeys);
|
||||||
|
sub.Kinds.add(EventKind.TextNote);
|
||||||
|
sub.Limit = 10;
|
||||||
|
|
||||||
useEffect(() => {
|
return sub;
|
||||||
if (system && pubKeys.length > 0) {
|
}, [pubKeys]);
|
||||||
const sub = new Subscriptions();
|
|
||||||
sub.Authors = new Set(pubKeys);
|
|
||||||
sub.Kinds.add(EventKind.TextNote);
|
|
||||||
sub.Limit = 10;
|
|
||||||
|
|
||||||
sub.OnEvent = (e) => {
|
const { notes } = useSubscription(sub, { leaveOpen: true });
|
||||||
setNotes(n => {
|
|
||||||
if (Array.isArray(n) && !n.some(a => a.id === e.id)) {
|
|
||||||
return [
|
|
||||||
...n,
|
|
||||||
e
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
system.AddSubscription(sub);
|
|
||||||
return () => {
|
|
||||||
system.RemoveSubscription(sub.Id);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [system, pubKeys]);
|
|
||||||
|
|
||||||
return { notes };
|
return { notes };
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { NostrContext } from "..";
|
import { System } from "..";
|
||||||
import Event from "../nostr/Event";
|
import Event from "../nostr/Event";
|
||||||
import EventKind from "../nostr/EventKind";
|
import EventKind from "../nostr/EventKind";
|
||||||
import { Subscriptions } from "../nostr/Subscriptions";
|
import { Subscriptions } from "../nostr/Subscriptions";
|
||||||
@ -8,7 +8,6 @@ import { setUserData } from "../state/Users";
|
|||||||
|
|
||||||
export default function useUsersCache() {
|
export default function useUsersCache() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const system = useContext(NostrContext);
|
|
||||||
const pKeys = useSelector(s => s.users.pubKeys);
|
const pKeys = useSelector(s => s.users.pubKeys);
|
||||||
const users = useSelector(s => s.users.users);
|
const users = useSelector(s => s.users.users);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -35,15 +34,16 @@ export default function useUsersCache() {
|
|||||||
if (needProfiles.length === 0) {
|
if (needProfiles.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.debug("Need profiles: ", needProfiles);
|
|
||||||
let sub = new Subscriptions();
|
let sub = new Subscriptions();
|
||||||
|
sub.Id = "profiles";
|
||||||
sub.Authors = new Set(needProfiles);
|
sub.Authors = new Set(needProfiles);
|
||||||
sub.Kinds.add(EventKind.SetMetadata);
|
sub.Kinds.add(EventKind.SetMetadata);
|
||||||
sub.OnEvent = (ev) => {
|
sub.OnEvent = (ev) => {
|
||||||
dispatch(setUserData(mapEventToProfile(ev)));
|
dispatch(setUserData(mapEventToProfile(ev)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let events = await system.RequestSubscription(sub);
|
let events = await System.RequestSubscription(sub);
|
||||||
let profiles = events
|
let profiles = events
|
||||||
.filter(a => a.kind === EventKind.SetMetadata)
|
.filter(a => a.kind === EventKind.SetMetadata)
|
||||||
.map(mapEventToProfile);
|
.map(mapEventToProfile);
|
||||||
@ -61,14 +61,14 @@ export default function useUsersCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (system && pKeys.length > 0 && !loading) {
|
if (pKeys.length > 0 && !loading) {
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
getUsers()
|
getUsers()
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
.then(() => setLoading(false));
|
.then(() => setLoading(false));
|
||||||
}
|
}
|
||||||
}, [system, pKeys, loading]);
|
}, [pKeys, loading]);
|
||||||
|
|
||||||
return { users };
|
return { users };
|
||||||
}
|
}
|
@ -145,8 +145,9 @@ div.form-group > div:first-child {
|
|||||||
margin-top: 5vh;
|
margin-top: 5vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ReactModal__Body--open {
|
body.scroll-lock {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr10 {
|
.mr10 {
|
||||||
|
32
src/index.js
32
src/index.js
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@ -17,26 +16,25 @@ import LoginPage from './pages/Login';
|
|||||||
import ProfilePage from './pages/ProfilePage';
|
import ProfilePage from './pages/ProfilePage';
|
||||||
import RootPage from './pages/Root';
|
import RootPage from './pages/Root';
|
||||||
import Store from "./state/Store";
|
import Store from "./state/Store";
|
||||||
|
import NotificationsPage from './pages/Notifications';
|
||||||
|
|
||||||
const System = new NostrSystem();
|
export const System = new NostrSystem();
|
||||||
export const NostrContext = React.createContext();
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<NostrContext.Provider value={System}>
|
<Provider store={Store}>
|
||||||
<Provider store={Store}>
|
<Router>
|
||||||
<Router>
|
<Layout>
|
||||||
<Layout>
|
<Routes>
|
||||||
<Routes>
|
<Route path="/" exact element={<RootPage />} />
|
||||||
<Route path="/" exact element={<RootPage/>} />
|
<Route path="/login" exact element={<LoginPage />} />
|
||||||
<Route path="/login" exact element={<LoginPage />} />
|
<Route path="/e/:id" exact element={<EventPage />} />
|
||||||
<Route path="/e/:id" exact element={<EventPage />} />
|
<Route path="/p/:id" exact element={<ProfilePage />} />
|
||||||
<Route path="/p/:id" exact element={<ProfilePage />} />
|
<Route path="/notifications" exact element={<NotificationsPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Router>
|
</Router>
|
||||||
</Provider>
|
</Provider>
|
||||||
</NostrContext.Provider>
|
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Subscriptions } from "./Subscriptions";
|
import { Subscriptions } from "./Subscriptions";
|
||||||
import Event from "./Event";
|
import Event from "./Event";
|
||||||
|
import * as secp from "@noble/secp256k1";
|
||||||
|
|
||||||
const DefaultConnectTimeout = 1000;
|
const DefaultConnectTimeout = 1000;
|
||||||
|
|
||||||
@ -16,15 +17,11 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Connect() {
|
Connect() {
|
||||||
try {
|
this.Socket = new WebSocket(this.Address);
|
||||||
this.Socket = new WebSocket(this.Address);
|
this.Socket.onopen = (e) => this.OnOpen(e);
|
||||||
this.Socket.onopen = (e) => this.OnOpen(e);
|
this.Socket.onmessage = (e) => this.OnMessage(e);
|
||||||
this.Socket.onmessage = (e) => this.OnMessage(e);
|
this.Socket.onerror = (e) => this.OnError(e);
|
||||||
this.Socket.onerror = (e) => this.OnError(e);
|
this.Socket.onclose = (e) => this.OnClose(e);
|
||||||
this.Socket.onclose = (e) => this.OnClose(e);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`[${this.Address}] Connect failed!`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OnOpen(e) {
|
OnOpen(e) {
|
||||||
@ -82,6 +79,9 @@ export default class Connection {
|
|||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
*/
|
*/
|
||||||
SendEvent(e) {
|
SendEvent(e) {
|
||||||
|
if (!this.Write) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let req = ["EVENT", e.ToObject()];
|
let req = ["EVENT", e.ToObject()];
|
||||||
this._SendJson(req);
|
this._SendJson(req);
|
||||||
}
|
}
|
||||||
@ -91,11 +91,20 @@ export default class Connection {
|
|||||||
* @param {Subscriptions | Array<Subscriptions>} sub Subscriptions object
|
* @param {Subscriptions | Array<Subscriptions>} sub Subscriptions object
|
||||||
*/
|
*/
|
||||||
AddSubscription(sub) {
|
AddSubscription(sub) {
|
||||||
|
if (!this.Read) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let subObj = sub.ToObject();
|
let subObj = sub.ToObject();
|
||||||
if (Object.keys(subObj).length === 0) {
|
if (Object.keys(subObj).length === 0) {
|
||||||
debugger;
|
debugger;
|
||||||
throw "CANNOT SEND EMPTY SUB - FIX ME";
|
throw "CANNOT SEND EMPTY SUB - FIX ME";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.Subscriptions[sub.Id]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let req = ["REQ", sub.Id, subObj];
|
let req = ["REQ", sub.Id, subObj];
|
||||||
if (sub.OrSubs.length > 0) {
|
if (sub.OrSubs.length > 0) {
|
||||||
req = [
|
req = [
|
||||||
@ -133,7 +142,13 @@ export default class Connection {
|
|||||||
|
|
||||||
_OnEvent(subId, ev) {
|
_OnEvent(subId, ev) {
|
||||||
if (this.Subscriptions[subId]) {
|
if (this.Subscriptions[subId]) {
|
||||||
this.Subscriptions[subId].OnEvent(ev);
|
this._VerifySig(ev)
|
||||||
|
.then((e) => {
|
||||||
|
if (this.Subscriptions[subId]) {
|
||||||
|
this.Subscriptions[subId].OnEvent(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`No subscription for event! ${subId}`);
|
console.warn(`No subscription for event! ${subId}`);
|
||||||
}
|
}
|
||||||
@ -148,4 +163,23 @@ export default class Connection {
|
|||||||
console.warn(`No subscription for end! ${subId}`);
|
console.warn(`No subscription for end! ${subId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _VerifySig(ev) {
|
||||||
|
let payload = [
|
||||||
|
0,
|
||||||
|
ev.pubkey,
|
||||||
|
ev.created_at,
|
||||||
|
ev.kind,
|
||||||
|
ev.tags,
|
||||||
|
ev.content
|
||||||
|
];
|
||||||
|
|
||||||
|
let payloadData = new TextEncoder().encode(JSON.stringify(payload));
|
||||||
|
let data = await secp.utils.sha256(payloadData);
|
||||||
|
let hash = secp.utils.bytesToHex(data);
|
||||||
|
if (!await secp.schnorr.verify(ev.sig, hash, ev.pubkey)) {
|
||||||
|
throw "Sig verify failed";
|
||||||
|
}
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
import "./Layout.css";
|
import "./Layout.css";
|
||||||
import { useContext, useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { faBell } from "@fortawesome/free-solid-svg-icons";
|
import { faBell } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
import { NostrContext } from ".."
|
import { System } from ".."
|
||||||
import ProfileImage from "../element/ProfileImage";
|
import ProfileImage from "../element/ProfileImage";
|
||||||
import { init } from "../state/Login";
|
import { init } from "../state/Login";
|
||||||
import useLoginFeed from "../feed/LoginFeed";
|
import useLoginFeed from "../feed/LoginFeed";
|
||||||
@ -13,20 +13,20 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
export default function Layout(props) {
|
export default function Layout(props) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const system = useContext(NostrContext);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const key = useSelector(s => s.login.publicKey);
|
const key = useSelector(s => s.login.publicKey);
|
||||||
const relays = useSelector(s => s.login.relays);
|
const relays = useSelector(s => s.login.relays);
|
||||||
|
const notifications = useSelector(s => s.login.notifications);
|
||||||
useUsersCache();
|
useUsersCache();
|
||||||
useLoginFeed();
|
useLoginFeed();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (system && relays) {
|
if (relays) {
|
||||||
for (let [k, v] of Object.entries(relays)) {
|
for (let [k, v] of Object.entries(relays)) {
|
||||||
system.ConnectToRelay(k, v);
|
System.ConnectToRelay(k, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [relays, system]);
|
}, [relays]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(init());
|
dispatch(init());
|
||||||
@ -35,8 +35,9 @@ export default function Layout(props) {
|
|||||||
function accountHeader() {
|
function accountHeader() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="btn btn-rnd notifications">
|
<div className="btn btn-rnd notifications" onClick={() => navigate("/notifications")}>
|
||||||
<FontAwesomeIcon icon={faBell} size="xl" />
|
<FontAwesomeIcon icon={faBell} size="xl" />
|
||||||
|
{notifications?.length ?? 0}
|
||||||
</div>
|
</div>
|
||||||
<ProfileImage pubKey={key} />
|
<ProfileImage pubKey={key} />
|
||||||
</>
|
</>
|
||||||
|
6
src/pages/Notifications.js
Normal file
6
src/pages/Notifications.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default function NotificationsPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -13,7 +13,7 @@ import useEventPublisher from "../feed/EventPublisher";
|
|||||||
import useTimelineFeed from "../feed/TimelineFeed";
|
import useTimelineFeed from "../feed/TimelineFeed";
|
||||||
import Note from "../element/Note";
|
import Note from "../element/Note";
|
||||||
import QRCodeStyling from "qr-code-styling";
|
import QRCodeStyling from "qr-code-styling";
|
||||||
import ReactModal from "react-modal";
|
import Modal from "../element/Modal";
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -65,7 +65,7 @@ export default function ProfilePage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (qrRef.current && showLnQr) {
|
if (qrRef.current && showLnQr) {
|
||||||
let qr = new QRCodeStyling({
|
let qr = new QRCodeStyling({
|
||||||
data: "",
|
data: {lud16},
|
||||||
type: "canvas"
|
type: "canvas"
|
||||||
});
|
});
|
||||||
qrRef.current.innerHTML = "";
|
qrRef.current.innerHTML = "";
|
||||||
@ -143,9 +143,10 @@ export default function ProfilePage() {
|
|||||||
<div> ⚡️ {lud16}</div>
|
<div> ⚡️ {lud16}</div>
|
||||||
</div> : null}
|
</div> : null}
|
||||||
{showLnQr === true ?
|
{showLnQr === true ?
|
||||||
<ReactModal isOpen={showLnQr} onRequestClose={() => setShowLnQr(false)} overlayClassName="modal" className="modal-content" preventScroll={true}>
|
<Modal onClose={() => setShowLnQr(false)}>
|
||||||
<div ref={qrRef}>QR</div>
|
<h4>{lud16}</h4>
|
||||||
</ReactModal> : null}
|
<div ref={qrRef}></div>
|
||||||
|
</Modal> : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,11 @@ const LoginSlice = createSlice({
|
|||||||
* Login keys are managed by extension
|
* Login keys are managed by extension
|
||||||
*/
|
*/
|
||||||
nip07: false,
|
nip07: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifications for this login session
|
||||||
|
*/
|
||||||
|
notifications: []
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
init: (state) => {
|
init: (state) => {
|
||||||
@ -40,7 +45,6 @@ const LoginSlice = createSlice({
|
|||||||
state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(state.privateKey, true));
|
state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(state.privateKey, true));
|
||||||
}
|
}
|
||||||
state.relays = {
|
state.relays = {
|
||||||
"wss://beta.nostr.v0l.io": { read: true, write: true },
|
|
||||||
"wss://nostr.v0l.io": { read: true, write: true },
|
"wss://nostr.v0l.io": { read: true, write: true },
|
||||||
"wss://relay.damus.io": { read: true, write: true },
|
"wss://relay.damus.io": { read: true, write: true },
|
||||||
"wss://nostr-pub.wellorder.net": { read: true, write: true }
|
"wss://nostr-pub.wellorder.net": { read: true, write: true }
|
||||||
@ -48,7 +52,7 @@ const LoginSlice = createSlice({
|
|||||||
|
|
||||||
// check nip07 pub key
|
// check nip07 pub key
|
||||||
let nip07PubKey = window.localStorage.getItem(Nip07PublicKeyItem);
|
let nip07PubKey = window.localStorage.getItem(Nip07PublicKeyItem);
|
||||||
if(nip07PubKey && !state.privateKey) {
|
if (nip07PubKey && !state.privateKey) {
|
||||||
state.publicKey = nip07PubKey;
|
state.publicKey = nip07PubKey;
|
||||||
state.nip07 = true;
|
state.nip07 = true;
|
||||||
}
|
}
|
||||||
@ -72,6 +76,21 @@ const LoginSlice = createSlice({
|
|||||||
setFollows: (state, action) => {
|
setFollows: (state, action) => {
|
||||||
state.follows = action.payload;
|
state.follows = action.payload;
|
||||||
},
|
},
|
||||||
|
addNotifications: (state, action) => {
|
||||||
|
let n = action.payload;
|
||||||
|
if (!Array.isArray(n)) {
|
||||||
|
n = [n];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let x in n) {
|
||||||
|
if (!state.notifications.some(a => a.id === x.id)) {
|
||||||
|
state.notifications.push(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.notifications = [
|
||||||
|
...state.notifications
|
||||||
|
];
|
||||||
|
},
|
||||||
logout: (state) => {
|
logout: (state) => {
|
||||||
state.privateKey = null;
|
state.privateKey = null;
|
||||||
window.localStorage.removeItem(PrivateKeyItem);
|
window.localStorage.removeItem(PrivateKeyItem);
|
||||||
@ -79,5 +98,5 @@ const LoginSlice = createSlice({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { init, setPrivateKey, setPublicKey, setNip07PubKey, setRelays, setFollows, logout } = LoginSlice.actions;
|
export const { init, setPrivateKey, setPublicKey, setNip07PubKey, setRelays, setFollows, addNotifications, logout } = LoginSlice.actions;
|
||||||
export const reducer = LoginSlice.reducer;
|
export const reducer = LoginSlice.reducer;
|
@ -1,13 +1,11 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import { reducer as UsersReducer } from "./Users";
|
import { reducer as UsersReducer } from "./Users";
|
||||||
import { reducer as LoginReducer } from "./Login";
|
import { reducer as LoginReducer } from "./Login";
|
||||||
import { reducer as ThreadReducer } from "./Thread";
|
|
||||||
|
|
||||||
const Store = configureStore({
|
const Store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
users: UsersReducer,
|
users: UsersReducer,
|
||||||
login: LoginReducer,
|
login: LoginReducer
|
||||||
thread: ThreadReducer
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit'
|
|
||||||
|
|
||||||
const ThreadSlice = createSlice({
|
|
||||||
name: "Thread",
|
|
||||||
initialState: {
|
|
||||||
notes: [],
|
|
||||||
},
|
|
||||||
reducers: {
|
|
||||||
setNotes: (state, action) => {
|
|
||||||
state.notes = action.payload;
|
|
||||||
},
|
|
||||||
addNote: (state, action) => {
|
|
||||||
if (!state.notes.some(n => n.id === action.payload.id)) {
|
|
||||||
let tmp = new Set(state.notes);
|
|
||||||
tmp.add(action.payload);
|
|
||||||
state.notes = Array.from(tmp);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
reset: (state) => {
|
|
||||||
state.notes = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { setNotes, addNote, reset } = ThreadSlice.actions;
|
|
||||||
export const reducer = ThreadSlice.reducer;
|
|
31
yarn.lock
31
yarn.lock
@ -4124,11 +4124,6 @@ execa@^5.0.0:
|
|||||||
signal-exit "^3.0.3"
|
signal-exit "^3.0.3"
|
||||||
strip-final-newline "^2.0.0"
|
strip-final-newline "^2.0.0"
|
||||||
|
|
||||||
exenv@^1.2.0:
|
|
||||||
version "1.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
|
|
||||||
integrity sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==
|
|
||||||
|
|
||||||
exit@^0.1.2:
|
exit@^0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
|
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
|
||||||
@ -5843,7 +5838,7 @@ lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||||
@ -7035,7 +7030,7 @@ prompts@^2.0.1, prompts@^2.4.2:
|
|||||||
kleur "^3.0.3"
|
kleur "^3.0.3"
|
||||||
sisteransi "^1.0.5"
|
sisteransi "^1.0.5"
|
||||||
|
|
||||||
prop-types@^15.7.2, prop-types@^15.8.1:
|
prop-types@^15.8.1:
|
||||||
version "15.8.1"
|
version "15.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
@ -7200,21 +7195,6 @@ react-is@^18.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
||||||
|
|
||||||
react-lifecycles-compat@^3.0.0:
|
|
||||||
version "3.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
|
||||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
|
||||||
|
|
||||||
react-modal@^3.16.1:
|
|
||||||
version "3.16.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.16.1.tgz#34018528fc206561b1a5467fc3beeaddafb39b2b"
|
|
||||||
integrity sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==
|
|
||||||
dependencies:
|
|
||||||
exenv "^1.2.0"
|
|
||||||
prop-types "^15.7.2"
|
|
||||||
react-lifecycles-compat "^3.0.0"
|
|
||||||
warning "^4.0.3"
|
|
||||||
|
|
||||||
react-redux@^8.0.5:
|
react-redux@^8.0.5:
|
||||||
version "8.0.5"
|
version "8.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd"
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd"
|
||||||
@ -8511,13 +8491,6 @@ walker@^1.0.7:
|
|||||||
dependencies:
|
dependencies:
|
||||||
makeerror "1.0.12"
|
makeerror "1.0.12"
|
||||||
|
|
||||||
warning@^4.0.3:
|
|
||||||
version "4.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
|
||||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
|
||||||
dependencies:
|
|
||||||
loose-envify "^1.0.0"
|
|
||||||
|
|
||||||
watchpack@^2.4.0:
|
watchpack@^2.4.0:
|
||||||
version "2.4.0"
|
version "2.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
||||||
|
Loading…
Reference in New Issue
Block a user