Squashed commit of the following:
commit 52ca4c48661f69e0885fda9bfca7b3171b9e6a36
Author: Kamal Raj Sekar <notify.kamalraj@gmail.com>
Date: Thu Nov 30 16:41:45 2023 +0000
compose the api call in debounce and useeffect
commit fc6a933643ad7f4ac26851eccec080f81e5a84d9
Author: Kamal Raj Sekar <notify.kamalraj@gmail.com>
Date: Thu Nov 30 03:32:21 2023 +0000
useeffect for lud16 verification, reuse nip05verifier and some cleanup
commit 0516b38a2f074e1d5457e26f484305410cfe102c
Merge: 202eaa07 aaa56738
Author: Kamal Raj Sekar <notify.kamalraj@gmail.com>
Date: Tue Nov 21 06:04:01 2023 +0000
Merge branch 'main' into enhancements/593-validation
commit 202eaa0773b19ae782381ac8e21c4a8200c57b26
Author: Kamal Raj Sekar <notify.kamalraj@gmail.com>
Date: Tue Nov 21 05:49:15 2023 +0000
Lud16 test and some clean up
commit 169596288d77e6eaa1998b5b8ec2b6944e240ae4
Author: Kamal Raj Sekar <notify.kamalraj@gmail.com>
Date: Mon Nov 20 16:15:52 2023 +0000
username and about length validation
commit d150a0622cfc90650d2342587c2d5d513085fe01
Author: Kamal Raj Sekar <notify.kamalraj@gmail.com>
Date: Mon Nov 20 15:38:45 2023 +0000
verify nostr address - nip05
This commit is contained in:
@ -155,3 +155,13 @@ export const WavlakeRegex =
|
|||||||
* Regex to match any base64 string
|
* Regex to match any base64 string
|
||||||
*/
|
*/
|
||||||
export const CashuRegex = /(cashuA[A-Za-z0-9_-]{0,10000}={0,3})/i;
|
export const CashuRegex = /(cashuA[A-Za-z0-9_-]{0,10000}={0,3})/i;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Max username length - profile/settings
|
||||||
|
*/
|
||||||
|
export const MaxUsernameLength = 100;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Max about length - profile/settings
|
||||||
|
*/
|
||||||
|
export const MaxAboutLength = 1000;
|
||||||
|
@ -101,4 +101,9 @@ export default defineMessages({
|
|||||||
ReBroadcast: { defaultMessage: "Broadcast Again", id: "c3g2hL" },
|
ReBroadcast: { defaultMessage: "Broadcast Again", id: "c3g2hL" },
|
||||||
IrisUserNameLengthError: { defaultMessage: "Name must be between 1 and 32 characters", id: "4MBtMa" },
|
IrisUserNameLengthError: { defaultMessage: "Name must be between 1 and 32 characters", id: "4MBtMa" },
|
||||||
IrisUserNameFormatError: { defaultMessage: "Username must only contain lowercase letters and numbers", id: "RSr2uB" },
|
IrisUserNameFormatError: { defaultMessage: "Username must only contain lowercase letters and numbers", id: "RSr2uB" },
|
||||||
|
InvalidNip05Address: { defaultMessage: "Invalid Nostr Address(nip05)", id: 'X6TK3B' },
|
||||||
|
ErrorValidatingNip05Address: { defaultMessage: "Cannot verify Nostr Address(nip05)", id: 'I32UtP'},
|
||||||
|
UserNameLengthError: { defaultMessage: "Name must be less than 100 characters", id: 'X/GZY6' },
|
||||||
|
AboutLengthError: { defaultMessage: "About must be less than 1000 characters", id: 'vCanok' },
|
||||||
|
InvalidLud16:{ defaultMessage: "Invalid Lightning Address", id: 'GqQeu/' }
|
||||||
});
|
});
|
||||||
|
@ -5,15 +5,19 @@ import { mapEventToProfile } from "@snort/system";
|
|||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import { openFile } from "@/SnortUtils";
|
import { openFile, debounce } from "@/SnortUtils";
|
||||||
import useFileUpload from "@/Upload";
|
import useFileUpload from "@/Upload";
|
||||||
import AsyncButton from "@/Element/Button/AsyncButton";
|
import AsyncButton from "@/Element/Button/AsyncButton";
|
||||||
import { UserCache } from "@/Cache";
|
import { UserCache } from "@/Cache";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import Icon from "@/Icons/Icon";
|
import Icon from "@/Icons/Icon";
|
||||||
import Avatar from "@/Element/User/Avatar";
|
import Avatar from "@/Element/User/Avatar";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { ErrorOrOffline } from "@/Element/ErrorOrOffline";
|
import { ErrorOrOffline } from "@/Element/ErrorOrOffline";
|
||||||
|
import { LNURL, LNURLError } from '@snort/shared';
|
||||||
|
import messages from "@/Element/messages";
|
||||||
|
import { MaxAboutLength, MaxUsernameLength } from "@/Const";
|
||||||
|
import { fetchNip05Pubkey } from "../../Nip05/Verifier";
|
||||||
|
|
||||||
export interface ProfileSettingsProps {
|
export interface ProfileSettingsProps {
|
||||||
avatar?: boolean;
|
avatar?: boolean;
|
||||||
@ -22,6 +26,7 @@ export interface ProfileSettingsProps {
|
|||||||
|
|
||||||
export default function ProfileSettings(props: ProfileSettingsProps) {
|
export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const { publicKey: id, readonly } = useLogin(s => ({ publicKey: s.publicKey, readonly: s.readonly }));
|
const { publicKey: id, readonly } = useLogin(s => ({ publicKey: s.publicKey, readonly: s.readonly }));
|
||||||
const user = useUserProfile(id ?? "");
|
const user = useUserProfile(id ?? "");
|
||||||
const { publisher, system } = useEventPublisher();
|
const { publisher, system } = useEventPublisher();
|
||||||
@ -35,7 +40,16 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
const [website, setWebsite] = useState<string>();
|
const [website, setWebsite] = useState<string>();
|
||||||
const [nip05, setNip05] = useState<string>();
|
const [nip05, setNip05] = useState<string>();
|
||||||
const [lud16, setLud16] = useState<string>();
|
const [lud16, setLud16] = useState<string>();
|
||||||
|
const [nip05AddressValid, setNip05AddressValid] = useState<boolean>();
|
||||||
|
const [invalidNip05AddressMessage, setInvalidNip05AddressMessage] = useState<null | string>();
|
||||||
|
const [usernameValid, setUsernameValid] = useState<boolean>();
|
||||||
|
const [invalidUsernameMessage, setInvalidUsernameMessage] = useState<null | string>();
|
||||||
|
const [aboutValid, setAboutValid] = useState<boolean>();
|
||||||
|
const [invalidAboutMessage, setInvalidAboutMessage] = useState<null | string>();
|
||||||
|
const [lud16Valid, setLud16Valid] = useState<boolean>();
|
||||||
|
const [invalidLud16Message, setInvalidLud16Message] = useState<null | string>();
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
setName(user.name);
|
setName(user.name);
|
||||||
@ -48,6 +62,45 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return debounce(500,async () => {
|
||||||
|
if(lud16){
|
||||||
|
try {
|
||||||
|
await new LNURL(lud16).load();
|
||||||
|
setLud16Valid(true);
|
||||||
|
setInvalidLud16Message("")
|
||||||
|
}catch(e){
|
||||||
|
setLud16Valid(false);
|
||||||
|
if(e instanceof LNURLError)
|
||||||
|
setInvalidLud16Message((e as LNURLError).message);
|
||||||
|
else
|
||||||
|
setInvalidLud16Message(formatMessage(messages.InvalidLud16));
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
setInvalidLud16Message("")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [lud16]);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
return debounce(500,async () => {
|
||||||
|
const Nip05AddressElements = nip05?.split('@') ?? [];
|
||||||
|
if (nip05.length === 0) {
|
||||||
|
setNip05AddressValid(false)
|
||||||
|
setInvalidNip05AddressMessage("")
|
||||||
|
}else if(Nip05AddressElements.length < 2){
|
||||||
|
setNip05AddressValid(false)
|
||||||
|
setInvalidNip05AddressMessage(formatMessage(messages.InvalidNip05Address))
|
||||||
|
}
|
||||||
|
else if(Nip05AddressElements.length === 2){
|
||||||
|
nip05NostrAddressVerification(Nip05AddressElements.pop(), Nip05AddressElements.pop());
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
setNip05AddressValid(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},[nip05])
|
||||||
|
|
||||||
async function saveProfile() {
|
async function saveProfile() {
|
||||||
// copy user object and delete internal fields
|
// copy user object and delete internal fields
|
||||||
const userCopy = {
|
const userCopy = {
|
||||||
@ -112,6 +165,53 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onNip05Change(e: React.ChangeEvent<HTMLInputElement>){
|
||||||
|
const Nip05Address = e.target.value.toLowerCase();
|
||||||
|
setNip05(Nip05Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onLimitCheck(val:string, field:string){
|
||||||
|
if(field === "username"){
|
||||||
|
setName(val);
|
||||||
|
if(val?.length >= MaxUsernameLength){
|
||||||
|
setUsernameValid(false)
|
||||||
|
setInvalidUsernameMessage(formatMessage(messages.UserNameLengthError))
|
||||||
|
}else{
|
||||||
|
setUsernameValid(true)
|
||||||
|
setInvalidUsernameMessage("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(field === "about"){
|
||||||
|
setAbout(val);
|
||||||
|
if(val?.length >= MaxAboutLength){
|
||||||
|
setAboutValid(false)
|
||||||
|
setInvalidAboutMessage(formatMessage(messages.AboutLengthError))
|
||||||
|
}else{
|
||||||
|
setAboutValid(true)
|
||||||
|
setInvalidAboutMessage("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function nip05NostrAddressVerification(nip05Domain: string | undefined, nip05Name: string| undefined) {
|
||||||
|
try{
|
||||||
|
const result = await fetchNip05Pubkey(nip05Name!,nip05Domain!);
|
||||||
|
if(result){
|
||||||
|
setNip05AddressValid(true);
|
||||||
|
}else{
|
||||||
|
setNip05AddressValid(false);
|
||||||
|
setInvalidNip05AddressMessage(formatMessage(messages.InvalidNip05Address))
|
||||||
|
}
|
||||||
|
}catch(e){
|
||||||
|
setNip05AddressValid(false)
|
||||||
|
setInvalidNip05AddressMessage(formatMessage(messages.InvalidNip05Address))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onLud16Change(address:string){
|
||||||
|
setLud16(address)
|
||||||
|
}
|
||||||
|
|
||||||
function editor() {
|
function editor() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col g24">
|
<div className="flex flex-col g24">
|
||||||
@ -123,9 +223,17 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
className="w-max"
|
className="w-max"
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={e => setName(e.target.value)}
|
onChange={e => onLimitCheck(e.target.value,"username")}
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
|
maxLength={MaxUsernameLength}
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
|
{usernameValid === false ? (
|
||||||
|
<span className="error">{invalidUsernameMessage}</span>
|
||||||
|
)
|
||||||
|
: (<></>)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-max g8">
|
<div className="flex flex-col w-max g8">
|
||||||
<h4>
|
<h4>
|
||||||
@ -133,9 +241,16 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
</h4>
|
</h4>
|
||||||
<textarea
|
<textarea
|
||||||
className="w-max"
|
className="w-max"
|
||||||
onChange={e => setAbout(e.target.value)}
|
onChange={e => onLimitCheck(e.target.value,"about")}
|
||||||
value={about}
|
value={about}
|
||||||
disabled={readonly}></textarea>
|
disabled={readonly} maxLength={MaxAboutLength}></textarea>
|
||||||
|
<div>
|
||||||
|
{aboutValid === false ? (
|
||||||
|
<span className="error">{invalidAboutMessage}</span>
|
||||||
|
)
|
||||||
|
: (<></>)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-max g8">
|
<div className="flex flex-col w-max g8">
|
||||||
<h4>
|
<h4>
|
||||||
@ -158,9 +273,20 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
type="text"
|
type="text"
|
||||||
className="w-max"
|
className="w-max"
|
||||||
value={nip05}
|
value={nip05}
|
||||||
onChange={e => setNip05(e.target.value)}
|
onChange={e => onNip05Change(e)}
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
|
{nip05AddressValid ? (
|
||||||
|
<>
|
||||||
|
<span className="success">
|
||||||
|
<FormattedMessage defaultMessage="NIP05 Verified" id="vw95kf" />
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="error">{invalidNip05AddressMessage}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<small>
|
<small>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Usernames are not unique on Nostr. The nostr address is your unique human-readable address that is unique to you upon registration."
|
defaultMessage="Usernames are not unique on Nostr. The nostr address is your unique human-readable address that is unique to you upon registration."
|
||||||
@ -188,9 +314,16 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
className="w-max"
|
className="w-max"
|
||||||
type="text"
|
type="text"
|
||||||
value={lud16}
|
value={lud16}
|
||||||
onChange={e => setLud16(e.target.value)}
|
onChange={e => onLud16Change(e.target.value.toLowerCase())}
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
|
{lud16Valid === false ? (
|
||||||
|
<span className="error">{invalidLud16Message}</span>
|
||||||
|
)
|
||||||
|
: (<></>)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton className="primary" onClick={() => saveProfile()} disabled={readonly}>
|
<AsyncButton className="primary" onClick={() => saveProfile()} disabled={readonly}>
|
||||||
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
|
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
|
||||||
|
Reference in New Issue
Block a user