feat: add for you column

This commit is contained in:
2024-01-20 09:06:00 +07:00
parent a3460418f6
commit b726ae3c7c
25 changed files with 511 additions and 91 deletions

View File

@@ -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 () => {

View File

@@ -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 ? (

View File

@@ -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>
);
});
}

View File

@@ -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>
);
});
}

View File

@@ -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}