Skip to content

Commit

Permalink
edit source modal and prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
Mothana committed Oct 4, 2024
1 parent 65e61bc commit ecd64a6
Show file tree
Hide file tree
Showing 17 changed files with 705 additions and 174 deletions.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import "@ionic/react/css/display.css";

import LoadingOverlay from "./Components/LoadingOverlay";
import { DebitRequestModal, EditDebitModal } from "./Components/Modals/DebitRequestModal";
import { EditSourceModal } from "./Components/Modals/EditSourceModal";

setupIonicReact();

Expand Down Expand Up @@ -86,6 +87,7 @@ const AppContent: React.FC = () => {
{/* Modals */}
<DebitRequestModal />
<EditDebitModal />
<EditSourceModal />
{/* Modals */}


Expand Down
14 changes: 13 additions & 1 deletion src/Components/Background.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,21 @@ export const Background = () => {
...source,
balance: `${res.balance}`,
maxWithdrawable: `${res.max_withdrawable}`,
ndebit: res.ndebit
ndebit: res.ndebit,
},
meta: { skipChangelog: true }
})
const paySourceToEdit = paySource.sources[source.id]
if (paySourceToEdit && res.bridge_url && !paySourceToEdit.bridgeUrl) {
dispatch({
type: "paySources/editPaySources",
payload: {
...paySourceToEdit,
bridgeUrl: res.bridge_url
},
meta: { skipChangelog: true }
})
}
}
const fetchSourceHistory = useCallback(async (source: SpendFrom, client: NostrClient, sourceId: string, newCurosor?: Partial<Types.GetUserOperationsRequest>, newData?: Types.UserOperation[]) => {
const req = populateCursorRequest(newCurosor || cursor, !!newData)
Expand Down Expand Up @@ -216,6 +227,7 @@ export const Background = () => {
}
nostrSpends.forEach(async s => {
const { pubkey, relays } = parseNprofile(s.pasteField)
console.log({relays})
const client = await getNostrClient({ pubkey, relays }, s.keys)
await fetchSourceHistory(s, client, s.id)
})
Expand Down
3 changes: 2 additions & 1 deletion src/Components/BackgroundJobs/NewSourceCheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useDispatch, useSelector } from "../../State/store"
import { NOSTR_PUB_DESTINATION, NOSTR_RELAYS, OLD_NOSTR_PUB_DESTINATION, options } from "../../constants"
import { addSpendSources } from "../../State/Slices/spendSourcesSlice"
import { nip19 } from "nostr-tools";
import { SourceTrustLevel } from "../../globalTypes";

export const NewSourceCheck = () => {
const spendSource = useSelector(({ spendSource }) => spendSource)
Expand All @@ -28,7 +29,7 @@ export const NewSourceCheck = () => {
id: NOSTR_PUB_DESTINATION,
label: "Bootstrap Node",
pasteField: newProfile,
option: options.little,
option: SourceTrustLevel.LOW,
icon: "0",
balance: "0",
pubSource: true
Expand Down
7 changes: 5 additions & 2 deletions src/Components/Dropdowns/LVDropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import { Period } from "../../../Pages/Metrics";
import useClickOutside from "../../../Hooks/useClickOutside";
import { Interval } from "../../../Pages/Automation";
import { WalletIntervalEnum } from "../../Modals/DebitRequestModal/helpers";
import { SourceTrustLevel } from "../../../globalTypes";


interface Props<T> {
setState: (data: T) => void;
otherOptions: T[];
jsx: React.ReactNode;
className?: string
}

const Dropdown = <T extends "number" | "string" | Period | Interval | WalletIntervalEnum>({ setState, jsx, otherOptions }: Props<T>) => {
const Dropdown = <T extends "number" | "string" | Period | Interval | WalletIntervalEnum | SourceTrustLevel>({ setState, jsx, otherOptions, className }: Props<T>) => {
const [expand, setExpand] = useState(false);
const dropDownRef = useRef<HTMLDivElement>(null);
useClickOutside([dropDownRef], () => setExpand(false), false);
Expand All @@ -35,7 +38,7 @@ const Dropdown = <T extends "number" | "string" | Period | Interval | WalletInte
{expand && (
<AnimatePresence>
<motion.div
className={styles["options"]}
className={classNames(styles["options"], className)}
initial={{ top: "-100%" }}
animate={{ top: "100%" }}
exit={{ top: "-100%" }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@


padding: 12px 21px;
/* height: 1.5rem; */
&:hover {
background-color: #4285b9;

Expand Down
270 changes: 270 additions & 0 deletions src/Components/Modals/EditSourceModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@

import { useDispatch, useSelector } from "../../../State/store";
import { Modal } from "../Modal";

import styles from "./styles/index.module.scss";
import { CrossIcon, ShieldIcon, TrashIcon } from '../../../Assets/SvgIconLibrary';

import { arrangeIcon } from "../../../jsxHelpers";
import Dropdown from "../../Dropdowns/LVDropdown";
import { useCallback, useEffect, useState } from "react";
import { nip19 } from "nostr-tools";
import classNames from "classnames";
import { setSourceToEdit } from "../../../State/Slices/modalsSlice";
import { deletePaySources, editPaySources } from "../../../State/Slices/paySourcesSlice";
import { toast } from "react-toastify";
import Toast from "../../Toast";
import { SourceTrustLevel } from "../../../globalTypes";
import { deleteSpendSources, editSpendSources } from "../../../State/Slices/spendSourcesSlice";
import PromptForActionModal, { ActionType } from "../PromptForActionModal";

const isValidUrl = (url: string) => {
try {
new URL(url);
return true;
} catch {
return false;
}
}






const trustLevelsArray = Object.values(SourceTrustLevel);

const substringNpub = (npub: string) => {
return `${npub.substring(0, 15)}...${npub.substring(npub.length - 15, npub.length)}`;
}



export const EditSourceModal = () => {
const dispatch = useDispatch()
const paySources = useSelector(state => state.paySource)
const spendSources = useSelector(state => state.spendSource)
const sourceToEdit = useSelector(state => state.modalsSlice.sourceToEdit);
const [editValues, setEditValues] = useState({
relay: "",
nameService: "",
trustLevel: SourceTrustLevel.MEDIUM
})

const [initialRelay, setInitialRelay] = useState("")
const [initialNameService, setInitialNameService] = useState("")

const [isPromptConfirmDelete, setIsPromptConfirmDelete] = useState(false);



useEffect(() => {
if (!sourceToEdit) return;
setIsPromptConfirmDelete(false)

let relay = ""
let nameService = ""
if (sourceToEdit.source.pubSource) {
const { type, data } = nip19.decode(sourceToEdit.source.pasteField);
if (type === "nprofile" && data.relays) {
relay = data.relays[0]
setInitialRelay(relay)
}
}
if (sourceToEdit.type === "payTo") {
nameService = sourceToEdit.source.bridgeUrl || ""
setInitialNameService(nameService)
}
setEditValues(state => ({ ...state, nameService, relay }));

}, [sourceToEdit])






const handleSave = useCallback(() => {
if (!sourceToEdit) return;

const shouldUpdate = (editValues.relay !== initialRelay) || (editValues.nameService !== initialNameService) || (editValues.trustLevel !== sourceToEdit.source.option)

let newPasteField = sourceToEdit.source.pasteField

if (editValues.relay !== initialRelay) {
if (!isValidUrl(editValues.relay)) {
toast.error(<Toast title="Relay Error" message="The relay URL you entered is not a valid url" />);
return;
}

newPasteField = nip19.nprofileEncode({ pubkey: sourceToEdit.source.id.split("-")[0], relays: [editValues.relay] })
}

if (editValues.nameService !== initialNameService) {
if (!isValidUrl) {
toast.error(<Toast title="Name Service Error" message="The name service URL you entered is not a valid url" />);
return;
}
}

if (shouldUpdate) {
if (sourceToEdit.type === "payTo") {
dispatch(editPaySources({
...sourceToEdit.source,
pasteField: newPasteField,
option: editValues.trustLevel,
bridgeUrl: editValues.nameService
}))
const counterpartSource = spendSources.sources[sourceToEdit.source.id];
if (counterpartSource) {
dispatch(editSpendSources({
...counterpartSource,
option: editValues.trustLevel,
pasteField: newPasteField,
}))
}
} else {
dispatch(editSpendSources({
...sourceToEdit.source,
pasteField: newPasteField,
option: editValues.trustLevel,
}))

const counterpartSource = paySources.sources[sourceToEdit.source.id];
if (counterpartSource) {
dispatch(editPaySources({
...counterpartSource,
option: editValues.trustLevel,
pasteField: newPasteField,
}))
}
}

toast.success("Source Properties saved successfuly")
}
dispatch(setSourceToEdit(null))
}, [sourceToEdit, editValues, initialNameService, initialRelay, dispatch, spendSources, paySources]);

const handleSourceDelete = useCallback(() => {
if (!sourceToEdit) return;
dispatch(deleteSpendSources(sourceToEdit.source.id))
dispatch(deletePaySources(sourceToEdit.source.id))
dispatch(setSourceToEdit(null))
toast.success(<Toast title="Success" message="Source deleted successfuly" />)
}, [dispatch, sourceToEdit])



const modalContent = sourceToEdit ? (
<div className={styles["container"]}>
<div className={styles["corner-icon"]} onClick={() => setIsPromptConfirmDelete(true)}>
<TrashIcon />
</div>
<div className={styles["modal-header"]}>Source Properties</div>
<div className={styles["modal-body"]}>
<div className={styles["requestor-container"]}>
{
arrangeIcon(sourceToEdit.source.icon)
}
<span className={styles["source-label"]}>{sourceToEdit.source.label}</span>
<Dropdown<SourceTrustLevel>
setState={(option) => setEditValues(state => ({ ...state, trustLevel: option }))}
jsx={
<div className={styles["dropdown-box"]}>{editValues.trustLevel}</div>
}
otherOptions={trustLevelsArray}
className={styles["dropdown-options"]}
/>

</div>
<div className={styles["source-items-container"]}>
{
sourceToEdit.source.pubSource
&&
<>
<div className={styles["item-line"]}>
<span className={styles["item-label"]}>
Source Key:
</span>
<span className={classNames(styles["item-value"], styles["npub"])}>
{substringNpub(nip19.npubEncode(sourceToEdit.source.id.split("-")[0]))}
</span>
</div>
<div className={styles["item-line"]}>
<span className={styles["item-label"]}>
Local Key:
</span>
<span className={classNames(styles["item-value"], styles["npub"])}>
{substringNpub(nip19.npubEncode(sourceToEdit.source.id.split("-")[1]))}
</span>
</div>
<div className={styles["item-line"]}>
<span className={styles["item-label"]}>
Relay:
</span>
<span className={classNames(styles["item-value"], styles["input"])}>
<input value={editValues.relay} onChange={(e) => setEditValues(state => ({ ...state, relay: e.target.value }))} />
</span>
</div>
{
sourceToEdit.type === "payTo"
&&
<div className={styles["item-line"]}>
<span className={styles["item-label"]}>
Name Service:
</span>
<span className={classNames(styles["item-value"], styles["input"])}>
<input value={editValues.nameService} onChange={(e) => setEditValues(state => ({ ...state, nameService: e.target.value }))} />
</span>
</div>
}
{
(sourceToEdit.type === "payTo" && sourceToEdit.source.vanityName)
&&
<span>{sourceToEdit.source.vanityName.split("@")[0]}</span>
}
</>


}
</div>
<div className={styles["buttons-container"]}>
<button onClick={() => dispatch(setSourceToEdit(null))}>
<>
<CrossIcon />
CANCEL
</>
</button>
<button onClick={handleSave}>
<>
<ShieldIcon />
SAVE
</>
</button>
</div>
</div>

</div>
) : <></>;



return (
<>
<Modal isShown={!!sourceToEdit && !isPromptConfirmDelete} hide={() => dispatch(setSourceToEdit(null))} modalContent={modalContent} headerText={''} />
{
isPromptConfirmDelete
&&
<PromptForActionModal
descriptionText="Are you sure you want to delete this source?"
title="Delete Source" actionType={ActionType.DANGER}
closeModal={() => setIsPromptConfirmDelete(false)}
action={handleSourceDelete}
actionText="Delete"
/>

}
</>
)
}

Loading

0 comments on commit ecd64a6

Please sign in to comment.