feat: add for you column
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
"react-router-dom": "^6.21.3",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"sonner": "^1.3.1",
|
||||
"string-strip-html": "^13.4.5",
|
||||
"tippy.js": "^6.3.7",
|
||||
"use-context-selector": "^1.4.1"
|
||||
},
|
||||
|
||||
@@ -30,6 +30,12 @@ export function ColumnProvider({ children }: { children: ReactNode }) {
|
||||
content: "",
|
||||
kind: COL_TYPES.newsfeed,
|
||||
},
|
||||
{
|
||||
id: 9998,
|
||||
title: "For You",
|
||||
content: "",
|
||||
kind: COL_TYPES.foryou,
|
||||
},
|
||||
]);
|
||||
|
||||
const loadAllColumns = useCallback(async () => {
|
||||
|
||||
@@ -13,10 +13,11 @@ import { NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import getUrls from "get-urls";
|
||||
import { nanoid } from "nanoid";
|
||||
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import { ReactNode, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import { toast } from "sonner";
|
||||
import { stripHtml } from "string-strip-html";
|
||||
import { Hashtag } from "./mentions/hashtag";
|
||||
import { MentionNote } from "./mentions/note";
|
||||
import { MentionUser } from "./mentions/user";
|
||||
@@ -28,10 +29,8 @@ import { useNoteContext } from "./provider";
|
||||
|
||||
export function NoteContent({
|
||||
className,
|
||||
mini = false,
|
||||
}: {
|
||||
className?: string;
|
||||
mini?: boolean;
|
||||
}) {
|
||||
const storage = useStorage();
|
||||
const event = useNoteContext();
|
||||
@@ -45,7 +44,9 @@ export function NoteContent({
|
||||
const richContent = useMemo(() => {
|
||||
if (event.kind !== NDKKind.Text) return content;
|
||||
|
||||
let parsedContent: string | ReactNode[] = content.replace(/\n+/g, "\n");
|
||||
let parsedContent: string | ReactNode[] = stripHtml(
|
||||
content.replace(/\n{2,}\s*/g, "\n"),
|
||||
).result;
|
||||
let linkPreview: string = undefined;
|
||||
let images: string[] = [];
|
||||
let videos: string[] = [];
|
||||
@@ -56,7 +57,7 @@ export function NoteContent({
|
||||
const words = text.split(/( |\n)/);
|
||||
const urls = [...getUrls(text)];
|
||||
|
||||
if (storage.settings.media && !storage.settings.lowPower && !mini) {
|
||||
if (storage.settings.media && !storage.settings.lowPower) {
|
||||
images = urls.filter((word) =>
|
||||
IMAGES.some((el) => {
|
||||
const url = new URL(word);
|
||||
@@ -83,11 +84,9 @@ export function NoteContent({
|
||||
);
|
||||
}
|
||||
|
||||
if (!mini) {
|
||||
events = words.filter((word) =>
|
||||
NOSTR_EVENTS.some((el) => word.startsWith(el)),
|
||||
);
|
||||
}
|
||||
events = words.filter((word) =>
|
||||
NOSTR_EVENTS.some((el) => word.startsWith(el)),
|
||||
);
|
||||
|
||||
const hashtags = words.filter((word) => word.startsWith("#"));
|
||||
const mentions = words.filter((word) =>
|
||||
@@ -184,11 +183,9 @@ export function NoteContent({
|
||||
},
|
||||
);
|
||||
|
||||
if (!mini) {
|
||||
parsedContent = reactStringReplace(parsedContent, "\n", () => {
|
||||
return <div key={nanoid()} className="h-3" />;
|
||||
});
|
||||
}
|
||||
parsedContent = reactStringReplace(parsedContent, "\n", () => {
|
||||
return <div key={nanoid()} className="h-3" />;
|
||||
});
|
||||
|
||||
if (typeof parsedContent[0] === "string") {
|
||||
parsedContent[0] = parsedContent[0].trimStart();
|
||||
@@ -235,12 +232,7 @@ export function NoteContent({
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"break-p select-text text-balance leading-normal",
|
||||
!mini ? "whitespace-pre-line" : "",
|
||||
)}
|
||||
>
|
||||
<div className="break-p select-text text-balance leading-normal whitespace-pre-line">
|
||||
{richContent}
|
||||
</div>
|
||||
{storage.settings.translation && translate.translatable ? (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PinIcon } from "@lume/icons";
|
||||
import { COL_TYPES, NOSTR_MENTIONS } from "@lume/utils";
|
||||
import { ReactNode, memo, useMemo } from "react";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import { useEvent } from "../../../hooks/useEvent";
|
||||
@@ -9,7 +9,7 @@ import { User } from "../../user";
|
||||
import { Hashtag } from "./hashtag";
|
||||
import { MentionUser } from "./user";
|
||||
|
||||
export const MentionNote = memo(function MentionNote({
|
||||
export function MentionNote({
|
||||
eventId,
|
||||
openable = true,
|
||||
}: { eventId: string; openable?: boolean }) {
|
||||
@@ -66,7 +66,7 @@ export const MentionNote = memo(function MentionNote({
|
||||
to={url.toString()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="break-p font-normal text-blue-500 hover:text-blue-600"
|
||||
className="break-p inline-block truncate w-full font-normal text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{url.toString()}
|
||||
</Link>
|
||||
@@ -104,50 +104,48 @@ export const MentionNote = memo(function MentionNote({
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col w-full my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900 border border-neutral-100 dark:border-neutral-900">
|
||||
<User.Provider pubkey={data.pubkey}>
|
||||
<User.Root className="flex h-10 px-3 items-center gap-2">
|
||||
<User.Avatar className="size-6 shrink-0 rounded-md object-cover" />
|
||||
<div className="flex-1 inline-flex gap-2">
|
||||
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
|
||||
<span className="text-neutral-600 dark:text-neutral-400">·</span>
|
||||
<User.Time
|
||||
time={data.created_at}
|
||||
className="text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="px-3 select-text text-balance leading-normal line-clamp-4 whitespace-pre-line">
|
||||
{richContent}
|
||||
</div>
|
||||
{openable ? (
|
||||
<div className="px-3 h-10 flex items-center justify-between">
|
||||
<Link
|
||||
to={`/events/${data.id}`}
|
||||
className="text-sm font-medium text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
Show more
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () =>
|
||||
await addColumn({
|
||||
kind: COL_TYPES.thread,
|
||||
title: "Thread",
|
||||
content: data.id,
|
||||
})
|
||||
}
|
||||
className="inline-flex items-center justify-center rounded-md text-neutral-600 dark:text-neutral-400 size-6 bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<PinIcon className="size-4" />
|
||||
</button>
|
||||
<div className="flex flex-col w-full my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900 border border-black/5 dark:border-white/5">
|
||||
<User.Provider pubkey={data.pubkey}>
|
||||
<User.Root className="flex h-10 px-3 items-center gap-2">
|
||||
<User.Avatar className="size-6 shrink-0 rounded-md object-cover" />
|
||||
<div className="flex-1 inline-flex gap-2">
|
||||
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
|
||||
<span className="text-neutral-600 dark:text-neutral-400">·</span>
|
||||
<User.Time
|
||||
time={data.created_at}
|
||||
className="text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-3" />
|
||||
)}
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="px-3 select-text text-balance leading-normal line-clamp-4 whitespace-pre-line">
|
||||
{richContent}
|
||||
</div>
|
||||
{openable ? (
|
||||
<div className="px-3 h-10 flex items-center justify-between">
|
||||
<Link
|
||||
to={`/events/${data.id}`}
|
||||
className="text-sm text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
Show more
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () =>
|
||||
await addColumn({
|
||||
kind: COL_TYPES.thread,
|
||||
title: "Thread",
|
||||
content: data.id,
|
||||
})
|
||||
}
|
||||
className="inline-flex items-center justify-center rounded-md text-neutral-600 dark:text-neutral-400 size-6 bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<PinIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-3" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { COL_TYPES } from "@lume/utils";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { memo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useArk } from "../../../hooks/useArk";
|
||||
import { useProfile } from "../../../hooks/useProfile";
|
||||
import { useColumnContext } from "../../column/provider";
|
||||
|
||||
export const MentionUser = memo(function MentionUser({
|
||||
pubkey,
|
||||
}: { pubkey: string }) {
|
||||
export function MentionUser({ pubkey }: { pubkey: string }) {
|
||||
const ark = useArk();
|
||||
const cleanPubkey = ark.getCleanPubkey(pubkey);
|
||||
|
||||
@@ -51,4 +48,4 @@ export const MentionUser = memo(function MentionUser({
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function LinkPreview({ url }: { url: string }) {
|
||||
|
||||
if (status === "pending") {
|
||||
return (
|
||||
<div className="flex flex-col w-full my-1 rounded-lg overflow-hidden bg-neutral-100 dark:bg-neutral-900">
|
||||
<div className="flex flex-col w-full my-1 rounded-lg overflow-hidden bg-neutral-100 dark:bg-neutral-900 border border-black/5 dark:border-white/5">
|
||||
<div className="w-full h-48 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
||||
<div className="flex flex-col gap-2 px-3 py-3">
|
||||
<div className="w-2/3 h-3 rounded animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
||||
@@ -24,7 +24,7 @@ export function LinkPreview({ url }: { url: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.title && !data.image) {
|
||||
if (!data.title && !data.image && !data.description) {
|
||||
return (
|
||||
<Link
|
||||
to={url}
|
||||
@@ -48,6 +48,8 @@ export function LinkPreview({ url }: { url: string }) {
|
||||
<img
|
||||
src={data.image}
|
||||
alt={url}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="object-cover w-full h-48 bg-white rounded-t-lg"
|
||||
/>
|
||||
) : null}
|
||||
@@ -59,7 +61,7 @@ export function LinkPreview({ url }: { url: string }) {
|
||||
</div>
|
||||
) : null}
|
||||
{data.description ? (
|
||||
<div className="mb-2 text-sm break-p line-clamp-3 text-neutral-700 dark:text-neutral-400">
|
||||
<div className="mb-2 text-balance text-sm break-p line-clamp-3 text-neutral-700 dark:text-neutral-400">
|
||||
{data.description}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user