Profile / Thread styles
This commit is contained in:
6
packages/system/src/cache/user-metadata.ts
vendored
6
packages/system/src/cache/user-metadata.ts
vendored
@ -96,7 +96,7 @@ export class UserProfileCache extends FeedCache<MetadataCache> {
|
||||
});
|
||||
}
|
||||
},
|
||||
5
|
||||
5,
|
||||
);
|
||||
|
||||
setTimeout(() => this.#processZapperQueue(), 1_000);
|
||||
@ -116,7 +116,7 @@ export class UserProfileCache extends FeedCache<MetadataCache> {
|
||||
});
|
||||
}
|
||||
},
|
||||
5
|
||||
5,
|
||||
);
|
||||
|
||||
setTimeout(() => this.#processNip5Queue(), 1_000);
|
||||
@ -135,7 +135,7 @@ export class UserProfileCache extends FeedCache<MetadataCache> {
|
||||
console.warn("Failed to process item", i);
|
||||
}
|
||||
batch.pop(); // pop any
|
||||
})()
|
||||
})(),
|
||||
);
|
||||
if (batch.length === batchSize) {
|
||||
await Promise.all(batch);
|
||||
|
@ -153,7 +153,7 @@ export class Connection extends ExternalStore<ConnectionStateSnapshot> {
|
||||
this.#log(
|
||||
`[${this.Address}] Closed (code=${e.code}), trying again in ${(this.ConnectTimeout / 1000)
|
||||
.toFixed(0)
|
||||
.toLocaleString()} sec`
|
||||
.toLocaleString()} sec`,
|
||||
);
|
||||
this.ReconnectTimer = setTimeout(() => {
|
||||
this.Connect();
|
||||
@ -425,7 +425,7 @@ export class Connection extends ExternalStore<ConnectionStateSnapshot> {
|
||||
"%s Inactive connection has %d active requests! %O",
|
||||
this.Address,
|
||||
this.ActiveRequests.size,
|
||||
this.ActiveRequests
|
||||
this.ActiveRequests,
|
||||
);
|
||||
} else {
|
||||
this.Close();
|
||||
|
@ -33,4 +33,4 @@ export const CashuRegex = /(cashuA[A-Za-z0-9_-]{0,10000}={0,3})/i;
|
||||
/**
|
||||
* Regex to match any npub/nevent/naddr/nprofile/note
|
||||
*/
|
||||
export const MentionNostrEntityRegex = /@n(pub|profile|event|ote|addr|)1[acdefghjklmnpqrstuvwxyz023456789]+/g;
|
||||
export const MentionNostrEntityRegex = /@n(pub|profile|event|ote|addr|)1[acdefghjklmnpqrstuvwxyz023456789]+/g;
|
||||
|
@ -43,9 +43,7 @@ export class EventBuilder {
|
||||
*/
|
||||
processContent() {
|
||||
if (this.#content) {
|
||||
this.#content = this.#content.replace(MentionNostrEntityRegex, m =>
|
||||
this.#replaceMention(m)
|
||||
);
|
||||
this.#content = this.#content.replace(MentionNostrEntityRegex, m => this.#replaceMention(m));
|
||||
|
||||
const hashTags = [...this.#content.matchAll(HashtagRegex)];
|
||||
hashTags.map(hashTag => {
|
||||
|
@ -146,7 +146,7 @@ export class EventPublisher {
|
||||
relays: Array<string>,
|
||||
note?: HexKey,
|
||||
msg?: string,
|
||||
fnExtra?: EventBuilderHook
|
||||
fnExtra?: EventBuilderHook,
|
||||
) {
|
||||
const eb = this.#eb(EventKind.ZapRequest);
|
||||
eb.content(msg ?? "");
|
||||
|
@ -175,6 +175,6 @@ function pickTopRelays(cache: RelayCache, authors: Array<string>, n: number) {
|
||||
key: a.key,
|
||||
relays: [],
|
||||
};
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { MessageEncryptor, MessageEncryptorPayload, MessageEncryptorVersion } from "index";
|
||||
|
||||
import { base64 } from "@scure/base";
|
||||
import { secp256k1 } from "@noble/curves/secp256k1";
|
||||
|
||||
export class Nip4WebCryptoEncryptor implements MessageEncryptor {
|
||||
@ -20,7 +18,7 @@ export class Nip4WebCryptoEncryptor implements MessageEncryptor {
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return {
|
||||
ciphertext: new Uint8Array(result),
|
||||
|
@ -94,7 +94,7 @@ export class Nip46Signer implements EventSigner {
|
||||
"#p": [this.#localPubkey],
|
||||
},
|
||||
],
|
||||
() => {}
|
||||
() => {},
|
||||
);
|
||||
|
||||
if (isBunker) {
|
||||
@ -181,7 +181,7 @@ export class Nip46Signer implements EventSigner {
|
||||
result: "ack",
|
||||
error: "",
|
||||
},
|
||||
unwrap(this.#remotePubkey)
|
||||
unwrap(this.#remotePubkey),
|
||||
);
|
||||
id = "connect";
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ export class Nip7Signer implements EventSigner {
|
||||
throw new Error("Cannot use NIP-07 signer, not found!");
|
||||
}
|
||||
return await barrierQueue(Nip7Queue, () =>
|
||||
unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content)
|
||||
unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content),
|
||||
);
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ export class Nip7Signer implements EventSigner {
|
||||
throw new Error("Cannot use NIP-07 signer, not found!");
|
||||
}
|
||||
return await barrierQueue(Nip7Queue, () =>
|
||||
unwrap(window.nostr?.nip04?.decrypt).call(window.nostr?.nip04, otherKey, content)
|
||||
unwrap(window.nostr?.nip04?.decrypt).call(window.nostr?.nip04, otherKey, content),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -128,7 +128,7 @@ export class ProfileLoaderService {
|
||||
pubkey: a,
|
||||
loaded: unixNowMs() - ProfileCacheExpire + 30_000, // expire in 30s
|
||||
created: 69,
|
||||
} as MetadataCache)
|
||||
} as MetadataCache),
|
||||
);
|
||||
await Promise.all(empty);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class QueryTrace {
|
||||
readonly filters: Array<ReqFilter>,
|
||||
readonly connId: string,
|
||||
fnClose: (id: string) => void,
|
||||
fnProgress: () => void
|
||||
fnProgress: () => void,
|
||||
) {
|
||||
this.id = uuid();
|
||||
this.start = unixNowMs();
|
||||
@ -293,7 +293,7 @@ export class Query implements QueryBase {
|
||||
q.filters,
|
||||
c.Id,
|
||||
x => c.CloseReq(x),
|
||||
() => this.#onProgress()
|
||||
() => this.#onProgress(),
|
||||
);
|
||||
this.#tracing.push(qt);
|
||||
c.QueueReq(["REQ", qt.id, ...qt.filters], () => qt.sentToRelay());
|
||||
|
@ -5,195 +5,198 @@ import { validateNostrLink } from "./nostr-link";
|
||||
import { splitByUrl } from "./utils";
|
||||
|
||||
export interface ParsedFragment {
|
||||
type: "text" | "link" | "mention" | "invoice" | "media" | "cashu" | "hashtag" | "custom_emoji"
|
||||
content: string
|
||||
mimeType?: string
|
||||
type: "text" | "link" | "mention" | "invoice" | "media" | "cashu" | "hashtag" | "custom_emoji";
|
||||
content: string;
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
export type Fragment = string | ParsedFragment;
|
||||
|
||||
function extractLinks(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return splitByUrl(f).map(a => {
|
||||
const validateLink = () => {
|
||||
const normalizedStr = a.toLowerCase();
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return splitByUrl(f).map(a => {
|
||||
const validateLink = () => {
|
||||
const normalizedStr = a.toLowerCase();
|
||||
|
||||
if (normalizedStr.startsWith("web+nostr:") || normalizedStr.startsWith("nostr:")) {
|
||||
return validateNostrLink(normalizedStr);
|
||||
}
|
||||
|
||||
return (
|
||||
normalizedStr.startsWith("http:") ||
|
||||
normalizedStr.startsWith("https:") ||
|
||||
normalizedStr.startsWith("magnet:")
|
||||
);
|
||||
};
|
||||
|
||||
if (validateLink()) {
|
||||
const url = new URL(a);
|
||||
const extension = url.pathname.match(FileExtensionRegex);
|
||||
|
||||
if (extension && extension.length > 1) {
|
||||
const mediaType = (() => {
|
||||
switch (extension[1]) {
|
||||
case "gif":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "jfif":
|
||||
case "png":
|
||||
case "bmp":
|
||||
case "webp":
|
||||
return "image";
|
||||
case "wav":
|
||||
case "mp3":
|
||||
case "ogg":
|
||||
return "audio";
|
||||
case "mp4":
|
||||
case "mov":
|
||||
case "mkv":
|
||||
case "avi":
|
||||
case "m4v":
|
||||
case "webm":
|
||||
case "m3u8":
|
||||
return "video";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
})();
|
||||
return {
|
||||
type: "media",
|
||||
content: a,
|
||||
mimeType: `${mediaType}/${extension[1]}`
|
||||
} as ParsedFragment;
|
||||
} else {
|
||||
return {
|
||||
type: "link",
|
||||
content: a
|
||||
} as ParsedFragment;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
});
|
||||
if (normalizedStr.startsWith("web+nostr:") || normalizedStr.startsWith("nostr:")) {
|
||||
return validateNostrLink(normalizedStr);
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
|
||||
return (
|
||||
normalizedStr.startsWith("http:") ||
|
||||
normalizedStr.startsWith("https:") ||
|
||||
normalizedStr.startsWith("magnet:")
|
||||
);
|
||||
};
|
||||
|
||||
if (validateLink()) {
|
||||
const url = new URL(a);
|
||||
const extension = url.pathname.match(FileExtensionRegex);
|
||||
|
||||
if (extension && extension.length > 1) {
|
||||
const mediaType = (() => {
|
||||
switch (extension[1]) {
|
||||
case "gif":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "jfif":
|
||||
case "png":
|
||||
case "bmp":
|
||||
case "webp":
|
||||
return "image";
|
||||
case "wav":
|
||||
case "mp3":
|
||||
case "ogg":
|
||||
return "audio";
|
||||
case "mp4":
|
||||
case "mov":
|
||||
case "mkv":
|
||||
case "avi":
|
||||
case "m4v":
|
||||
case "webm":
|
||||
case "m3u8":
|
||||
return "video";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
})();
|
||||
return {
|
||||
type: "media",
|
||||
content: a,
|
||||
mimeType: `${mediaType}/${extension[1]}`,
|
||||
} as ParsedFragment;
|
||||
} else {
|
||||
return {
|
||||
type: "link",
|
||||
content: a,
|
||||
} as ParsedFragment;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractMentions(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(MentionNostrEntityRegex).map(i => {
|
||||
if (MentionNostrEntityRegex.test(i)) {
|
||||
return {
|
||||
type: "mention",
|
||||
content: i
|
||||
} as ParsedFragment;
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(MentionNostrEntityRegex).map(i => {
|
||||
if (MentionNostrEntityRegex.test(i)) {
|
||||
return {
|
||||
type: "mention",
|
||||
content: i,
|
||||
} as ParsedFragment;
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractCashuTokens(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string" && f.includes("cashuA")) {
|
||||
return f.split(CashuRegex).map(a => {
|
||||
return {
|
||||
type: "cashu",
|
||||
content: a
|
||||
} as ParsedFragment
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string" && f.includes("cashuA")) {
|
||||
return f.split(CashuRegex).map(a => {
|
||||
return {
|
||||
type: "cashu",
|
||||
content: a,
|
||||
} as ParsedFragment;
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractInvoices(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(InvoiceRegex).map(i => {
|
||||
if (i.toLowerCase().startsWith("lnbc")) {
|
||||
return {
|
||||
type: "invoice",
|
||||
content: i
|
||||
} as ParsedFragment
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(InvoiceRegex).map(i => {
|
||||
if (i.toLowerCase().startsWith("lnbc")) {
|
||||
return {
|
||||
type: "invoice",
|
||||
content: i,
|
||||
} as ParsedFragment;
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractHashtags(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(HashtagRegex).map(i => {
|
||||
if (i.toLowerCase().startsWith("#")) {
|
||||
return {
|
||||
type: "hashtag",
|
||||
content: i.substring(1)
|
||||
} as ParsedFragment;
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(HashtagRegex).map(i => {
|
||||
if (i.toLowerCase().startsWith("#")) {
|
||||
return {
|
||||
type: "hashtag",
|
||||
content: i.substring(1),
|
||||
} as ParsedFragment;
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractCustomEmoji(fragments: Fragment[], tags: Array<Array<string>>) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(/:(\w+):/g).map(i => {
|
||||
const t = tags.find(a => a[0] === "emoji" && a[1] === i);
|
||||
if (t) {
|
||||
return {
|
||||
type: "custom_emoji",
|
||||
content: t[2]
|
||||
} as ParsedFragment
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(/:(\w+):/g).map(i => {
|
||||
const t = tags.find(a => a[0] === "emoji" && a[1] === i);
|
||||
if (t) {
|
||||
return {
|
||||
type: "custom_emoji",
|
||||
content: t[2],
|
||||
} as ParsedFragment;
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
export function transformText(body: string, tags: Array<Array<string>>) {
|
||||
let fragments = extractLinks([body]);
|
||||
fragments = extractMentions(fragments);
|
||||
fragments = extractHashtags(fragments);
|
||||
fragments = extractInvoices(fragments);
|
||||
fragments = extractCashuTokens(fragments);
|
||||
fragments = extractCustomEmoji(fragments, tags);
|
||||
fragments = fragments.map(a => {
|
||||
if (typeof a === "string") {
|
||||
if (a.length > 0) {
|
||||
return { type: "text", content: a } as ParsedFragment;
|
||||
}
|
||||
} else {
|
||||
return a;
|
||||
let fragments = extractLinks([body]);
|
||||
fragments = extractMentions(fragments);
|
||||
fragments = extractHashtags(fragments);
|
||||
fragments = extractInvoices(fragments);
|
||||
fragments = extractCashuTokens(fragments);
|
||||
fragments = extractCustomEmoji(fragments, tags);
|
||||
fragments = fragments
|
||||
.map(a => {
|
||||
if (typeof a === "string") {
|
||||
if (a.length > 0) {
|
||||
return { type: "text", content: a } as ParsedFragment;
|
||||
}
|
||||
}).filter(a => a).map(a => unwrap(a));
|
||||
return fragments as Array<ParsedFragment>;
|
||||
}
|
||||
} else {
|
||||
return a;
|
||||
}
|
||||
})
|
||||
.filter(a => a)
|
||||
.map(a => unwrap(a));
|
||||
return fragments as Array<ParsedFragment>;
|
||||
}
|
||||
|
@ -27,24 +27,26 @@ export function reqFilterEq(a: FlatReqFilter | ReqFilter, b: FlatReqFilter | Req
|
||||
}
|
||||
|
||||
export function flatFilterEq(a: FlatReqFilter, b: FlatReqFilter): boolean {
|
||||
return a.keys === b.keys
|
||||
&& a.since === b.since
|
||||
&& a.until === b.until
|
||||
&& a.limit === b.limit
|
||||
&& a.search === b.search
|
||||
&& a.ids === b.ids
|
||||
&& a.kinds === b.kinds
|
||||
&& a.authors === b.authors
|
||||
&& a["#e"] === b["#e"]
|
||||
&& a["#p"] === b["#p"]
|
||||
&& a["#t"] === b["#t"]
|
||||
&& a["#d"] === b["#d"]
|
||||
&& a["#r"] === b["#r"];
|
||||
return (
|
||||
a.keys === b.keys &&
|
||||
a.since === b.since &&
|
||||
a.until === b.until &&
|
||||
a.limit === b.limit &&
|
||||
a.search === b.search &&
|
||||
a.ids === b.ids &&
|
||||
a.kinds === b.kinds &&
|
||||
a.authors === b.authors &&
|
||||
a["#e"] === b["#e"] &&
|
||||
a["#p"] === b["#p"] &&
|
||||
a["#t"] === b["#t"] &&
|
||||
a["#d"] === b["#d"] &&
|
||||
a["#r"] === b["#r"]
|
||||
);
|
||||
}
|
||||
|
||||
export function splitByUrl(str: string) {
|
||||
const urlRegex =
|
||||
/((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~()_|]))/i;
|
||||
|
||||
return str.split(urlRegex);
|
||||
}
|
||||
const urlRegex =
|
||||
/((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~()_|]))/i;
|
||||
|
||||
return str.split(urlRegex);
|
||||
}
|
||||
|
Reference in New Issue
Block a user