feat: add cache, fix ui
This commit is contained in:
@@ -7,11 +7,20 @@ type Token =
|
||||
| { type: "italic"; value: string }
|
||||
| { type: "strike"; value: string }
|
||||
| { type: "code"; value: string }
|
||||
| { type: "url"; value: string };
|
||||
| { type: "url"; value: string }
|
||||
| { type: "mention"; value: string };
|
||||
|
||||
function renderWhatsAppMessage(message: string): React.ReactNode {
|
||||
type RenderOptions = {
|
||||
getMentionLabel?: (username: string) => string | null;
|
||||
onMentionClick?: (username: string) => void;
|
||||
};
|
||||
|
||||
function renderWhatsAppMessage(
|
||||
message: string,
|
||||
options?: RenderOptions,
|
||||
): React.ReactNode {
|
||||
const regex =
|
||||
/(\*[^*\n]+\*|_[^_\n]+_|~[^~\n]+~|`[^`\n]+`|https?:\/\/[^\s<]+|www\.[^\s<]+)/g;
|
||||
/(\*[^*\n]+\*|_[^_\n]+_|~[^~\n]+~|`[^`\n]+`|https?:\/\/[^\s<]+|www\.[^\s<]+|@\d+)/g;
|
||||
|
||||
const tokens: Token[] = [];
|
||||
let lastIndex = 0;
|
||||
@@ -35,6 +44,11 @@ function renderWhatsAppMessage(message: string): React.ReactNode {
|
||||
tokens.push({ type: "strike", value: value.slice(1, -1) });
|
||||
} else if (value.startsWith("`")) {
|
||||
tokens.push({ type: "code", value: value.slice(1, -1) });
|
||||
} else if (value.startsWith("@")) {
|
||||
tokens.push({
|
||||
type: "mention",
|
||||
value: value.slice(1), // remove @
|
||||
});
|
||||
} else {
|
||||
// Remove punctuation that commonly follows a URL.
|
||||
const url = value.replace(/[.,!?;:)]+$/, "");
|
||||
@@ -110,23 +124,36 @@ function renderWhatsAppMessage(message: string): React.ReactNode {
|
||||
</a>
|
||||
);
|
||||
|
||||
case "mention":
|
||||
const label = options?.getMentionLabel?.(token.value);
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
onClick={() => options?.onMentionClick?.(token.value)}
|
||||
className="font-medium text-blue-200 cursor-pointer hover:underline"
|
||||
>
|
||||
@{label || token.value}
|
||||
</button>
|
||||
);
|
||||
|
||||
case "text":
|
||||
return <React.Fragment key={i}>{token.value}</React.Fragment>;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type Props = {
|
||||
type Props = RenderOptions & {
|
||||
text?: string | null;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function Paragraph({ text, className }: Props) {
|
||||
export default function Paragraph({ text, className, ...options }: Props) {
|
||||
if (!text) return null;
|
||||
|
||||
return (
|
||||
<div className={cn("whitespace-pre-wrap wrap-break-word", className)}>
|
||||
{renderWhatsAppMessage(text)}
|
||||
{renderWhatsAppMessage(text, options)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useDebounceFn } from "@/hooks/useDebounce";
|
||||
import { cn } from "cn";
|
||||
import { SearchIcon, XIcon } from "lucide-react";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
type Props = React.ComponentProps<"input"> & {
|
||||
onValueChange?: (value: string) => void;
|
||||
inputClassName?: string;
|
||||
};
|
||||
|
||||
export default function SearchInput({
|
||||
onValueChange,
|
||||
className,
|
||||
inputClassName,
|
||||
...props
|
||||
}: Props) {
|
||||
const ref = React.useRef<HTMLInputElement>(null!);
|
||||
const closeRef = React.useRef<HTMLButtonElement>(null!);
|
||||
const debounce = useDebounceFn();
|
||||
|
||||
useEffect(() => {
|
||||
ref.current.value = String(props.value || "");
|
||||
}, [props.value]);
|
||||
|
||||
function onClear() {
|
||||
ref.current.value = "";
|
||||
closeRef.current.style.display = "none";
|
||||
onValueChange?.("");
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-xl bg-muted px-3 py-2.5",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<SearchIcon size={17} className="text-muted-foreground" />
|
||||
<input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground",
|
||||
inputClassName,
|
||||
)}
|
||||
defaultValue=""
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
closeRef.current.style.display = value.length ? "block" : "none";
|
||||
debounce(() => onValueChange?.(value), 500);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<button
|
||||
ref={closeRef}
|
||||
aria-label="Close search"
|
||||
className="hidden cursor-pointer"
|
||||
onClick={onClear}
|
||||
>
|
||||
<XIcon size={16} className="text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,22 +10,22 @@ type Props = {
|
||||
};
|
||||
|
||||
const colorSchemes = [
|
||||
"bg-rose-200 text-rose-800",
|
||||
"bg-pink-200 text-pink-800",
|
||||
"bg-fuchsia-200 text-fuchsia-800",
|
||||
"bg-purple-200 text-purple-800",
|
||||
"bg-violet-200 text-violet-800",
|
||||
"bg-indigo-200 text-indigo-800",
|
||||
"bg-blue-200 text-blue-800",
|
||||
"bg-sky-200 text-sky-800",
|
||||
"bg-cyan-200 text-cyan-800",
|
||||
"bg-teal-200 text-teal-800",
|
||||
"bg-emerald-200 text-emerald-800",
|
||||
"bg-green-200 text-green-800",
|
||||
"bg-lime-200 text-lime-800",
|
||||
"bg-yellow-200 text-yellow-800",
|
||||
"bg-amber-200 text-amber-800",
|
||||
"bg-orange-200 text-orange-800",
|
||||
"text-rose-200 bg-rose-800",
|
||||
"text-pink-200 bg-pink-800",
|
||||
"text-fuchsia-200 bg-fuchsia-800",
|
||||
"text-purple-200 bg-purple-800",
|
||||
"text-violet-200 bg-violet-800",
|
||||
"text-indigo-200 bg-indigo-800",
|
||||
"text-blue-200 bg-blue-800",
|
||||
"text-sky-200 bg-sky-800",
|
||||
"text-cyan-200 bg-cyan-800",
|
||||
"text-teal-200 bg-teal-800",
|
||||
"text-emerald-200 bg-emerald-800",
|
||||
"text-green-200 bg-green-800",
|
||||
"text-lime-200 bg-lime-800",
|
||||
"text-yellow-200 bg-yellow-800",
|
||||
"text-amber-200 bg-amber-800",
|
||||
"text-orange-200 bg-orange-800",
|
||||
];
|
||||
|
||||
function getColorBasedOnName(name?: string | null) {
|
||||
@@ -41,6 +41,7 @@ export default function Avatar({
|
||||
iconClassName,
|
||||
}: Props) {
|
||||
const color = getColorBasedOnName(name);
|
||||
const initial = getInitial(name);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -52,8 +53,8 @@ export default function Avatar({
|
||||
>
|
||||
{icon ? (
|
||||
icon
|
||||
) : name ? (
|
||||
getInitial(name)
|
||||
) : initial ? (
|
||||
initial
|
||||
) : (
|
||||
<UserIcon className={cn("size-4", iconClassName)} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user