forked from Kieran/snort
Compare commits
10 Commits
b993c3ff3c
...
f33961232b
Author | SHA1 | Date | |
---|---|---|---|
f33961232b | |||
ecb0f0e78a | |||
fc38049b87 | |||
1d1e8889dc | |||
|
a0824646eb | ||
60326ea17f | |||
c4f40b5c8a | |||
e165ce232a | |||
b239bc65d8 | |||
|
c223c89045 |
@ -1,2 +1,2 @@
|
|||||||
/*
|
/*
|
||||||
Content-Security-Policy: default-src 'self'; manifest-src *; 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 https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://nostrnests.com https://embed.wavlake.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://analytics.v0l.io https://platform.twitter.com https://embed.tidal.com;
|
Content-Security-Policy: default-src 'self'; manifest-src *; 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 https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://nostrnests.com https://embed.wavlake.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://analytics.v0l.io https://platform.twitter.com https://embed.tidal.com https://challenges.cloudflare.com;
|
@ -15,6 +15,11 @@ export const Day = Hour * 24;
|
|||||||
*/
|
*/
|
||||||
export const ApiHost = "https://api.snort.social";
|
export const ApiHost = "https://api.snort.social";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iris api for free nip05 names
|
||||||
|
*/
|
||||||
|
export const IrisHost = "https://api.iris.to";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LibreTranslate endpoint
|
* LibreTranslate endpoint
|
||||||
*/
|
*/
|
||||||
|
@ -49,6 +49,7 @@ export interface NoteProps {
|
|||||||
ignoreModeration?: boolean;
|
ignoreModeration?: boolean;
|
||||||
onClick?: (e: TaggedNostrEvent) => void;
|
onClick?: (e: TaggedNostrEvent) => void;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
|
searchedValue?: string;
|
||||||
options?: {
|
options?: {
|
||||||
showHeader?: boolean;
|
showHeader?: boolean;
|
||||||
showContextMenu?: boolean;
|
showContextMenu?: boolean;
|
||||||
@ -206,6 +207,7 @@ export function NoteInner(props: NoteProps) {
|
|||||||
<Text
|
<Text
|
||||||
id={ev.id}
|
id={ev.id}
|
||||||
content={ev.content}
|
content={ev.content}
|
||||||
|
highlighText={props.searchedValue}
|
||||||
tags={ev.tags}
|
tags={ev.tags}
|
||||||
creator={ev.pubkey}
|
creator={ev.pubkey}
|
||||||
depth={props.depth}
|
depth={props.depth}
|
||||||
@ -222,6 +224,7 @@ export function NoteInner(props: NoteProps) {
|
|||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
id={ev.id}
|
id={ev.id}
|
||||||
|
highlighText={props.searchedValue}
|
||||||
content={body}
|
content={body}
|
||||||
tags={ev.tags}
|
tags={ev.tags}
|
||||||
creator={ev.pubkey}
|
creator={ev.pubkey}
|
||||||
|
@ -107,7 +107,14 @@ const Timeline = (props: TimelineProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{mainFeed.map(e => (
|
{mainFeed.map(e => (
|
||||||
<Note key={e.id} data={e} related={relatedFeed(e.id)} ignoreModeration={props.ignoreModeration} depth={0} />
|
<Note
|
||||||
|
key={e.id}
|
||||||
|
searchedValue={props.subject.discriminator}
|
||||||
|
data={e}
|
||||||
|
related={relatedFeed(e.id)}
|
||||||
|
ignoreModeration={props.ignoreModeration}
|
||||||
|
depth={0}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
{(props.loadMore === undefined || props.loadMore === true) && (
|
{(props.loadMore === undefined || props.loadMore === true) && (
|
||||||
<div className="flex f-center">
|
<div className="flex f-center">
|
||||||
|
5
packages/app/src/Element/HighlightedText.tsx
Normal file
5
packages/app/src/Element/HighlightedText.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const HighlightedText = ({ content }: { content: string }) => {
|
||||||
|
return <strong className="highlighted-text">{content}</strong>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HighlightedText;
|
30
packages/app/src/Element/IrisAccount/AccountName.tsx
Normal file
30
packages/app/src/Element/IrisAccount/AccountName.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function AccountName({ name = "", link = true }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
Username: <b>{name}</b>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Short link:{" "}
|
||||||
|
{link ? (
|
||||||
|
<a
|
||||||
|
href={`https://iris.to/${name}`}
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(`/${name}`);
|
||||||
|
}}>
|
||||||
|
iris.to/{name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<>iris.to/{name}</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Nostr address (nip05): <b>{name}@iris.to</b>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
73
packages/app/src/Element/IrisAccount/ActiveAccount.tsx
Normal file
73
packages/app/src/Element/IrisAccount/ActiveAccount.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import AccountName from "./AccountName";
|
||||||
|
import useLogin from "../../Hooks/useLogin";
|
||||||
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
import { System } from "../../index";
|
||||||
|
import { UserCache } from "../../Cache";
|
||||||
|
import useEventPublisher from "../../Hooks/useEventPublisher";
|
||||||
|
import { mapEventToProfile } from "@snort/system";
|
||||||
|
|
||||||
|
export default function ActiveAccount({ name = "", setAsPrimary = () => {} }) {
|
||||||
|
const { publicKey, readonly } = useLogin(s => ({
|
||||||
|
publicKey: s.publicKey,
|
||||||
|
readonly: s.readonly,
|
||||||
|
}));
|
||||||
|
const profile = useUserProfile(publicKey);
|
||||||
|
const publisher = useEventPublisher();
|
||||||
|
|
||||||
|
async function saveProfile(nip05: string) {
|
||||||
|
if (readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// copy user object and delete internal fields
|
||||||
|
const userCopy = {
|
||||||
|
...(profile || {}),
|
||||||
|
nip05,
|
||||||
|
} as Record<string, string | number | undefined | boolean>;
|
||||||
|
delete userCopy["loaded"];
|
||||||
|
delete userCopy["created"];
|
||||||
|
delete userCopy["pubkey"];
|
||||||
|
delete userCopy["npub"];
|
||||||
|
delete userCopy["deleted"];
|
||||||
|
delete userCopy["zapService"];
|
||||||
|
delete userCopy["isNostrAddressValid"];
|
||||||
|
console.debug(userCopy);
|
||||||
|
|
||||||
|
if (publisher) {
|
||||||
|
const ev = await publisher.metadata(userCopy);
|
||||||
|
System.BroadcastEvent(ev);
|
||||||
|
|
||||||
|
const newProfile = mapEventToProfile(ev);
|
||||||
|
if (newProfile) {
|
||||||
|
await UserCache.update(newProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
const newNip = name + "@iris.to";
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
saveProfile(newNip);
|
||||||
|
}, 2000);
|
||||||
|
if (profile) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (profile.nip05 !== newNip) {
|
||||||
|
saveProfile(newNip);
|
||||||
|
setAsPrimary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="negative">
|
||||||
|
You have an active iris.to account:
|
||||||
|
<AccountName name={name} />
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<button type="button" onClick={onClick}>
|
||||||
|
Set as primary Nostr address (nip05)
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
291
packages/app/src/Element/IrisAccount/IrisAccount.tsx
Normal file
291
packages/app/src/Element/IrisAccount/IrisAccount.tsx
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Component, FormEvent } from "react";
|
||||||
|
import { LoginStore } from "Login";
|
||||||
|
|
||||||
|
import AccountName from "./AccountName";
|
||||||
|
import ActiveAccount from "./ActiveAccount";
|
||||||
|
import ReservedAccount from "./ReservedAccount";
|
||||||
|
import { ProfileLoader } from "../../index";
|
||||||
|
//import {ProfileLoader} from "../../index";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
cf_turnstile_callback: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO split into smaller components
|
||||||
|
export default class IrisAccount extends Component {
|
||||||
|
state = {
|
||||||
|
irisToActive: false,
|
||||||
|
existing: null as any,
|
||||||
|
profile: null as any,
|
||||||
|
newUserName: "",
|
||||||
|
newUserNameValid: false,
|
||||||
|
error: null as any,
|
||||||
|
showChallenge: false,
|
||||||
|
invalidUsernameMessage: null as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let view: any;
|
||||||
|
|
||||||
|
if (this.state.irisToActive) {
|
||||||
|
const username = this.state.profile?.nip05.split("@")[0];
|
||||||
|
view = <AccountName name={username} />;
|
||||||
|
} else if (this.state.existing && this.state.existing.confirmed) {
|
||||||
|
view = (
|
||||||
|
<ActiveAccount name={this.state.existing.name} setAsPrimary={() => this.setState({ irisToActive: true })} />
|
||||||
|
);
|
||||||
|
} else if (this.state.existing) {
|
||||||
|
view = (
|
||||||
|
<ReservedAccount
|
||||||
|
name={this.state.existing.name}
|
||||||
|
enableReserved={() => this.enableReserved()}
|
||||||
|
declineReserved={() => this.declineReserved()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (this.state.error) {
|
||||||
|
view = <div className="text-iris-red">Error: {this.state.error}</div>;
|
||||||
|
} else if (this.state.showChallenge) {
|
||||||
|
window.cf_turnstile_callback = (token: any) => this.register(token);
|
||||||
|
view = (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="cf-turnstile"
|
||||||
|
data-sitekey={
|
||||||
|
["iris.to", "beta.iris.to", "snort.social"].includes(window.location.hostname)
|
||||||
|
? "0x4AAAAAAACsEd8XuwpPTFwz"
|
||||||
|
: "3x00000000000000000000FF"
|
||||||
|
}
|
||||||
|
data-callback="cf_turnstile_callback"></div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
view = (
|
||||||
|
<div>
|
||||||
|
<p>Register an Iris username (iris.to/username)</p>
|
||||||
|
<form onSubmit={e => this.showChallenge(e)}>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
value={this.state.newUserName}
|
||||||
|
onInput={e => this.onNewUserNameChange(e)}
|
||||||
|
/>
|
||||||
|
<button type="submit">Register</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{this.state.newUserNameValid ? (
|
||||||
|
<>
|
||||||
|
<span className="text-iris-green">Username is available</span>
|
||||||
|
<AccountName name={this.state.newUserName} link={false} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-iris-red">{this.state.invalidUsernameMessage}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>Iris.to account</h3>
|
||||||
|
{view}
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/irislib/faq#iris-username">FAQ</a>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onNewUserNameChange(e: any) {
|
||||||
|
const newUserName = e.target.value;
|
||||||
|
if (newUserName.length === 0) {
|
||||||
|
this.setState({
|
||||||
|
newUserName,
|
||||||
|
newUserNameValid: false,
|
||||||
|
invalidUsernameMessage: "",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newUserName.length < 8 || newUserName.length > 15) {
|
||||||
|
this.setState({
|
||||||
|
newUserName,
|
||||||
|
newUserNameValid: false,
|
||||||
|
invalidUsernameMessage: "Username must be between 8 and 15 characters",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!newUserName.match(/^[a-z0-9_.]+$/)) {
|
||||||
|
this.setState({
|
||||||
|
newUserName,
|
||||||
|
newUserNameValid: false,
|
||||||
|
invalidUsernameMessage: "Username must only contain lowercase letters and numbers",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
newUserName,
|
||||||
|
invalidUsernameMessage: "",
|
||||||
|
});
|
||||||
|
this.checkAvailabilityFromAPI(newUserName);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAvailabilityFromAPI = async (name: string) => {
|
||||||
|
const res = await fetch(`https://api.iris.to/user/available?name=${encodeURIComponent(name)}`);
|
||||||
|
if (name !== this.state.newUserName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.status < 500) {
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.available) {
|
||||||
|
this.setState({ newUserNameValid: true });
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
newUserNameValid: false,
|
||||||
|
invalidUsernameMessage: json.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
newUserNameValid: false,
|
||||||
|
invalidUsernameMessage: "Error checking username availability",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
showChallenge(e: FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.state.newUserNameValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ showChallenge: true }, () => {
|
||||||
|
// Dynamically injecting Cloudflare script
|
||||||
|
if (!document.querySelector('script[src="https://challenges.cloudflare.com/turnstile/v0/api.js"]')) {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
||||||
|
script.async = true;
|
||||||
|
script.defer = true;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(cfToken: any) {
|
||||||
|
console.log("register", cfToken);
|
||||||
|
const login = LoginStore.snapshot();
|
||||||
|
const publisher = LoginStore.getPublisher(login.id);
|
||||||
|
const event = await publisher?.note(`iris.to/${this.state.newUserName}`);
|
||||||
|
// post signed event as request body to https://api.iris.to/user/confirm_user
|
||||||
|
const res = await fetch("https://api.iris.to/user/signup", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ event, cfToken }),
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.setState({
|
||||||
|
error: null,
|
||||||
|
existing: {
|
||||||
|
confirmed: true,
|
||||||
|
name: this.state.newUserName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
delete window.cf_turnstile_callback;
|
||||||
|
} else {
|
||||||
|
res
|
||||||
|
.json()
|
||||||
|
.then(json => {
|
||||||
|
this.setState({ error: json.message || "error" });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.setState({ error: "error" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async enableReserved() {
|
||||||
|
const login = LoginStore.snapshot();
|
||||||
|
const publisher = LoginStore.getPublisher(login.id);
|
||||||
|
const event = await publisher?.note(`iris.to/${this.state.newUserName}`);
|
||||||
|
// post signed event as request body to https://api.iris.to/user/confirm_user
|
||||||
|
const res = await fetch("https://api.iris.to/user/confirm_user", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(event),
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.setState({
|
||||||
|
error: null,
|
||||||
|
existing: { confirmed: true, name: this.state.existing.name },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res
|
||||||
|
.json()
|
||||||
|
.then(json => {
|
||||||
|
this.setState({ error: json.message || "error" });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.setState({ error: "error" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async declineReserved() {
|
||||||
|
if (!confirm(`Are you sure you want to decline iris.to/${name}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const login = LoginStore.snapshot();
|
||||||
|
const publisher = LoginStore.getPublisher(login.id);
|
||||||
|
const event = await publisher?.note(`decline iris.to/${this.state.newUserName}`);
|
||||||
|
// post signed event as request body to https://api.iris.to/user/confirm_user
|
||||||
|
const res = await fetch("https://api.iris.to/user/decline_user", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(event),
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.setState({ confirmSuccess: false, error: null, existing: null });
|
||||||
|
} else {
|
||||||
|
res
|
||||||
|
.json()
|
||||||
|
.then(json => {
|
||||||
|
this.setState({ error: json.message || "error" });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.setState({ error: "error" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const session = LoginStore.snapshot();
|
||||||
|
const myPub = session.publicKey;
|
||||||
|
ProfileLoader.Cache.hook(() => {
|
||||||
|
const profile = ProfileLoader.Cache.getFromCache(myPub);
|
||||||
|
const irisToActive = profile && profile.nip05 && profile.nip05.endsWith("@iris.to");
|
||||||
|
this.setState({ profile, irisToActive });
|
||||||
|
if (profile && !irisToActive) {
|
||||||
|
this.checkExistingAccount(myPub);
|
||||||
|
}
|
||||||
|
}, myPub);
|
||||||
|
this.checkExistingAccount(myPub);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkExistingAccount(pub: any) {
|
||||||
|
const res = await fetch(`https://api.iris.to/user/find?public_key=${pub}`);
|
||||||
|
if (res.status === 200) {
|
||||||
|
const json = await res.json();
|
||||||
|
this.setState({ existing: json });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
packages/app/src/Element/IrisAccount/ReservedAccount.tsx
Normal file
22
packages/app/src/Element/IrisAccount/ReservedAccount.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import AccountName from "./AccountName";
|
||||||
|
|
||||||
|
export default function ReservedAccount({ name = "", enableReserved = () => {}, declineReserved = () => {} }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="text-iris-green">
|
||||||
|
Username iris.to/<b>{name}</b> is reserved for you!
|
||||||
|
</p>
|
||||||
|
<AccountName name={name} link={false} />
|
||||||
|
<p>
|
||||||
|
<button className="btn btn-sm btn-primary" onClick={() => enableReserved()}>
|
||||||
|
Yes please
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button className="btn btn-sm btn-neutral" onClick={() => declineReserved()}>
|
||||||
|
No thanks
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -9,6 +9,7 @@ import CashuNuts from "Element/Embed/CashuNuts";
|
|||||||
import RevealMedia from "./Event/RevealMedia";
|
import RevealMedia from "./Event/RevealMedia";
|
||||||
import { ProxyImg } from "./ProxyImg";
|
import { ProxyImg } from "./ProxyImg";
|
||||||
import { SpotlightMediaModal } from "./Deck/SpotlightMedia";
|
import { SpotlightMediaModal } from "./Deck/SpotlightMedia";
|
||||||
|
import HighlightedText from "./HighlightedText";
|
||||||
import { useTextTransformer } from "Hooks/useTextTransformCache";
|
import { useTextTransformer } from "Hooks/useTextTransformCache";
|
||||||
|
|
||||||
export interface TextProps {
|
export interface TextProps {
|
||||||
@ -22,6 +23,7 @@ export interface TextProps {
|
|||||||
depth?: number;
|
depth?: number;
|
||||||
truncate?: number;
|
truncate?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
highlighText?: string;
|
||||||
onClick?: (e: React.MouseEvent) => void;
|
onClick?: (e: React.MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,6 +38,7 @@ export default function Text({
|
|||||||
disableLinkPreview,
|
disableLinkPreview,
|
||||||
truncate,
|
truncate,
|
||||||
className,
|
className,
|
||||||
|
highlighText,
|
||||||
onClick,
|
onClick,
|
||||||
}: TextProps) {
|
}: TextProps) {
|
||||||
const [showSpotlight, setShowSpotlight] = useState(false);
|
const [showSpotlight, setShowSpotlight] = useState(false);
|
||||||
@ -45,6 +48,35 @@ export default function Text({
|
|||||||
|
|
||||||
const images = elements.filter(a => a.type === "media" && a.mimeType?.startsWith("image")).map(a => a.content);
|
const images = elements.filter(a => a.type === "media" && a.mimeType?.startsWith("image")).map(a => a.content);
|
||||||
|
|
||||||
|
function renderContentWithHighlightedText(content: string, textToHighlight: string) {
|
||||||
|
const textToHighlightArray = textToHighlight.trim().toLowerCase().split(" ");
|
||||||
|
const re = new RegExp(`(${textToHighlightArray.join("|")})`, "gi");
|
||||||
|
const splittedContent = content.split(re);
|
||||||
|
|
||||||
|
const fragments = splittedContent.map(c => {
|
||||||
|
if (textToHighlightArray.includes(c.toLowerCase())) {
|
||||||
|
return {
|
||||||
|
type: "highlighted_text",
|
||||||
|
content: c,
|
||||||
|
} as ParsedFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{fragments.map(f => {
|
||||||
|
if (typeof f === "string") {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <HighlightedText content={f.content} />;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
let lenCtr = 0;
|
let lenCtr = 0;
|
||||||
function renderChunk(a: ParsedFragment) {
|
function renderChunk(a: ParsedFragment) {
|
||||||
@ -96,7 +128,11 @@ export default function Text({
|
|||||||
case "custom_emoji":
|
case "custom_emoji":
|
||||||
return <ProxyImg src={a.content} size={15} className="custom-emoji" />;
|
return <ProxyImg src={a.content} size={15} className="custom-emoji" />;
|
||||||
default:
|
default:
|
||||||
return <div className="text-frag">{a.content}</div>;
|
return (
|
||||||
|
<div className="text-frag">
|
||||||
|
{highlighText ? renderContentWithHighlightedText(a.content, highlighText) : a.content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import Copy from "Element/Copy";
|
|||||||
|
|
||||||
const Developers = [
|
const Developers = [
|
||||||
bech32ToHex(KieranPubKey), // kieran
|
bech32ToHex(KieranPubKey), // kieran
|
||||||
|
bech32ToHex("npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk"), // Martti
|
||||||
bech32ToHex("npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg"), // verbiricha
|
bech32ToHex("npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg"), // verbiricha
|
||||||
bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"), // Karnage
|
bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"), // Karnage
|
||||||
];
|
];
|
||||||
@ -26,6 +27,7 @@ const Contributors = [
|
|||||||
bech32ToHex("npub179rec9sw2a5ngkr2wsjpjhwp2ksygjxn6uw5py9daj2ezhw3aw5swv3s6q"), // h3y6e - JA + other stuff
|
bech32ToHex("npub179rec9sw2a5ngkr2wsjpjhwp2ksygjxn6uw5py9daj2ezhw3aw5swv3s6q"), // h3y6e - JA + other stuff
|
||||||
bech32ToHex("npub17q5n2z8naw0xl6vu9lvt560lg33pdpe29k0k09umlfxm3vc4tqrq466f2y"), // w3irdrobot
|
bech32ToHex("npub17q5n2z8naw0xl6vu9lvt560lg33pdpe29k0k09umlfxm3vc4tqrq466f2y"), // w3irdrobot
|
||||||
bech32ToHex("npub1ltx67888tz7lqnxlrg06x234vjnq349tcfyp52r0lstclp548mcqnuz40t"), // Vivek
|
bech32ToHex("npub1ltx67888tz7lqnxlrg06x234vjnq349tcfyp52r0lstclp548mcqnuz40t"), // Vivek
|
||||||
|
bech32ToHex("npub1wh30wunfpkezx5s7edqu9g0s0raeetf5dgthzm0zw7sk8wqygmjqqfljgh"), // Fernando Porazzi
|
||||||
];
|
];
|
||||||
|
|
||||||
const Translators = [
|
const Translators = [
|
||||||
|
38
packages/app/src/Pages/FreeNostrAddressPage.tsx
Normal file
38
packages/app/src/Pages/FreeNostrAddressPage.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import FormattedMessage from "@snort/app/src/Element/FormattedMessage";
|
||||||
|
|
||||||
|
/*
|
||||||
|
import { IrisHost } from "Const";
|
||||||
|
import Nip5Service from "Element/Nip5Service";
|
||||||
|
*/
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
import IrisAccount from "../Element/IrisAccount/IrisAccount";
|
||||||
|
|
||||||
|
export default function FreeNostrAddressPage() {
|
||||||
|
return (
|
||||||
|
<div className="main-content p">
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage defaultMessage="Get a free nostr address" />
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage {...messages.Nip05} />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage {...messages.Nip05Pros} />
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<FormattedMessage {...messages.AvoidImpersonators} />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FormattedMessage {...messages.EasierToFind} />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FormattedMessage {...messages.Funding} />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<IrisAccount />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -162,7 +162,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
<button className="flex f-center" type="button" onClick={() => navigate("/nostr-address")}>
|
<button className="flex f-center" type="button" onClick={() => navigate("/nostr-address")}>
|
||||||
<FormattedMessage defaultMessage="Buy nostr address" />
|
<FormattedMessage defaultMessage="Buy nostr address" />
|
||||||
</button>
|
</button>
|
||||||
<button className="flex f-center secondary" type="button" onClick={() => navigate("/nostr-address")}>
|
<button className="flex f-center secondary" type="button" onClick={() => navigate("/free-nostr-address")}>
|
||||||
<FormattedMessage defaultMessage="Get a free one" />
|
<FormattedMessage defaultMessage="Get a free one" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,6 +45,7 @@ import { db } from "Db";
|
|||||||
import { preload, RelayMetrics, UserCache, UserRelays } from "Cache";
|
import { preload, RelayMetrics, UserCache, UserRelays } from "Cache";
|
||||||
import { LoginStore } from "Login";
|
import { LoginStore } from "Login";
|
||||||
import { SnortDeckLayout } from "Pages/DeckLayout";
|
import { SnortDeckLayout } from "Pages/DeckLayout";
|
||||||
|
import FreeNostrAddressPage from "./Pages/FreeNostrAddressPage";
|
||||||
|
|
||||||
const WasmQueryOptimizer = {
|
const WasmQueryOptimizer = {
|
||||||
expandFilter: (f: ReqFilter) => {
|
expandFilter: (f: ReqFilter) => {
|
||||||
@ -163,6 +164,10 @@ export const router = createBrowserRouter([
|
|||||||
element: <SettingsPage />,
|
element: <SettingsPage />,
|
||||||
children: SettingsRoutes,
|
children: SettingsRoutes,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/free-nostr-address",
|
||||||
|
element: <FreeNostrAddressPage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/nostr-address",
|
path: "/nostr-address",
|
||||||
element: <NostrAddressPage />,
|
element: <NostrAddressPage />,
|
||||||
|
@ -662,6 +662,9 @@
|
|||||||
"OLEm6z": {
|
"OLEm6z": {
|
||||||
"defaultMessage": "Unknown login error"
|
"defaultMessage": "Unknown login error"
|
||||||
},
|
},
|
||||||
|
"OQSOJF": {
|
||||||
|
"defaultMessage": "Get a free nostr address"
|
||||||
|
},
|
||||||
"OQXnew": {
|
"OQXnew": {
|
||||||
"defaultMessage": "You subscription is still active, you can't renew yet"
|
"defaultMessage": "You subscription is still active, you can't renew yet"
|
||||||
},
|
},
|
||||||
|
@ -217,6 +217,7 @@
|
|||||||
"OEW7yJ": "Zaps",
|
"OEW7yJ": "Zaps",
|
||||||
"OKhRC6": "Share",
|
"OKhRC6": "Share",
|
||||||
"OLEm6z": "Unknown login error",
|
"OLEm6z": "Unknown login error",
|
||||||
|
"OQSOJF": "Get a free nostr address",
|
||||||
"OQXnew": "You subscription is still active, you can't renew yet",
|
"OQXnew": "You subscription is still active, you can't renew yet",
|
||||||
"ORGv1Q": "Created",
|
"ORGv1Q": "Created",
|
||||||
"P61BTu": "Copy Event JSON",
|
"P61BTu": "Copy Event JSON",
|
||||||
|
@ -5,10 +5,21 @@ export const useCopy = (timeout = 2000) => {
|
|||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const copy = async (text: string) => {
|
const copy = async (text: string) => {
|
||||||
|
setError(false);
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
} else {
|
||||||
|
const textArea = document.createElement("textarea");
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = "absolute";
|
||||||
|
textArea.style.opacity = "0";
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
await document.execCommand("copy");
|
||||||
|
textArea.remove();
|
||||||
|
}
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setError(false);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(true);
|
setError(true);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { validateNostrLink } from "./nostr-link";
|
|||||||
import { splitByUrl } from "./utils";
|
import { splitByUrl } from "./utils";
|
||||||
|
|
||||||
export interface ParsedFragment {
|
export interface ParsedFragment {
|
||||||
type: "text" | "link" | "mention" | "invoice" | "media" | "cashu" | "hashtag" | "custom_emoji";
|
type: "text" | "link" | "mention" | "invoice" | "media" | "cashu" | "hashtag" | "custom_emoji" | "highlighted_text";
|
||||||
content: string;
|
content: string;
|
||||||
mimeType?: string;
|
mimeType?: string;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user