diff --git a/apps/desktop2/package.json b/apps/desktop2/package.json index 843ad036..8a74016b 100644 --- a/apps/desktop2/package.json +++ b/apps/desktop2/package.json @@ -24,6 +24,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", + "slate": "^0.101.5", + "slate-react": "^0.101.6", "sonner": "^1.4.0", "virtua": "^0.27.0" }, diff --git a/apps/desktop2/src/routes/app.tsx b/apps/desktop2/src/routes/app.tsx index f74a8654..6f4dcc46 100644 --- a/apps/desktop2/src/routes/app.tsx +++ b/apps/desktop2/src/routes/app.tsx @@ -1,6 +1,7 @@ import { BellFilledIcon, BellIcon, + EditIcon, HomeFilledIcon, HomeIcon, SpaceFilledIcon, @@ -10,12 +11,14 @@ import { Link } from "@tanstack/react-router"; import { Outlet, createFileRoute } from "@tanstack/react-router"; import { cn } from "@lume/utils"; import { Accounts } from "@/components/accounts"; +import { useArk } from "@lume/ark"; export const Route = createFileRoute("/app")({ component: App, }); function App() { + const ark = useArk(); const context = Route.useRouteContext(); return ( @@ -28,7 +31,17 @@ function App() { )} > - +
+ + +
@@ -45,7 +58,7 @@ function Navigation() { data-tauri-drag-region className="flex h-full flex-1 items-center gap-2" > - + {({ isActive }) => (
{ - const lastEvent = lastPage.at(-1); + const lastEvent = lastPage?.at(-1); if (!lastEvent) return; return lastEvent.created_at - 1; }, @@ -49,8 +49,9 @@ function LocalTimeline() { return (
{isLoading ? ( -
+
+

Loading...

) : !data.length ? (
diff --git a/apps/desktop2/src/routes/auth/create/self.lazy.tsx b/apps/desktop2/src/routes/auth/create/self.lazy.tsx index 35f2a4a6..12a5ef51 100644 --- a/apps/desktop2/src/routes/auth/create/self.lazy.tsx +++ b/apps/desktop2/src/routes/auth/create/self.lazy.tsx @@ -26,7 +26,7 @@ function Create() { try { await ark.save_account(keys); navigate({ - to: "/app/home", + to: "/app/home/local", search: { onboarding: true }, replace: true, }); diff --git a/apps/desktop2/src/routes/editor/-components/media.tsx b/apps/desktop2/src/routes/editor/-components/media.tsx new file mode 100644 index 00000000..147f14c3 --- /dev/null +++ b/apps/desktop2/src/routes/editor/-components/media.tsx @@ -0,0 +1,78 @@ +import { useArk } from "@lume/ark"; +import { AddMediaIcon, LoaderIcon } from "@lume/icons"; +import { cn, insertImage, isImagePath } from "@lume/utils"; +import { useEffect, useState } from "react"; +import { useSlateStatic } from "slate-react"; +import { toast } from "sonner"; +import { getCurrent } from "@tauri-apps/api/window"; +import { UnlistenFn } from "@tauri-apps/api/event"; + +export function MediaButton({ className }: { className?: string }) { + const ark = useArk(); + const editor = useSlateStatic(); + + const [loading, setLoading] = useState(false); + + const uploadToNostrBuild = async () => { + try { + setLoading(true); + + const image = await ark.upload(); + + if (image) { + insertImage(editor, image); + } + + setLoading(false); + } catch (e) { + setLoading(false); + toast.error(`Upload failed, error: ${e}`); + } + }; + + useEffect(() => { + let unlisten: UnlistenFn = undefined; + + async function listenFileDrop() { + const window = getCurrent(); + if (!unlisten) { + unlisten = await window.listen("tauri://file-drop", async (event) => { + // @ts-ignore, lfg !!! + const items: string[] = event.payload.paths; + // start loading + setLoading(true); + // upload all images + for (const item of items) { + if (isImagePath(item)) { + const image = await ark.upload(item); + insertImage(editor, image); + } + } + // stop loading + setLoading(false); + }); + } + } + + listenFileDrop(); + + return () => { + if (unlisten) unlisten(); + }; + }, []); + + return ( + + ); +} diff --git a/apps/desktop2/src/routes/editor/index.lazy.tsx b/apps/desktop2/src/routes/editor/index.lazy.tsx new file mode 100644 index 00000000..e3e23eed --- /dev/null +++ b/apps/desktop2/src/routes/editor/index.lazy.tsx @@ -0,0 +1,379 @@ +import { useArk } from "@lume/ark"; +import { LoaderIcon, TrashIcon } from "@lume/icons"; +import { + Portal, + cn, + insertImage, + insertMention, + insertNostrEvent, + isImagePath, + isImageUrl, + sendNativeNotification, +} from "@lume/utils"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { MediaButton } from "./-components/media"; +import { MentionNote } from "@lume/ui/src/note/mentions/note"; +import { + Descendant, + Editor, + Node, + Range, + Transforms, + createEditor, +} from "slate"; +import { + ReactEditor, + useSlateStatic, + useSelected, + useFocused, + withReact, + Slate, + Editable, +} from "slate-react"; +import { Contact } from "@lume/types"; +import { User } from "@lume/ui"; + +export const Route = createLazyFileRoute("/editor/")({ + component: Screen, +}); + +function Screen() { + const ark = useArk(); + const ref = useRef(); + + const [t] = useTranslation(); + const [editorValue, setEditorValue] = useState([ + { + type: "paragraph", + children: [{ text: "" }], + }, + ]); + const [contacts, setContacts] = useState([]); + const [target, setTarget] = useState(); + const [index, setIndex] = useState(0); + const [search, setSearch] = useState(""); + const [loading, setLoading] = useState(false); + const [editor] = useState(() => + withMentions(withNostrEvent(withImages(withReact(createEditor())))), + ); + + const filters = contacts + ?.filter((c) => + c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()), + ) + ?.slice(0, 10); + + const reset = () => { + // @ts-expect-error, backlog + editor.children = [{ type: "paragraph", children: [{ text: "" }] }]; + setEditorValue([{ type: "paragraph", children: [{ text: "" }] }]); + }; + + const serialize = (nodes: Descendant[]) => { + return nodes + .map((n) => { + // @ts-expect-error, backlog + if (n.type === "image") return n.url; + // @ts-expect-error, backlog + if (n.type === "event") return n.eventId; + + // @ts-expect-error, backlog + if (n.children.length) { + // @ts-expect-error, backlog + return n.children + .map((n) => { + if (n.type === "mention") return n.npub; + return Node.string(n).trim(); + }) + .join(" "); + } + + return Node.string(n); + }) + .join("\n"); + }; + + const publish = async () => { + try { + // start loading + setLoading(true); + + const content = serialize(editor.children); + const eventId = await ark.publish(content); + + if (eventId) { + await sendNativeNotification("You've publish new post successfully."); + return reset(); + } + + // stop loading + setLoading(false); + } catch (e) { + setLoading(false); + await sendNativeNotification(String(e)); + } + }; + + useEffect(() => { + if (target && filters.length > 0) { + const el = ref.current; + const domRange = ReactEditor.toDOMRange(editor, target); + const rect = domRange.getBoundingClientRect(); + el.style.top = `${rect.top + window.scrollY + 24}px`; + el.style.left = `${rect.left + window.scrollX}px`; + } + }, [filters.length, editor, index, search, target]); + + return ( +
+ { + const { selection } = editor; + + if (selection && Range.isCollapsed(selection)) { + const [start] = Range.edges(selection); + const wordBefore = Editor.before(editor, start, { unit: "word" }); + const before = wordBefore && Editor.before(editor, wordBefore); + const beforeRange = before && Editor.range(editor, before, start); + const beforeText = + beforeRange && Editor.string(editor, beforeRange); + const beforeMatch = beforeText?.match(/^@(\w+)$/); + const after = Editor.after(editor, start); + const afterRange = Editor.range(editor, start, after); + const afterText = Editor.string(editor, afterRange); + const afterMatch = afterText.match(/^(\s|$)/); + + if (beforeMatch && afterMatch) { + setTarget(beforeRange); + setSearch(beforeMatch[1]); + setIndex(0); + return; + } + } + + setTarget(null); + }} + > +
+ + +
+
+
+
+ } + placeholder={t("editor.placeholder")} + className="focus:outline-none" + /> + {target && filters.length > 0 && ( + +
+ {filters.map((contact) => ( + + ))} +
+
+ )} +
+
+
+
+
+ ); +} + +const withNostrEvent = (editor: ReactEditor) => { + const { insertData, isVoid } = editor; + + editor.isVoid = (element) => { + // @ts-expect-error, wtf + return element.type === "event" ? true : isVoid(element); + }; + + editor.insertData = (data) => { + const text = data.getData("text/plain"); + + if (text.startsWith("nevent1") || text.startsWith("note1")) { + insertNostrEvent(editor, text); + } else { + insertData(data); + } + }; + + return editor; +}; + +const withMentions = (editor: ReactEditor) => { + const { isInline, isVoid, markableVoid } = editor; + + editor.isInline = (element) => { + // @ts-expect-error, wtf + return element.type === "mention" ? true : isInline(element); + }; + + editor.isVoid = (element) => { + // @ts-expect-error, wtf + return element.type === "mention" ? true : isVoid(element); + }; + + editor.markableVoid = (element) => { + // @ts-expect-error, wtf + return element.type === "mention" || markableVoid(element); + }; + + return editor; +}; + +const withImages = (editor: ReactEditor) => { + const { insertData, isVoid } = editor; + + editor.isVoid = (element) => { + // @ts-expect-error, wtf + return element.type === "image" ? true : isVoid(element); + }; + + editor.insertData = (data) => { + const text = data.getData("text/plain"); + + if (isImageUrl(text)) { + insertImage(editor, text); + } else { + insertData(data); + } + }; + + return editor; +}; + +const Image = ({ attributes, children, element }) => { + const editor = useSlateStatic(); + const path = ReactEditor.findPath(editor as ReactEditor, element); + + const selected = useSelected(); + const focused = useFocused(); + + return ( +
+ {children} +
+ {element.url} + +
+
+ ); +}; + +const Mention = ({ attributes, element }) => { + const editor = useSlateStatic(); + const path = ReactEditor.findPath(editor as ReactEditor, element); + + return ( + Transforms.removeNodes(editor, { at: path })} + className="inline-block align-baseline text-blue-500 hover:text-blue-600" + >{`@${element.name}`} + ); +}; + +const Event = ({ attributes, element, children }) => { + const editor = useSlateStatic(); + const path = ReactEditor.findPath(editor as ReactEditor, element); + + return ( +
+ {children} + {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} +
Transforms.removeNodes(editor, { at: path })} + className="user-select-none relative my-2" + > + +
+
+ ); +}; + +const Element = (props) => { + const { attributes, children, element } = props; + + switch (element.type) { + case "image": + return ; + case "mention": + return ; + case "event": + return ; + default: + return ( +

+ {children} +

+ ); + } +}; diff --git a/apps/desktop2/src/routes/events/-components/replyList.tsx b/apps/desktop2/src/routes/events/-components/replyList.tsx index ded929ab..2ab517e6 100644 --- a/apps/desktop2/src/routes/events/-components/replyList.tsx +++ b/apps/desktop2/src/routes/events/-components/replyList.tsx @@ -4,6 +4,7 @@ import { cn } from "@lume/utils"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { EventWithReplies } from "@lume/types"; +import { Reply } from "./reply"; export function ReplyList({ eventId, diff --git a/packages/ark/package.json b/packages/ark/package.json index 7d6aa06e..80b5f61a 100644 --- a/packages/ark/package.json +++ b/packages/ark/package.json @@ -6,7 +6,6 @@ "dependencies": { "@getalby/sdk": "^3.3.0", "@lume/icons": "workspace:^", - "@lume/storage": "workspace:^", "@lume/utils": "workspace:^", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index 61d0a24e..81f31dc9 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -7,6 +7,9 @@ import type { } from "@lume/types"; import { invoke } from "@tauri-apps/api/core"; import { WebviewWindow } from "@tauri-apps/api/webview"; +import { open } from "@tauri-apps/plugin-dialog"; +import { readFile } from "@tauri-apps/plugin-fs"; +import { generateContentTags } from "@lume/utils"; export class Ark { public account: Account; @@ -147,17 +150,27 @@ export class Ark { } return nostrEvents.sort((a, b) => b.created_at - a.created_at); - } catch (e) { + } catch { return []; } } public async publish(content: string) { try { - const cmd: string = await invoke("publish", { content }); + const g = await generateContentTags(content); + + const eventContent = g.content; + const eventTags = g.tags; + + const cmd: string = await invoke("publish", { + content: eventContent, + tags: eventTags, + }); + return cmd; } catch (e) { console.error(String(e)); + return false; } } @@ -341,6 +354,61 @@ export class Ark { } } + public async upload(filePath?: string) { + try { + const allowExts = [ + "png", + "jpeg", + "jpg", + "gif", + "mp4", + "mp3", + "webm", + "mkv", + "avi", + "mov", + ]; + + let selected = + filePath || + ( + await open({ + multiple: false, + filters: [ + { + name: "Media", + extensions: allowExts, + }, + ], + }) + ).path; + + if (!selected) return null; + + const file = await readFile(selected); + const blob = new Blob([file]); + + const data = new FormData(); + data.append("fileToUpload", blob); + data.append("submit", "Upload Image"); + + const res = await fetch("https://nostr.build/api/v2/upload/files", { + method: "POST", + body: data, + }); + + if (!res.ok) return null; + + const json = await res.json(); + const content = json.data[0]; + + return content.url as string; + } catch (e) { + console.error(String(e)); + return null; + } + } + public open_thread(id: string) { return new WebviewWindow(`event-${id}`, { title: "Thread", @@ -364,4 +432,17 @@ export class Ark { titleBarStyle: "overlay", }); } + + public open_editor() { + return new WebviewWindow("editor", { + title: "Editor", + url: "/editor", + minWidth: 500, + width: 600, + height: 400, + hiddenTitle: true, + titleBarStyle: "overlay", + fileDropEnabled: true, + }); + } } diff --git a/packages/lume-column-foryou/package.json b/packages/lume-column-foryou/package.json index e95e13a5..aac225c9 100644 --- a/packages/lume-column-foryou/package.json +++ b/packages/lume-column-foryou/package.json @@ -6,7 +6,6 @@ "dependencies": { "@lume/ark": "workspace:^", "@lume/icons": "workspace:^", - "@lume/storage": "workspace:^", "@lume/ui": "workspace:^", "@lume/utils": "workspace:^", "@tanstack/react-query": "^5.20.5", diff --git a/packages/storage/package.json b/packages/storage/package.json deleted file mode 100644 index 78e15393..00000000 --- a/packages/storage/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@lume/storage", - "version": "0.0.0", - "main": "./src/index.ts", - "private": true, - "license": "MIT", - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@tauri-apps/plugin-store": "2.0.0-beta.0", - "react": "^18.2.0", - "scheduler": "^0.23.0", - "use-context-selector": "^1.4.1", - "virtua": "^0.27.0", - "zustand": "^4.5.0" - }, - "devDependencies": { - "@lume/tsconfig": "workspace:*", - "@lume/types": "workspace:*", - "@lume/utils": "workspace:^", - "@types/react": "^18.2.55", - "typescript": "^5.3.3" - } -} diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts deleted file mode 100644 index 1a8f850f..00000000 --- a/packages/storage/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./provider"; diff --git a/packages/storage/src/provider.tsx b/packages/storage/src/provider.tsx deleted file mode 100644 index 8d598c4f..00000000 --- a/packages/storage/src/provider.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { LumeColumn } from "@lume/types"; -import { locale, platform } from "@tauri-apps/plugin-os"; -import { Store } from "@tauri-apps/plugin-store"; -import { - MutableRefObject, - PropsWithChildren, - useCallback, - useRef, - useState, -} from "react"; -import { createContext, useContextSelector } from "use-context-selector"; -import { type VListHandle } from "virtua"; -import { LumeStorage } from "./storage"; - -const platformName = await platform(); -const osLocale = (await locale()).slice(0, 2); - -const store = new Store("lume.dat"); -const storage = new LumeStorage(store, platformName, osLocale); -await storage.init(); - -type StorageContext = { - storage: LumeStorage; - column: { - columns: LumeColumn[]; - vlistRef: MutableRefObject; - create: (column: LumeColumn) => void; - remove: (id: number) => void; - move: (id: number, position: "left" | "right") => void; - update: (id: number, title: string, content: string) => void; - }; -}; - -const StorageContext = createContext(null); - -export const StorageProvider = ({ children }: PropsWithChildren) => { - const vlistRef = useRef(null); - - const [columns, setColumns] = useState([ - { - id: 1, - title: "Newsfeed", - content: "", - }, - { - id: 2, - title: "For You", - content: "", - }, - ]); - - const create = useCallback((column: LumeColumn) => { - setColumns((prev) => [...prev, column]); - vlistRef?.current.scrollToIndex(columns.length); - }, []); - - const remove = useCallback((id: number) => { - setColumns((prev) => prev.filter((t) => t.id !== id)); - }, []); - - const update = useCallback( - (id: number, title: string, content: string) => { - const newCols = columns.map((col) => { - if (col.id === id) { - return { ...col, title, content }; - } - return col; - }); - - setColumns(newCols); - }, - [columns], - ); - - const move = useCallback( - (id: number, position: "left" | "right") => { - const newCols = [...columns]; - - const col = newCols.find((el) => el.id === id); - const colIndex = newCols.findIndex((el) => el.id === id); - - newCols.splice(colIndex, 1); - - if (position === "left") newCols.splice(colIndex - 1, 0, col); - if (position === "right") newCols.splice(colIndex + 1, 0, col); - - setColumns(newCols); - }, - [columns], - ); - - return ( - - {children} - - ); -}; - -export const useStorage = () => { - const context = useContextSelector(StorageContext, (state) => state.storage); - if (context === undefined) { - throw new Error("Storage Provider is required"); - } - return context; -}; - -export const useColumn = () => { - const context = useContextSelector(StorageContext, (state) => state.column); - if (context === undefined) { - throw new Error("Storage Provider is required"); - } - return context; -}; diff --git a/packages/storage/src/storage.ts b/packages/storage/src/storage.ts deleted file mode 100644 index 2b2001a7..00000000 --- a/packages/storage/src/storage.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Settings } from "@lume/types"; -import { Platform } from "@tauri-apps/plugin-os"; -import { Store } from "@tauri-apps/plugin-store"; - -export class LumeStorage { - #store: Store; - readonly platform: Platform; - readonly locale: string; - public settings: Settings; - - constructor(store: Store, platform: Platform, locale: string) { - this.#store = store; - this.locale = locale; - this.platform = platform; - this.settings = { - autoupdate: false, - nsecbunker: false, - media: true, - hashtag: true, - lowPower: false, - translation: false, - translateApiKey: "", - instantZap: false, - defaultZapAmount: 21, - nwc: "", - }; - } - - public async init() { - this.loadSettings(); - } - - public async loadSettings() { - const data = await this.#store.get("settings"); - if (!data) return; - - const settings = JSON.parse(data as string) as Settings; - - if (Object.keys(settings).length) { - for (const [key, value] of Object.entries(settings)) { - this.settings[key] = value; - } - } - } - - public async createSetting(key: string, value: string | number | boolean) { - this.settings[key] = value; - - const settings: Settings = JSON.parse(await this.#store.get("settings")); - const newSettings = { ...settings, key: value }; - - await this.#store.set("settings", newSettings); - await this.#store.save(); - } -} diff --git a/packages/storage/tsconfig.json b/packages/storage/tsconfig.json deleted file mode 100644 index 86a6f0b8..00000000 --- a/packages/storage/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "@lume/tsconfig/base.json", - "compilerOptions": { - "outDir": "dist" - }, - "exclude": ["node_modules", "dist"] -} diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 4d546806..2ad8e838 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -54,6 +54,11 @@ export interface Metadata { lud16?: string; } +export interface Contact { + pubkey: string; + profile: Metadata; +} + export interface Account { npub: string; contacts?: string[]; diff --git a/packages/ui/package.json b/packages/ui/package.json index 78484c8b..dcf5e2b8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -7,7 +7,6 @@ "@getalby/sdk": "^3.3.0", "@lume/ark": "workspace:^", "@lume/icons": "workspace:^", - "@lume/storage": "workspace:^", "@lume/utils": "workspace:^", "@nostr-dev-kit/ndk": "^2.4.1", "@radix-ui/react-accordion": "^1.1.2", diff --git a/packages/ui/src/column/header.tsx b/packages/ui/src/column/header.tsx index 737d4de2..9e9c8f7c 100644 --- a/packages/ui/src/column/header.tsx +++ b/packages/ui/src/column/header.tsx @@ -1,89 +1,80 @@ import { - ChevronDownIcon, - MoveLeftIcon, - MoveRightIcon, - RefreshIcon, - TrashIcon, + ChevronDownIcon, + MoveLeftIcon, + MoveRightIcon, + RefreshIcon, + TrashIcon, } from "@lume/icons"; -import { useColumn } from "@lume/storage"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { useQueryClient } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; -import { useColumnContext } from "./provider"; -export function ColumnHeader({ - queryKey, -}: { - queryKey?: string[]; -}) { - const { t } = useTranslation(); - const { move, remove } = useColumn(); +export function ColumnHeader({ queryKey }: { queryKey?: string[] }) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); - const column = useColumnContext(); - const queryClient = useQueryClient(); + const refresh = async () => { + if (queryKey) await queryClient.refetchQueries({ queryKey }); + }; - const refresh = async () => { - if (queryKey) await queryClient.refetchQueries({ queryKey }); - }; - - return ( - -
- -
-
{column.title}
- -
-
- - - - - - - - - - - - - - - - - -
-
- ); + return ( + +
+ +
+
{column.title}
+ +
+
+ + + + + + + + + + + + + + + + + +
+
+ ); } diff --git a/packages/ui/src/editor/form.tsx b/packages/ui/src/editor/form.tsx index f75de83d..5c767deb 100644 --- a/packages/ui/src/editor/form.tsx +++ b/packages/ui/src/editor/form.tsx @@ -1,6 +1,4 @@ -import { useArk } from "@lume/ark"; import { LoaderIcon, TrashIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; import { cn, editorValueAtom } from "@lume/utils"; import { invoke } from "@tauri-apps/api/core"; import { useAtom } from "jotai"; diff --git a/packages/ui/src/editor/utils.ts b/packages/ui/src/editor/utils.ts index 66bf33ee..5bf9e946 100644 --- a/packages/ui/src/editor/utils.ts +++ b/packages/ui/src/editor/utils.ts @@ -1,4 +1,3 @@ -import { NDKCacheUserProfile } from "@lume/types"; import { ReactNode } from "react"; import ReactDOM from "react-dom"; import { BaseEditor, Transforms } from "slate"; diff --git a/packages/ui/src/note/child.tsx b/packages/ui/src/note/child.tsx index e4f03dc2..446509d2 100644 --- a/packages/ui/src/note/child.tsx +++ b/packages/ui/src/note/child.tsx @@ -21,12 +21,10 @@ export function NoteChild({ const richContent = useMemo(() => { if (!data) return ""; - let parsedContent: string | ReactNode[] = data.content.replace( - /\n+/g, - "\n", - ); + let parsedContent: string | ReactNode[] = + data.content.substring(0, 160) + "..."; - const text = parsedContent as string; + const text = data.content; const words = text.split(/( |\n)/); const hashtags = words.filter((word) => word.startsWith("#")); @@ -104,7 +102,7 @@ export function NoteChild({
-
+
{richContent}
diff --git a/packages/ui/src/note/mentions/note.tsx b/packages/ui/src/note/mentions/note.tsx index 0984459a..19335184 100644 --- a/packages/ui/src/note/mentions/note.tsx +++ b/packages/ui/src/note/mentions/note.tsx @@ -103,9 +103,9 @@ export function MentionNote({ } return ( -
+
- +
@@ -117,11 +117,11 @@ export function MentionNote({
-
+
{richContent}
{openable ? ( -
+