mirror of
https://github.com/lumehq/lume.git
synced 2025-08-09 06:12:08 +02:00
feat(rail): edit title & open user notes
This commit is contained in:
@@ -13,8 +13,8 @@
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose :where(iframe):not(:where([class~="not-prose"] *)) {
|
.prose :where(iframe):not(:where([class~='not-prose'] *)) {
|
||||||
@apply aspect-video w-full h-auto mx-auto;
|
@apply mx-auto aspect-video h-auto w-full;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ input::-ms-clear {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror p.is-empty::before {
|
.ProseMirror p.is-empty::before {
|
||||||
@apply text-neutral-600 dark:text-neutral-400 float-left h-0 pointer-events-none content-[attr(data-placeholder)];
|
@apply pointer-events-none float-left h-0 text-neutral-600 content-[attr(data-placeholder)] dark:text-neutral-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror img.ProseMirror-selectednode {
|
.ProseMirror img.ProseMirror-selectednode {
|
||||||
|
@@ -65,7 +65,7 @@ export const ChatListItem = memo(function ChatListItem({ event }: { event: NDKEv
|
|||||||
{user?.name ||
|
{user?.name ||
|
||||||
user?.display_name ||
|
user?.display_name ||
|
||||||
user?.displayName ||
|
user?.displayName ||
|
||||||
displayNpub(event.pubkey, 16)}
|
displayNpub(event.pubkey)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="max-w-[10rem] truncate text-sm">{decryptedContent}</div>
|
<div className="max-w-[10rem] truncate text-sm">{decryptedContent}</div>
|
||||||
|
@@ -47,7 +47,7 @@ export function MentionPopupItem({ pubkey, embed }: { pubkey: string; embed?: st
|
|||||||
{user?.display_name || user?.displayName || user?.name}
|
{user?.display_name || user?.displayName || user?.name}
|
||||||
</h5>
|
</h5>
|
||||||
<span className="text-sm leading-none text-neutral-600 dark:text-neutral-400">
|
<span className="text-sm leading-none text-neutral-600 dark:text-neutral-400">
|
||||||
{displayNpub(pubkey, 16)}
|
{displayNpub(pubkey)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -115,7 +115,7 @@ export function ArticleNoteScreen() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-4 border-l border-neutral-100 px-3 dark:border-neutral-900 xl:col-span-3">
|
<div className="col-span-4 border-l border-neutral-100 px-3 xl:col-span-3 dark:border-neutral-900">
|
||||||
<div className="mb-3 border-b border-neutral-100 pb-3 dark:border-neutral-900">
|
<div className="mb-3 border-b border-neutral-100 pb-3 dark:border-neutral-900">
|
||||||
<NoteReplyForm rootEvent={data} />
|
<NoteReplyForm rootEvent={data} />
|
||||||
</div>
|
</div>
|
||||||
|
@@ -104,7 +104,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-400">
|
<span className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
{displayNpub(pubkey, 16)}
|
{displayNpub(pubkey)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -330,6 +330,14 @@ export class Ark {
|
|||||||
if (res) return id;
|
if (res) return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async renameWidget(id: string, title: string) {
|
||||||
|
const res = await this.#storage.execute(
|
||||||
|
'UPDATE widgets SET title = $2 WHERE id = $1;',
|
||||||
|
[id, title]
|
||||||
|
);
|
||||||
|
if (res) return res;
|
||||||
|
}
|
||||||
|
|
||||||
public async createSetting(key: string, value: string | undefined) {
|
public async createSetting(key: string, value: string | undefined) {
|
||||||
if (value) {
|
if (value) {
|
||||||
return await this.#storage.execute(
|
return await this.#storage.execute(
|
||||||
|
@@ -7,9 +7,14 @@ import { Link } from 'react-router-dom';
|
|||||||
|
|
||||||
import { HorizontalDotsIcon } from '@shared/icons';
|
import { HorizontalDotsIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { WIDGET_KIND } from '@utils/constants';
|
||||||
|
import { useWidget } from '@utils/hooks/useWidget';
|
||||||
|
|
||||||
export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
|
export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const { addWidget } = useWidget();
|
||||||
|
|
||||||
const copyID = async () => {
|
const copyID = async () => {
|
||||||
await writeText(nip19.neventEncode({ id: id, author: pubkey } as EventPointer));
|
await writeText(nip19.neventEncode({ id: id, author: pubkey } as EventPointer));
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -49,6 +54,21 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
Copy ID
|
Copy ID
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-200 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||||
|
onClick={() =>
|
||||||
|
addWidget.mutate({
|
||||||
|
kind: WIDGET_KIND.user,
|
||||||
|
title: pubkey,
|
||||||
|
content: pubkey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Open Notes
|
||||||
|
</button>
|
||||||
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<Link
|
<Link
|
||||||
to={`/users/${pubkey}`}
|
to={`/users/${pubkey}`}
|
||||||
|
@@ -474,6 +474,8 @@ export const User = memo(function User({
|
|||||||
<span>{createdAt}</span>
|
<span>{createdAt}</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{fallbackName}</span>
|
<span>{fallbackName}</span>
|
||||||
|
<div className="grow"></div>
|
||||||
|
<MoreActions id={eventId} pubkey={pubkey} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -96,7 +96,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
{displayNpub(pubkey, 16)}
|
{displayNpub(pubkey)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,13 +1,16 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useArk } from '@libs/ark';
|
import { useArk } from '@libs/ark';
|
||||||
|
|
||||||
import { CancelIcon } from '@shared/icons';
|
import { CancelIcon, EditIcon, EnterIcon } from '@shared/icons';
|
||||||
import { User } from '@shared/user';
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
|
import { cropText } from '@utils/formater';
|
||||||
import { useWidget } from '@utils/hooks/useWidget';
|
import { useWidget } from '@utils/hooks/useWidget';
|
||||||
|
|
||||||
export function TitleBar({
|
export function TitleBar({
|
||||||
id,
|
id,
|
||||||
title,
|
title: aTitle,
|
||||||
isLive,
|
isLive,
|
||||||
}: {
|
}: {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -15,7 +18,15 @@ export function TitleBar({
|
|||||||
isLive?: boolean;
|
isLive?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { ark } = useArk();
|
const { ark } = useArk();
|
||||||
const { removeWidget } = useWidget();
|
|
||||||
|
const [title, setTitle] = useState(aTitle);
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const { removeWidget, renameWidget } = useWidget();
|
||||||
|
|
||||||
|
const submitTitleChange = () => {
|
||||||
|
renameWidget.mutate({ id, title });
|
||||||
|
setEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-11 w-full shrink-0 grid-cols-3 items-center px-3">
|
<div className="grid h-11 w-full shrink-0 grid-cols-3 items-center px-3">
|
||||||
@@ -44,14 +55,50 @@ export function TitleBar({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : !editing ? (
|
||||||
<h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
|
<h3
|
||||||
{title}
|
title={title}
|
||||||
|
className="text-sm font-semibold text-neutral-900 dark:text-neutral-100"
|
||||||
|
>
|
||||||
|
{cropText(title)}
|
||||||
</h3>
|
</h3>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
onKeyUp={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
submitTitleChange();
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setTitle(aTitle);
|
||||||
|
setEditing(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="text"
|
||||||
|
spellCheck={false}
|
||||||
|
autoFocus={editing}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
placeholder="type here..."
|
||||||
|
value={title}
|
||||||
|
className="dark:transparent max-h-6 border-transparent bg-transparent px-3 text-sm placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
|
></input>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 flex justify-end">
|
|
||||||
{id !== '9999' && id !== '9998' ? (
|
{id !== '9999' && id !== '9998' ? (
|
||||||
|
<div className="col-span-1 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => (editing ? submitTitleChange() : setEditing(true))}
|
||||||
|
className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded text-neutral-900 backdrop-blur-xl hover:bg-neutral-100 dark:text-neutral-100 dark:hover:bg-neutral-900"
|
||||||
|
>
|
||||||
|
{editing ? (
|
||||||
|
<EnterIcon className="h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<EditIcon className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeWidget.mutate(id)}
|
onClick={() => removeWidget.mutate(id)}
|
||||||
@@ -59,8 +106,8 @@ export function TitleBar({
|
|||||||
>
|
>
|
||||||
<CancelIcon className="h-3 w-3" />
|
<CancelIcon className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -43,9 +43,14 @@ export function formatCreatedAt(time: number, message: boolean = false) {
|
|||||||
return formated;
|
return formated;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function displayNpub(pubkey: string, len: number, separator?: string) {
|
export function displayNpub(pubkey: string, len: number = 16, separator?: string) {
|
||||||
const npub = pubkey.startsWith('npub1') ? pubkey : (nip19.npubEncode(pubkey) as string);
|
const npub = pubkey.startsWith('npub1') ? pubkey : (nip19.npubEncode(pubkey) as string);
|
||||||
if (npub.length <= len) return npub;
|
|
||||||
|
return cropText(npub, len, separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cropText(text: string, len: number = 16, separator?: string) {
|
||||||
|
if (text.length <= len) return text;
|
||||||
|
|
||||||
separator = separator || ' ... ';
|
separator = separator || ' ... ';
|
||||||
|
|
||||||
@@ -54,7 +59,7 @@ export function displayNpub(pubkey: string, len: number, separator?: string) {
|
|||||||
frontChars = Math.ceil(charsToShow / 2),
|
frontChars = Math.ceil(charsToShow / 2),
|
||||||
backChars = Math.floor(charsToShow / 2);
|
backChars = Math.floor(charsToShow / 2);
|
||||||
|
|
||||||
return npub.substr(0, frontChars) + separator + npub.substr(npub.length - backChars);
|
return text.substr(0, frontChars) + separator + text.substr(text.length - backChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert number to K, M, B, T, etc.
|
// convert number to K, M, B, T, etc.
|
||||||
|
@@ -67,5 +67,29 @@ export function useWidget() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { addWidget, replaceWidget, removeWidget };
|
const renameWidget = useMutation({
|
||||||
|
mutationFn: async ({ id, title }: { id: string; title: string }) => {
|
||||||
|
// Cancel any outgoing refetches
|
||||||
|
await queryClient.cancelQueries({ queryKey: ['widgets'] });
|
||||||
|
|
||||||
|
// Snapshot the previous value
|
||||||
|
const prevWidgets = queryClient.getQueryData(['widgets']);
|
||||||
|
|
||||||
|
// Optimistically update to the new value
|
||||||
|
queryClient.setQueryData(['widgets'], (prev: Widget[]) =>
|
||||||
|
prev.filter((t) => t.id !== id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update in database
|
||||||
|
await ark.renameWidget(id, title);
|
||||||
|
|
||||||
|
// Return a context object with the snapshotted value
|
||||||
|
return { prevWidgets };
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['widgets'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { addWidget, replaceWidget, removeWidget, renameWidget };
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user