Add support for nip 22 tags
This commit is contained in:
Generated
+9
-9
@@ -5742,7 +5742,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@welshman/app",
|
"name": "@welshman/app",
|
||||||
"version": "0.0.36",
|
"version": "0.0.38",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
@@ -5752,7 +5752,7 @@
|
|||||||
"@welshman/net": "~0.0.45",
|
"@welshman/net": "~0.0.45",
|
||||||
"@welshman/signer": "~0.0.19",
|
"@welshman/signer": "~0.0.19",
|
||||||
"@welshman/store": "~0.0.15",
|
"@welshman/store": "~0.0.15",
|
||||||
"@welshman/util": "~0.0.54",
|
"@welshman/util": "~0.0.58",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"idb": "^8.0.0",
|
"idb": "^8.0.0",
|
||||||
"svelte": "^4.2.18",
|
"svelte": "^4.2.18",
|
||||||
@@ -5780,7 +5780,7 @@
|
|||||||
"@noble/hashes": "^1.6.1",
|
"@noble/hashes": "^1.6.1",
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/net": "~0.0.45",
|
"@welshman/net": "~0.0.45",
|
||||||
"@welshman/util": "~0.0.54",
|
"@welshman/util": "~0.0.58",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -5798,7 +5798,7 @@
|
|||||||
},
|
},
|
||||||
"packages/editor": {
|
"packages/editor": {
|
||||||
"name": "@welshman/editor",
|
"name": "@welshman/editor",
|
||||||
"version": "0.0.4",
|
"version": "0.0.6",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/package": "^2.0.0",
|
"@sveltejs/package": "^2.0.0",
|
||||||
@@ -6394,7 +6394,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/util": "~0.0.54"
|
"@welshman/util": "~0.0.58"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/lib": {
|
"packages/lib": {
|
||||||
@@ -6420,7 +6420,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/util": "~0.0.54",
|
"@welshman/util": "~0.0.58",
|
||||||
"isomorphic-ws": "^5.0.0",
|
"isomorphic-ws": "^5.0.0",
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
},
|
},
|
||||||
@@ -6437,7 +6437,7 @@
|
|||||||
"@noble/hashes": "^1.6.1",
|
"@noble/hashes": "^1.6.1",
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/net": "~0.0.45",
|
"@welshman/net": "~0.0.45",
|
||||||
"@welshman/util": "~0.0.54",
|
"@welshman/util": "~0.0.58",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -6492,13 +6492,13 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/util": "~0.0.54",
|
"@welshman/util": "~0.0.58",
|
||||||
"svelte": "^4.2.18"
|
"svelte": "^4.2.18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@welshman/util",
|
"name": "@welshman/util",
|
||||||
"version": "0.0.55",
|
"version": "0.0.58",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/ws": "^8.5.13",
|
"@types/ws": "^8.5.13",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@welshman/net": "~0.0.45",
|
"@welshman/net": "~0.0.45",
|
||||||
"@welshman/signer": "~0.0.19",
|
"@welshman/signer": "~0.0.19",
|
||||||
"@welshman/store": "~0.0.15",
|
"@welshman/store": "~0.0.15",
|
||||||
"@welshman/util": "~0.0.54",
|
"@welshman/util": "~0.0.58",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"idb": "^8.0.0",
|
"idb": "^8.0.0",
|
||||||
"svelte": "^4.2.18",
|
"svelte": "^4.2.18",
|
||||||
|
|||||||
@@ -25,12 +25,14 @@ import {
|
|||||||
isLocalUrl,
|
isLocalUrl,
|
||||||
isIPAddress,
|
isIPAddress,
|
||||||
isShareableRelayUrl,
|
isShareableRelayUrl,
|
||||||
|
COMMENT,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
RELAYS,
|
RELAYS,
|
||||||
INBOX_RELAYS,
|
INBOX_RELAYS,
|
||||||
FOLLOWS,
|
FOLLOWS,
|
||||||
WRAP,
|
WRAP,
|
||||||
getAncestorTags,
|
getReplyTags,
|
||||||
|
getCommentTags,
|
||||||
getPubkeyTagValues,
|
getPubkeyTagValues,
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
@@ -191,8 +193,13 @@ export class Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EventAncestors = (event: TrustedEvent, type: "mentions" | "replies" | "roots") => {
|
EventAncestors = (event: TrustedEvent, type: "mentions" | "replies" | "roots") => {
|
||||||
|
const ancestorTags =
|
||||||
|
event.kind === COMMENT ? getCommentTags(event.tags) : getReplyTags(event.tags)
|
||||||
|
|
||||||
|
const tags: string[][] = (ancestorTags as any)[type] || []
|
||||||
|
|
||||||
return this.scenario(
|
return this.scenario(
|
||||||
getAncestorTags(event.tags)[type].flatMap(([_, value, relay, pubkey]) => {
|
tags.flatMap(([_, value, relay, pubkey]) => {
|
||||||
const selections = [makeSelection(this.ForUser().getUrls(), 0.5)]
|
const selections = [makeSelection(this.ForUser().getUrls(), 0.5)]
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
|
|||||||
+71
-31
@@ -1,10 +1,10 @@
|
|||||||
import {ctx} from "@welshman/lib"
|
import {ctx, remove, nthEq} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
getAddress,
|
getAddress,
|
||||||
isReplaceable,
|
isReplaceable,
|
||||||
getAncestorTags,
|
getReplyTags,
|
||||||
getPubkeyTagValues,
|
getPubkeyTagValues,
|
||||||
getIdAndAddress,
|
isReplaceableKind,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {displayProfileByPubkey} from "./profiles.js"
|
import {displayProfileByPubkey} from "./profiles.js"
|
||||||
@@ -35,25 +35,14 @@ export const tagEvent = (event: TrustedEvent, mark = "") => {
|
|||||||
return tags
|
return tags
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tagReplyTo = (event: TrustedEvent) => {
|
export const tagEventPubkeys = (event: TrustedEvent) =>
|
||||||
const $pubkey = pubkey.get()
|
remove(pubkey.get()!, [event.pubkey, ...getPubkeyTagValues(event.tags)]).map(tagPubkey)
|
||||||
const tagValues = getIdAndAddress(event)
|
|
||||||
const tags: string[][] = []
|
|
||||||
|
|
||||||
// Mention the event's author
|
export const tagEventForReply = (event: TrustedEvent) => {
|
||||||
if (event.pubkey !== $pubkey) {
|
const tags = tagEventPubkeys(event)
|
||||||
tags.push(tagPubkey(event.pubkey))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inherit p-tag mentions
|
|
||||||
for (const pubkey of getPubkeyTagValues(event.tags)) {
|
|
||||||
if (pubkey !== $pubkey) {
|
|
||||||
tags.push(tagPubkey(pubkey))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Based on NIP 10 legacy tags, order is root, mentions, reply
|
// Based on NIP 10 legacy tags, order is root, mentions, reply
|
||||||
const {roots, replies, mentions} = getAncestorTags(event.tags)
|
const {roots, replies, mentions} = getReplyTags(event.tags)
|
||||||
|
|
||||||
// Root comes first
|
// Root comes first
|
||||||
if (roots.length > 0) {
|
if (roots.length > 0) {
|
||||||
@@ -66,12 +55,9 @@ export const tagReplyTo = (event: TrustedEvent) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't repeat any tag values
|
|
||||||
const isRepeated = (v: string) => tagValues.includes(v) || tags.find(t => t[1] === v)
|
|
||||||
|
|
||||||
// Inherit mentions
|
// Inherit mentions
|
||||||
for (const t of mentions) {
|
for (const t of mentions) {
|
||||||
if (!isRepeated(t[1])) {
|
if (!tags.some(nthEq(1, t[1]))) {
|
||||||
tags.push([...t.slice(0, 3), "mention"])
|
tags.push([...t.slice(0, 3), "mention"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,21 +65,73 @@ export const tagReplyTo = (event: TrustedEvent) => {
|
|||||||
// Inherit replies if they weren't already included
|
// Inherit replies if they weren't already included
|
||||||
if (roots.length > 0) {
|
if (roots.length > 0) {
|
||||||
for (const t of replies) {
|
for (const t of replies) {
|
||||||
if (!isRepeated(t[1])) {
|
if (!tags.some(nthEq(1, t[1]))) {
|
||||||
tags.push([...t.slice(0, 3), "mention"])
|
tags.push([...t.slice(0, 3), "mention"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a/e-tags for the event event
|
// Finally, tag the event itself
|
||||||
for (const t of tagEvent(event, replies.length > 0 ? "reply" : "root")) {
|
const mark = replies.length > 0 ? "reply" : "root"
|
||||||
tags.push(t)
|
const hint = ctx.app.router.Event(event).getUrl()
|
||||||
|
|
||||||
|
// e-tag the event
|
||||||
|
tags.push(["e", event.id, hint, mark, event.pubkey])
|
||||||
|
|
||||||
|
// a-tag the event
|
||||||
|
if (isReplaceable(event)) {
|
||||||
|
tags.push(["a", getAddress(event), hint, mark, event.pubkey])
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags
|
return tags
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tagReactionTo = (event: TrustedEvent) => {
|
export const tagEventForComment = (event: TrustedEvent) => {
|
||||||
|
const pubkeyHint = ctx.app.router.FromPubkey(event.pubkey).getUrl()
|
||||||
|
const eventHint = ctx.app.router.Event(event).getUrl()
|
||||||
|
const address = getAddress(event)
|
||||||
|
const tags = tagEventPubkeys(event)
|
||||||
|
const seenRoots = new Set<string>()
|
||||||
|
|
||||||
|
for (const [raw, ...tag] of event.tags) {
|
||||||
|
const T = raw.toUpperCase()
|
||||||
|
const t = raw.toLowerCase()
|
||||||
|
|
||||||
|
if (!["k", "e", "a", "i", "p"].includes(t)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seenRoots.has(T)) {
|
||||||
|
tags.push([t, ...tag])
|
||||||
|
} else {
|
||||||
|
tags.push([T, ...tag])
|
||||||
|
seenRoots.add(T)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seenRoots.size === 0) {
|
||||||
|
tags.push(["K", String(event.kind)])
|
||||||
|
tags.push(["P", event.pubkey, pubkeyHint])
|
||||||
|
tags.push(["E", event.id, eventHint, event.pubkey])
|
||||||
|
|
||||||
|
if (isReplaceableKind(event.kind)) {
|
||||||
|
tags.push(["A", address, eventHint, event.pubkey])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.push(["k", String(event.kind)])
|
||||||
|
tags.push(["p", event.pubkey, pubkeyHint])
|
||||||
|
tags.push(["e", event.id, eventHint, event.pubkey])
|
||||||
|
|
||||||
|
if (isReplaceableKind(event.kind)) {
|
||||||
|
tags.push(["a", address, eventHint, event.pubkey])
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tagEventForReaction = (event: TrustedEvent) => {
|
||||||
|
const hint = ctx.app.router.Event(event).getUrl()
|
||||||
const tags: string[][] = []
|
const tags: string[][] = []
|
||||||
|
|
||||||
// Mention the event's author
|
// Mention the event's author
|
||||||
@@ -101,9 +139,11 @@ export const tagReactionTo = (event: TrustedEvent) => {
|
|||||||
tags.push(tagPubkey(event.pubkey))
|
tags.push(tagPubkey(event.pubkey))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a/e-tags for the event
|
tags.push(["k", String(event.kind)])
|
||||||
for (const t of tagEvent(event, "root")) {
|
tags.push(["e", event.id, hint])
|
||||||
tags.push(t)
|
|
||||||
|
if (isReplaceable(event)) {
|
||||||
|
tags.push(["a", getAddress(event), hint])
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags
|
return tags
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@noble/hashes": "^1.6.1",
|
"@noble/hashes": "^1.6.1",
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/net": "~0.0.45",
|
"@welshman/net": "~0.0.45",
|
||||||
"@welshman/util": "~0.0.54",
|
"@welshman/util": "~0.0.58",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/util": "~0.0.54"
|
"@welshman/util": "~0.0.58"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/util": "~0.0.54",
|
"@welshman/util": "~0.0.58",
|
||||||
"isomorphic-ws": "^5.0.0",
|
"isomorphic-ws": "^5.0.0",
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@noble/hashes": "^1.6.1",
|
"@noble/hashes": "^1.6.1",
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/net": "~0.0.45",
|
"@welshman/net": "~0.0.45",
|
||||||
"@welshman/util": "~0.0.54",
|
"@welshman/util": "~0.0.58",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/util": "~0.0.54",
|
"@welshman/util": "~0.0.58",
|
||||||
"svelte": "^4.2.18"
|
"svelte": "^4.2.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import {verifiedSymbol, getEventHash, verifyEvent} from "nostr-tools/pure"
|
import {verifiedSymbol, getEventHash, verifyEvent} from "nostr-tools/pure"
|
||||||
import {cached, pick, now} from "@welshman/lib"
|
import {cached, mapVals, first, pick, now} from "@welshman/lib"
|
||||||
import {getAncestorTagValues} from "./Tags.js"
|
import {getReplyTagValues, getCommentTagValues} from "./Tags.js"
|
||||||
import {getAddress} from "./Address.js"
|
import {getAddress, Address} from "./Address.js"
|
||||||
import {
|
import {
|
||||||
|
COMMENT,
|
||||||
isEphemeralKind,
|
isEphemeralKind,
|
||||||
isReplaceableKind,
|
isReplaceableKind,
|
||||||
isPlainReplaceableKind,
|
isPlainReplaceableKind,
|
||||||
@@ -135,9 +136,41 @@ export const isPlainReplaceable = (e: EventTemplate) => isPlainReplaceableKind(e
|
|||||||
export const isParameterizedReplaceable = (e: EventTemplate) =>
|
export const isParameterizedReplaceable = (e: EventTemplate) =>
|
||||||
isParameterizedReplaceableKind(e.kind)
|
isParameterizedReplaceableKind(e.kind)
|
||||||
|
|
||||||
export const isChildOf = (child: EventContent, parent: HashedEvent) => {
|
export const getAncestors = ({kind, tags}: EventTemplate) =>
|
||||||
const {roots, replies} = getAncestorTagValues(child.tags)
|
kind === COMMENT ? getCommentTagValues(tags) : getReplyTagValues(tags)
|
||||||
const parentIds = replies.length > 0 ? replies : roots
|
|
||||||
|
|
||||||
return getIdAndAddress(parent).some(x => parentIds.includes(x))
|
export const getParentIdsAndAddrs = (event: EventTemplate) => {
|
||||||
|
const {roots, replies} = getAncestors(event)
|
||||||
|
|
||||||
|
return replies.length > 0 ? replies : roots
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getParentIdOrAddr = (event: EventTemplate) => first(getParentIdsAndAddrs(event))
|
||||||
|
|
||||||
|
export const getParentIds = (event: EventTemplate) => {
|
||||||
|
const {roots, replies} = mapVals(
|
||||||
|
ids => ids.filter(id => !Address.isAddress(id)),
|
||||||
|
getAncestors(event),
|
||||||
|
)
|
||||||
|
|
||||||
|
return replies.length > 0 ? replies : roots
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getParentId = (event: EventTemplate) => first(getParentIds(event))
|
||||||
|
|
||||||
|
export const getParentAddrs = (event: EventTemplate) => {
|
||||||
|
const {roots, replies} = mapVals(
|
||||||
|
ids => ids.filter(id => Address.isAddress(id)),
|
||||||
|
getAncestors(event),
|
||||||
|
)
|
||||||
|
|
||||||
|
return replies.length > 0 ? replies : roots
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getParentAddr = (event: EventTemplate) => first(getParentAddrs(event))
|
||||||
|
|
||||||
|
export const isChildOf = (child: EventTemplate, parent: HashedEvent) => {
|
||||||
|
const idsAndAddrs = getParentIdsAndAddrs(child)
|
||||||
|
|
||||||
|
return getIdAndAddress(parent).some(x => idsAndAddrs.includes(x))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,17 @@ export const getKindTags = (tags: string[][]) =>
|
|||||||
|
|
||||||
export const getKindTagValues = (tags: string[][]) => getKindTags(tags).map(t => parseInt(t[1]))
|
export const getKindTagValues = (tags: string[][]) => getKindTags(tags).map(t => parseInt(t[1]))
|
||||||
|
|
||||||
export const getAncestorTags = (tags: string[][]) => {
|
export const getCommentTags = (tags: string[][]) => {
|
||||||
|
const roots = tags.filter(t => ["A", "E", "P", "K"].includes(t[0]))
|
||||||
|
const replies = tags.filter(t => ["a", "e", "p", "k"].includes(t[0]))
|
||||||
|
|
||||||
|
return {roots, replies}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCommentTagValues = (tags: string[][]) =>
|
||||||
|
mapVals(tags => tags.map(nth(1)), getCommentTags(tags))
|
||||||
|
|
||||||
|
export const getReplyTags = (tags: string[][]) => {
|
||||||
const validTags = tags.filter(t => ["a", "e", "q"].includes(t[0]))
|
const validTags = tags.filter(t => ["a", "e", "q"].includes(t[0]))
|
||||||
const mentionTags = validTags.filter(nthEq(0, "q"))
|
const mentionTags = validTags.filter(nthEq(0, "q"))
|
||||||
const roots: string[][] = []
|
const roots: string[][] = []
|
||||||
@@ -90,8 +100,8 @@ export const getAncestorTags = (tags: string[][]) => {
|
|||||||
return {roots, replies, mentions}
|
return {roots, replies, mentions}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAncestorTagValues = (tags: string[][]) =>
|
export const getReplyTagValues = (tags: string[][]) =>
|
||||||
mapVals(tags => tags.map(nth(1)), getAncestorTags(tags))
|
mapVals(tags => tags.map(nth(1)), getReplyTags(tags))
|
||||||
|
|
||||||
export const uniqTags = (tags: string[][]) => uniqBy(t => t.join(":"), tags)
|
export const uniqTags = (tags: string[][]) => uniqBy(t => t.join(":"), tags)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user