From 8e414a10c55f4f3a027fc7000e2a49d6d6a3e77b Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 25 Sep 2023 11:59:02 +0100 Subject: [PATCH] feat: nip-28 --- .vscode/extensions.json | 3 +- .yarn/sdks/eslint/package.json | 2 +- .yarn/sdks/prettier/index.js | 20 +++ .yarn/sdks/prettier/package.json | 6 + .yarn/sdks/typescript/package.json | 2 +- package.json | 8 +- packages/app/src/Element/ChatParticipant.tsx | 12 ++ packages/app/src/Element/DmWindow.tsx | 20 +-- packages/app/src/Element/ProfilePreview.tsx | 4 +- packages/app/src/Feed/LoginFeed.ts | 17 +- packages/app/src/Login/LoginSession.ts | 5 + packages/app/src/Login/MultiAccountStore.ts | 8 +- packages/app/src/Pages/MessagesPage.tsx | 55 +++++-- packages/app/src/chat/index.ts | 25 ++- packages/app/src/chat/nip28.ts | 164 +++++++++++++++++++ packages/app/src/chat/nip29.ts | 5 +- packages/app/src/chat/nip4.ts | 6 +- packages/system/src/event-kind.ts | 5 + yarn.lock | 10 +- 19 files changed, 318 insertions(+), 59 deletions(-) create mode 100755 .yarn/sdks/prettier/index.js create mode 100644 .yarn/sdks/prettier/package.json create mode 100644 packages/app/src/Element/ChatParticipant.tsx diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 8e8adf9ed..daaa5ee2e 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,7 @@ { "recommendations": [ "arcanis.vscode-zipfs", - "dbaeumer.vscode-eslint" + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" ] } diff --git a/.yarn/sdks/eslint/package.json b/.yarn/sdks/eslint/package.json index 06e74e37b..ff446fd6d 100644 --- a/.yarn/sdks/eslint/package.json +++ b/.yarn/sdks/eslint/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "8.44.0-sdk", + "version": "8.48.0-sdk", "main": "./lib/api.js", "type": "commonjs" } diff --git a/.yarn/sdks/prettier/index.js b/.yarn/sdks/prettier/index.js new file mode 100755 index 000000000..8758e367a --- /dev/null +++ b/.yarn/sdks/prettier/index.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire} = require(`module`); +const {resolve} = require(`path`); + +const relPnpApiPath = "../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absRequire = createRequire(absPnpApiPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require prettier + require(absPnpApiPath).setup(); + } +} + +// Defer to the real prettier your application uses +module.exports = absRequire(`prettier`); diff --git a/.yarn/sdks/prettier/package.json b/.yarn/sdks/prettier/package.json new file mode 100644 index 000000000..c102fa297 --- /dev/null +++ b/.yarn/sdks/prettier/package.json @@ -0,0 +1,6 @@ +{ + "name": "prettier", + "version": "3.0.3-sdk", + "main": "./index.js", + "type": "commonjs" +} diff --git a/.yarn/sdks/typescript/package.json b/.yarn/sdks/typescript/package.json index 7503a11ca..0bfa4eb2b 100644 --- a/.yarn/sdks/typescript/package.json +++ b/.yarn/sdks/typescript/package.json @@ -1,6 +1,6 @@ { "name": "typescript", - "version": "5.1.6-sdk", + "version": "5.2.2-sdk", "main": "./lib/typescript.js", "type": "commonjs" } diff --git a/package.json b/package.json index 71e0208d4..0d26662cd 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,6 @@ "test": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/app test && yarn workspace @snort/system test", "pre:commit": "yarn workspace @snort/app intl-extract && yarn workspace @snort/app intl-compile && yarn prettier --write ." }, - "devDependencies": { - "@cloudflare/workers-types": "^4.20230307.0", - "@tauri-apps/cli": "^1.2.3", - "prettier": "^3.0.0" - }, "prettier": { "printWidth": 120, "bracketSameLine": true, @@ -21,7 +16,10 @@ }, "packageManager": "yarn@3.6.3", "dependencies": { + "@cloudflare/workers-types": "^4.20230307.0", + "@tauri-apps/cli": "^1.2.3", "eslint": "^8.48.0", + "prettier": "^3.0.3", "typescript": "^5.2.2" } } diff --git a/packages/app/src/Element/ChatParticipant.tsx b/packages/app/src/Element/ChatParticipant.tsx new file mode 100644 index 000000000..2ccaca098 --- /dev/null +++ b/packages/app/src/Element/ChatParticipant.tsx @@ -0,0 +1,12 @@ +import { ChatParticipant } from "chat"; +import NoteToSelf from "./NoteToSelf"; +import ProfileImage from "./ProfileImage"; +import useLogin from "Hooks/useLogin"; + +export function ChatParticipantProfile({ participant }: { participant: ChatParticipant }) { + const { publicKey } = useLogin(s => ({ publicKey: s.publicKey })); + if (participant.id === publicKey) { + return ; + } + return ; +} diff --git a/packages/app/src/Element/DmWindow.tsx b/packages/app/src/Element/DmWindow.tsx index ef4715d2e..2620500bf 100644 --- a/packages/app/src/Element/DmWindow.tsx +++ b/packages/app/src/Element/DmWindow.tsx @@ -3,33 +3,19 @@ import { useMemo } from "react"; import ProfileImage from "Element/ProfileImage"; import DM from "Element/DM"; -import NoteToSelf from "Element/NoteToSelf"; import useLogin from "Hooks/useLogin"; import WriteMessage from "Element/WriteMessage"; -import { Chat, ChatParticipant, createEmptyChatObject, useChatSystem } from "chat"; +import { Chat, createEmptyChatObject, useChatSystem } from "chat"; import { FormattedMessage } from "react-intl"; +import { ChatParticipantProfile } from "./ChatParticipant"; export default function DmWindow({ id }: { id: string }) { - const { publicKey } = useLogin(s => ({ publicKey: s.publicKey })); const dms = useChatSystem(); const chat = dms.find(a => a.id === id) ?? createEmptyChatObject(id); - function participant(p: ChatParticipant) { - if (p.id === publicKey) { - return ; - } - if (p.type === "pubkey") { - return ; - } - if (p?.profile) { - return ; - } - return ; - } - function sender() { if (chat.participants.length === 1) { - return participant(chat.participants[0]); + return ; } else { return (
diff --git a/packages/app/src/Element/ProfilePreview.tsx b/packages/app/src/Element/ProfilePreview.tsx index 1e2b31e47..dffd1da61 100644 --- a/packages/app/src/Element/ProfilePreview.tsx +++ b/packages/app/src/Element/ProfilePreview.tsx @@ -1,6 +1,6 @@ import "./ProfilePreview.css"; import { ReactNode } from "react"; -import { HexKey } from "@snort/system"; +import { HexKey, UserMetadata } from "@snort/system"; import { useUserProfile } from "@snort/system-react"; import { useInView } from "react-intersection-observer"; @@ -13,6 +13,7 @@ export interface ProfilePreviewProps { about?: boolean; linkToProfile?: boolean; }; + profile?: UserMetadata; actions?: ReactNode; className?: string; onClick?: (e: React.MouseEvent) => void; @@ -41,6 +42,7 @@ export default function ProfilePreview(props: ProfilePreviewProps) { <> {user?.about}
: undefined} /> diff --git a/packages/app/src/Feed/LoginFeed.ts b/packages/app/src/Feed/LoginFeed.ts index 6de39538a..1698a764b 100644 --- a/packages/app/src/Feed/LoginFeed.ts +++ b/packages/app/src/Feed/LoginFeed.ts @@ -25,7 +25,7 @@ import { SubscriptionEvent } from "Subscription"; import useRelaysFeedFollows from "./RelaysFeedFollows"; import { FollowsFeed, GiftsCache, Notifications, UserRelays } from "Cache"; import { System } from "index"; -import { Nip4Chats } from "chat"; +import { Nip28Chats, Nip4Chats } from "chat"; import { useRefreshFeedCache } from "Hooks/useRefreshFeedcache"; /** @@ -42,7 +42,7 @@ export default function useLoginFeed() { useRefreshFeedCache(GiftsCache, true); const subLogin = useMemo(() => { - if (!pubKey) return null; + if (!login || !pubKey) return null; const b = new RequestBuilder(`login:${pubKey.slice(0, 12)}`); b.withOptions({ @@ -57,10 +57,16 @@ export default function useLoginFeed() { .tag("p", [pubKey]) .limit(1); - b.add(Nip4Chats.subscription(pubKey)); - + const n4Sub = Nip4Chats.subscription(login); + if (n4Sub) { + b.add(n4Sub); + } + const n28Sub = Nip28Chats.subscription(login); + if (n28Sub) { + b.add(n28Sub); + } return b; - }, [pubKey]); + }, [login]); const subLists = useMemo(() => { if (!pubKey) return null; @@ -94,6 +100,7 @@ export default function useLoginFeed() { } Nip4Chats.onEvent(loginFeed.data); + Nip28Chats.onEvent(loginFeed.data); const subs = loginFeed.data.filter( a => a.kind === EventKind.SnortSubscriptions && a.pubkey === bech32ToHex(SnortPubKey), diff --git a/packages/app/src/Login/LoginSession.ts b/packages/app/src/Login/LoginSession.ts index 3550d3752..12df2aa08 100644 --- a/packages/app/src/Login/LoginSession.ts +++ b/packages/app/src/Login/LoginSession.ts @@ -123,4 +123,9 @@ export interface LoginSession { * Snort application data */ appData: Newest; + + /** + * A list of chats which we have joined (NIP-28/NIP-29) + */ + extraChats: Array; } diff --git a/packages/app/src/Login/MultiAccountStore.ts b/packages/app/src/Login/MultiAccountStore.ts index 66015c987..258c61192 100644 --- a/packages/app/src/Login/MultiAccountStore.ts +++ b/packages/app/src/Login/MultiAccountStore.ts @@ -52,6 +52,7 @@ const LoggedOut = { }, timestamp: 0, }, + extraChats: [], } as LoginSession; export class MultiAccountStore extends ExternalStore { @@ -79,10 +80,11 @@ export class MultiAccountStore extends ExternalStore { } v.appData ??= { item: { - mutedWords: [] + mutedWords: [], }, - timestamp: 0 - } + timestamp: 0, + }; + v.extraChats ??= []; } } diff --git a/packages/app/src/Pages/MessagesPage.tsx b/packages/app/src/Pages/MessagesPage.tsx index 963fd0307..2d7026407 100644 --- a/packages/app/src/Pages/MessagesPage.tsx +++ b/packages/app/src/Pages/MessagesPage.tsx @@ -3,7 +3,7 @@ import "./MessagesPage.css"; import React, { useEffect, useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useNavigate, useParams } from "react-router-dom"; -import { TLVEntryType, decodeTLV } from "@snort/system"; +import { NostrLink, NostrPrefix, TLVEntryType, UserMetadata, decodeTLV } from "@snort/system"; import { useUserProfile, useUserSearch } from "@snort/system-react"; import UnreadCount from "Element/UnreadCount"; @@ -21,6 +21,10 @@ import Text from "Element/Text"; import { Chat, ChatType, createChatLink, useChatSystem } from "chat"; import Modal from "Element/Modal"; import ProfilePreview from "Element/ProfilePreview"; +import { useEventFeed } from "Feed/EventFeed"; +import { LoginSession, LoginStore } from "Login"; +import { Nip28ChatSystem } from "chat/nip28"; +import { ChatParticipantProfile } from "Element/ChatParticipant"; const TwoCol = 768; const ThreeCol = 1500; @@ -57,18 +61,12 @@ export default function MessagesPage() { function conversationIdent(cx: Chat) { if (cx.participants.length === 1) { - const p = cx.participants[0]; - - if (p.type === "pubkey") { - return ; - } else { - return ; - } + return ; } else { return (
{cx.participants.map(v => ( - + ))} {cx.title ?? }
@@ -168,17 +166,17 @@ function ProfileDmActions({ id }: { id: string }) { function NewChatWindow() { const [show, setShow] = useState(false); - const [newChat, setNewChat] = useState([]); - const [results, setResults] = useState([]); + const [newChat, setNewChat] = useState>([]); + const [results, setResults] = useState>([]); const [term, setSearchTerm] = useState(""); const navigate = useNavigate(); const search = useUserSearch(); - const { follows } = useLogin(); + const login = useLogin(); useEffect(() => { setNewChat([]); setSearchTerm(""); - setResults(follows.item); + setResults(login.follows.item); }, [show]); useEffect(() => { @@ -186,7 +184,7 @@ function NewChatWindow() { if (term) { search(term).then(setResults); } else { - setResults(follows.item); + setResults(login.follows.item); } }); }, [term]); @@ -259,6 +257,19 @@ function NewChatWindow() { /> ); })} + {results.length === 1 && ( + { + setShow(false); + LoginStore.updateSession({ + ...login, + extraChats: appendDedupe(login.extraChats, [Nip28ChatSystem.chatId(id)]), + } as LoginSession); + navigate(createChatLink(ChatType.PublicGroupChat, id)); + }} + /> + )} @@ -267,3 +278,19 @@ function NewChatWindow() { ); } + +export function Nip28ChatProfile({ id, onClick }: { id: string; onClick: (id: string) => void }) { + const channel = useEventFeed(new NostrLink(NostrPrefix.Event, id, 40)); + if (channel?.data) { + const meta = JSON.parse(channel.data.content) as UserMetadata; + return ( + } + onClick={() => onClick(id)} + /> + ); + } +} diff --git a/packages/app/src/chat/index.ts b/packages/app/src/chat/index.ts index ada8472e1..9f35d109d 100644 --- a/packages/app/src/chat/index.ts +++ b/packages/app/src/chat/index.ts @@ -20,6 +20,8 @@ import { Nip29ChatSystem } from "./nip29"; import useModeration from "Hooks/useModeration"; import useLogin from "Hooks/useLogin"; import { Nip24ChatSystem } from "./nip24"; +import { LoginSession } from "Login"; +import { Nip28ChatSystem } from "./nip28"; export enum ChatType { DirectMessage = 1, @@ -60,7 +62,7 @@ export interface ChatSystem { /** * Create a request for this system to get updates */ - subscription(id: string): RequestBuilder | undefined; + subscription(session: LoginSession): RequestBuilder | undefined; onEvent(evs: readonly TaggedNostrEvent[]): Promise | void; listChats(pk: string): Array; @@ -69,6 +71,7 @@ export interface ChatSystem { export const Nip4Chats = new Nip4ChatSystem(Chats); export const Nip29Chats = new Nip29ChatSystem(Chats); export const Nip24Chats = new Nip24ChatSystem(GiftsCache); +export const Nip28Chats = new Nip28ChatSystem(Chats); /** * Extract the P tag of the event @@ -143,17 +146,23 @@ export function createChatLink(type: ChatType, ...params: Array) { ), )}`; } + case ChatType.PublicGroupChat: { + return `/messages/${Nip28ChatSystem.chatId(params[0])}`; + } } throw new Error("Unknown chat type"); } export function createEmptyChatObject(id: string) { - if (id.startsWith("chat4")) { + if (id.startsWith("chat41")) { return Nip4ChatSystem.createChatObj(id, []); } - if (id.startsWith("chat24")) { + if (id.startsWith("chat241")) { return Nip24ChatSystem.createChatObj(id, []); } + if (id.startsWith("chat281")) { + return Nip28ChatSystem.createChatObj(id, []); + } throw new Error("Cant create new empty chat, unknown id"); } @@ -180,10 +189,18 @@ export function useNip24Chat() { ); } +export function useNip28Chat() { + return useSyncExternalStore( + c => Nip28Chats.hook(c), + () => Nip28Chats.snapshot(), + ); +} + export function useChatSystem() { const nip4 = useNip4Chat(); //const nip24 = useNip24Chat(); + const nip28 = useNip28Chat(); const { muted, blocked } = useModeration(); - return [...nip4].filter(a => !(muted.includes(a.id) || blocked.includes(a.id))); + return [...nip4, ...nip28].filter(a => !(muted.includes(a.id) || blocked.includes(a.id))); } diff --git a/packages/app/src/chat/nip28.ts b/packages/app/src/chat/nip28.ts index e69de29bb..cc0c5d86c 100644 --- a/packages/app/src/chat/nip28.ts +++ b/packages/app/src/chat/nip28.ts @@ -0,0 +1,164 @@ +import debug from "debug"; +import { ExternalStore, FeedCache, unixNow, unwrap } from "@snort/shared"; +import { + EventKind, + NostrEvent, + NostrPrefix, + RequestBuilder, + SystemInterface, + TLVEntryType, + TaggedNostrEvent, + UserMetadata, + decodeTLV, + encodeTLVEntries, +} from "@snort/system"; + +import { LoginSession } from "Login"; +import { findTag } from "SnortUtils"; +import { Chat, ChatParticipant, ChatSystem, ChatType, lastReadInChat } from "chat"; +import { Day } from "Const"; + +export class Nip28ChatSystem extends ExternalStore> implements ChatSystem { + #cache: FeedCache; + #log = debug("NIP-04"); + readonly ChannelKinds = [ + EventKind.PublicChatChannel, + EventKind.PublicChatMessage, + EventKind.PublicChatMetadata, + EventKind.PublicChatMuteMessage, + EventKind.PublicChatMuteUser, + ]; + + constructor(cache: FeedCache) { + super(); + this.#cache = cache; + } + + subscription(session: LoginSession): RequestBuilder | undefined { + const chats = (session.extraChats ?? []).filter(a => a.startsWith("chat281")); + if (chats.length === 0) return; + + const chatId = (v: string) => unwrap(decodeTLV(v).find(a => a.type === TLVEntryType.Special)).value as string; + + const messages = this.#chatChannels(); + const rb = new RequestBuilder(`nip28:${session.id}`); + rb.withFilter() + .ids(chats.map(v => chatId(v))) + .kinds([EventKind.PublicChatChannel, EventKind.PublicChatMetadata]); + for (const c of chats) { + const id = chatId(c); + const lastMessage = messages[id]?.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0) ?? 0; + rb.withFilter() + .tag("e", [id]) + .since(lastMessage === 0 ? unixNow() - 2 * Day : lastMessage) + .kinds(this.ChannelKinds); + } + + return rb; + } + + async onEvent(evs: readonly TaggedNostrEvent[]) { + const dms = evs.filter(a => this.ChannelKinds.includes(a.kind)); + if (dms.length > 0) { + await this.#cache.bulkSet(dms); + this.notifyChange(); + } + } + + listChats(): Chat[] { + const chats = this.#chatChannels(); + return Object.entries(chats).map(([k, v]) => { + return Nip28ChatSystem.createChatObj(Nip28ChatSystem.chatId(k), v); + }); + } + + static chatId(id: string) { + return encodeTLVEntries("chat28" as NostrPrefix, { + type: TLVEntryType.Special, + value: id, + length: id.length, + }); + } + + static createChatObj(id: string, messages: Array) { + const last = lastReadInChat(id); + const participants = decodeTLV(id) + .filter(v => v.type === TLVEntryType.Special) + .map( + v => + ({ + type: "generic", + id: v.value as string, + profile: this.#chatProfileFromMessages(messages), + }) as ChatParticipant, + ); + return { + type: ChatType.PublicGroupChat, + id, + unread: messages.reduce((acc, v) => (v.created_at > last ? acc++ : acc), 0), + lastMessage: messages.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0), + participants, + messages: messages + .filter(a => a.kind === EventKind.PublicChatMessage) + .map(m => ({ + id: m.id, + created_at: m.created_at, + from: m.pubkey, + tags: m.tags, + content: m.content, + needsDecryption: false, + })), + createMessage: async (msg, pub) => { + return [ + await pub.generic(eb => { + return eb.kind(EventKind.PublicChatMessage).content(msg).tag(["e", participants[0].id, "", "root"]); + }), + ]; + }, + sendMessage: (ev, system: SystemInterface) => { + ev.forEach(a => system.BroadcastEvent(a)); + }, + } as Chat; + } + + takeSnapshot(): Chat[] { + return this.listChats(); + } + + static #chatProfileFromMessages(messages: Array) { + const chatDefs = messages.filter( + a => a.kind === EventKind.PublicChatChannel || a.kind === EventKind.PublicChatMetadata, + ); + const chatDef = + chatDefs.length > 0 + ? chatDefs.reduce((acc, v) => (acc.created_at > v.created_at ? acc : v), chatDefs[0]) + : undefined; + return chatDef ? (JSON.parse(chatDef.content) as UserMetadata) : undefined; + } + + #chatChannels() { + const messages = this.#cache.snapshot(); + const chats = messages.reduce( + (acc, v) => { + const k = this.#chatId(v); + if (k) { + acc[k] ??= []; + acc[k].push(v); + } + return acc; + }, + {} as Record>, + ); + return chats; + } + + #chatId(ev: NostrEvent) { + if (ev.kind === EventKind.PublicChatChannel) { + return ev.id; + } else if (ev.kind === EventKind.PublicChatMetadata) { + return findTag(ev, "e"); + } else if (this.ChannelKinds.includes(ev.kind)) { + return ev.tags.find(a => a[0] === "e" && a[3] === "root")?.[1]; + } + } +} diff --git a/packages/app/src/chat/nip29.ts b/packages/app/src/chat/nip29.ts index 261f7e86d..ab7f3ce58 100644 --- a/packages/app/src/chat/nip29.ts +++ b/packages/app/src/chat/nip29.ts @@ -1,5 +1,6 @@ import { ExternalStore, FeedCache, dedupe } from "@snort/shared"; import { RequestBuilder, NostrEvent, EventKind, SystemInterface, TaggedNostrEvent } from "@snort/system"; +import { LoginSession } from "Login"; import { unwrap } from "SnortUtils"; import { Chat, ChatSystem, ChatType, lastReadInChat } from "chat"; @@ -15,7 +16,9 @@ export class Nip29ChatSystem extends ExternalStore> implements ChatS return this.listChats(); } - subscription(id: string) { + subscription(session: LoginSession) { + const id = session.publicKey; + if (!id) return; const gs = id.split("/", 2); const rb = new RequestBuilder(`nip29:${id}`); const last = this.listChats().find(a => a.id === id)?.lastMessage; diff --git a/packages/app/src/chat/nip4.ts b/packages/app/src/chat/nip4.ts index f9a3949b4..222b88422 100644 --- a/packages/app/src/chat/nip4.ts +++ b/packages/app/src/chat/nip4.ts @@ -10,6 +10,7 @@ import { TaggedNostrEvent, decodeTLV, } from "@snort/system"; +import { LoginSession } from "Login"; import { Chat, ChatSystem, ChatType, inChatWith, lastReadInChat } from "chat"; import { debug } from "debug"; @@ -30,7 +31,10 @@ export class Nip4ChatSystem extends ExternalStore> implements ChatSy } } - subscription(pk: string) { + subscription(session: LoginSession) { + const pk = session.publicKey; + if (!pk) return; + const rb = new RequestBuilder(`nip4:${pk.slice(0, 12)}`); const dms = this.#cache.snapshot(); const dmSince = dms.reduce( diff --git a/packages/system/src/event-kind.ts b/packages/system/src/event-kind.ts index 466a48185..e144a7b52 100644 --- a/packages/system/src/event-kind.ts +++ b/packages/system/src/event-kind.ts @@ -12,6 +12,11 @@ enum EventKind { SimpleChatMessage = 9, // NIP-29 SealedRumor = 13, // NIP-59 ChatRumor = 14, // NIP-24 + PublicChatChannel = 40, // NIP-28 + PublicChatMetadata = 41, // NIP-28 + PublicChatMessage = 42, // NIP-28 + PublicChatMuteMessage = 43, // NIP-28 + PublicChatMuteUser = 44, // NIP-28 SnortSubscriptions = 1000, // NIP-XX Polls = 6969, // NIP-69 GiftWrap = 1059, // NIP-59 diff --git a/yarn.lock b/yarn.lock index 2d992d761..5c7a35d6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10949,12 +10949,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.0.0": - version: 3.0.2 - resolution: "prettier@npm:3.0.2" +"prettier@npm:^3.0.3": + version: 3.0.3 + resolution: "prettier@npm:3.0.3" bin: prettier: bin/prettier.cjs - checksum: 118b59ddb6c80abe2315ab6d0f4dd1b253be5cfdb20622fa5b65bb1573dcd362e6dd3dcf2711dd3ebfe64aecf7bdc75de8a69dc2422dcd35bdde7610586b677a + checksum: e10b9af02b281f6c617362ebd2571b1d7fc9fb8a3bd17e371754428cda992e5e8d8b7a046e8f7d3e2da1dcd21aa001e2e3c797402ebb6111b5cd19609dd228e0 languageName: node linkType: hard @@ -11726,7 +11726,7 @@ __metadata: "@cloudflare/workers-types": ^4.20230307.0 "@tauri-apps/cli": ^1.2.3 eslint: ^8.48.0 - prettier: ^3.0.0 + prettier: ^3.0.3 typescript: ^5.2.2 languageName: unknown linkType: soft