mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 11:43:30 +00:00
add hashtag block modal
This commit is contained in:
parent
a4e03a59eb
commit
8d761caf3e
@ -91,7 +91,7 @@ export function UnknownsModal({ data }: { data: Chats[] }) {
|
|||||||
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-5">
|
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-5">
|
||||||
{data.map((user) => (
|
{data.map((user) => (
|
||||||
<div
|
<div
|
||||||
key={user.event_id}
|
key={user.event_id || user.id}
|
||||||
className="group flex items-center justify-between px-4 py-3 hover:bg-zinc-800"
|
className="group flex items-center justify-between px-4 py-3 hover:bg-zinc-800"
|
||||||
>
|
>
|
||||||
<User pubkey={user.sender_pubkey} />
|
<User pubkey={user.sender_pubkey} />
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AddFeedBlock } from '@app/space/components/addFeed';
|
import { AddFeedBlock } from '@app/space/components/addFeed';
|
||||||
|
import { AddHashTagBlock } from '@app/space/components/addHashtag';
|
||||||
import { AddImageBlock } from '@app/space/components/addImage';
|
import { AddImageBlock } from '@app/space/components/addImage';
|
||||||
|
|
||||||
export function AddBlock() {
|
export function AddBlock() {
|
||||||
@ -6,6 +7,7 @@ export function AddBlock() {
|
|||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<AddImageBlock />
|
<AddImageBlock />
|
||||||
<AddFeedBlock />
|
<AddFeedBlock />
|
||||||
|
<AddHashTagBlock />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ export function AddFeedBlock() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
className="inline-flex h-9 w-56 items-center justify-start gap-2.5 rounded-md px-2.5"
|
className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
|
174
src/app/space/components/addHashtag.tsx
Normal file
174
src/app/space/components/addHashtag.tsx
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { Fragment, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
|
import { createBlock } from '@libs/storage';
|
||||||
|
|
||||||
|
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
import { BLOCK_KINDS } from '@stores/constants';
|
||||||
|
import { ADD_HASHTAGBLOCK_SHORTCUT } from '@stores/shortcuts';
|
||||||
|
|
||||||
|
export function AddHashTagBlock() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useHotkeys(ADD_HASHTAGBLOCK_SHORTCUT, () => openModal());
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isDirty, isValid },
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
const block = useMutation({
|
||||||
|
mutationFn: (data: { kind: number; title: string; content: string }) => {
|
||||||
|
return createBlock(data.kind, data.title, data.content);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['blocks'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: { hashtag: string }) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// mutate
|
||||||
|
block.mutate({
|
||||||
|
kind: BLOCK_KINDS.hashtag,
|
||||||
|
title: data.hashtag,
|
||||||
|
content: data.hashtag.replace('#', ''),
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
// reset form
|
||||||
|
reset();
|
||||||
|
// close modal
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openModal()}
|
||||||
|
className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
|
<CommandIcon width={12} height={12} className="text-zinc-500" />
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
|
<span className="text-sm leading-none text-zinc-500">T</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium text-zinc-400">New hashtag block</h5>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-50" onClose={closeModal}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
|
||||||
|
</Transition.Child>
|
||||||
|
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
||||||
|
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-semibold leading-none text-zinc-100"
|
||||||
|
>
|
||||||
|
Create hashtag block
|
||||||
|
</Dialog.Title>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={closeModal}
|
||||||
|
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||||
|
>
|
||||||
|
<CancelIcon width={14} height={14} className="text-zinc-300" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Dialog.Description className="text-sm leading-tight text-zinc-400">
|
||||||
|
Pin the hashtag you want to keep follow up
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="mb-0 flex h-full w-full flex-col gap-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="title"
|
||||||
|
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
|
||||||
|
>
|
||||||
|
Hashtag *
|
||||||
|
</label>
|
||||||
|
<div className="after:shadow-highlight relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[6px] after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
|
||||||
|
<input
|
||||||
|
type={'text'}
|
||||||
|
{...register('hashtag', {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder="#"
|
||||||
|
className="shadow-input relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isDirty || !isValid}
|
||||||
|
className="shadow-button inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
|
) : (
|
||||||
|
'Confirm'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,24 +1,24 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react';
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { open } from '@tauri-apps/api/dialog';
|
|
||||||
import { Body, fetch } from '@tauri-apps/api/http';
|
|
||||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
import { createBlock } from '@libs/storage';
|
import { createBlock } from '@libs/storage';
|
||||||
|
|
||||||
import { CancelIcon, CommandIcon } from '@shared/icons';
|
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||||
import { Image } from '@shared/image';
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
|
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
|
||||||
import { ADD_IMAGEBLOCK_SHORTCUT } from '@stores/shortcuts';
|
import { ADD_IMAGEBLOCK_SHORTCUT } from '@stores/shortcuts';
|
||||||
|
|
||||||
import { createBlobFromFile } from '@utils/createBlobFromFile';
|
|
||||||
import { usePublish } from '@utils/hooks/usePublish';
|
import { usePublish } from '@utils/hooks/usePublish';
|
||||||
|
import { useImageUploader } from '@utils/hooks/useUploader';
|
||||||
|
|
||||||
export function AddImageBlock() {
|
export function AddImageBlock() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const upload = useImageUploader();
|
||||||
|
|
||||||
const { publish } = usePublish();
|
const { publish } = usePublish();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -45,54 +45,6 @@ export function AddImageBlock() {
|
|||||||
formState: { isDirty, isValid },
|
formState: { isDirty, isValid },
|
||||||
} = useForm();
|
} = useForm();
|
||||||
|
|
||||||
const openFileDialog = async () => {
|
|
||||||
const selected: any = await open({
|
|
||||||
multiple: false,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: 'Image',
|
|
||||||
extensions: ['png', 'jpeg', 'jpg'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Array.isArray(selected)) {
|
|
||||||
// user selected multiple files
|
|
||||||
} else if (selected === null) {
|
|
||||||
// user cancelled the selection
|
|
||||||
} else {
|
|
||||||
const filename = selected.split('/').pop();
|
|
||||||
const file = await createBlobFromFile(selected);
|
|
||||||
const buf = await file.arrayBuffer();
|
|
||||||
|
|
||||||
const res: any = await fetch('https://void.cat/upload?cli=false', {
|
|
||||||
method: 'POST',
|
|
||||||
timeout: 5,
|
|
||||||
headers: {
|
|
||||||
accept: '*/*',
|
|
||||||
'Content-Type': 'application/octet-stream',
|
|
||||||
'V-Filename': filename,
|
|
||||||
'V-Description': 'Upload from https://lume.nu',
|
|
||||||
'V-Strip-Metadata': 'true',
|
|
||||||
},
|
|
||||||
body: Body.bytes(buf),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const imageURL = `https://void.cat/d/${res.data.file.id}.webp`;
|
|
||||||
tags.current = [
|
|
||||||
['url', imageURL],
|
|
||||||
['m', res.data.file.metadata.mimeType],
|
|
||||||
['x', res.data.file.metadata.digest],
|
|
||||||
['size', res.data.file.metadata.size],
|
|
||||||
['magnet', res.data.file.metadata.magnetLink],
|
|
||||||
];
|
|
||||||
|
|
||||||
setImage(imageURL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const block = useMutation({
|
const block = useMutation({
|
||||||
mutationFn: (data: { kind: number; title: string; content: string }) => {
|
mutationFn: (data: { kind: number; title: string; content: string }) => {
|
||||||
return createBlock(data.kind, data.title, data.content);
|
return createBlock(data.kind, data.title, data.content);
|
||||||
@ -102,6 +54,13 @@ export function AddImageBlock() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const uploadImage = async () => {
|
||||||
|
const image = await upload(null);
|
||||||
|
if (image.url) {
|
||||||
|
setImage(image.url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: { kind: number; title: string; content: string }) => {
|
const onSubmit = async (data: { kind: number; title: string; content: string }) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
@ -127,7 +86,7 @@ export function AddImageBlock() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
className="inline-flex h-9 w-56 items-center justify-start gap-2.5 rounded-md px-2.5"
|
className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
||||||
@ -233,7 +192,7 @@ export function AddImageBlock() {
|
|||||||
/>
|
/>
|
||||||
<div className="absolute bottom-3 right-3 z-10">
|
<div className="absolute bottom-3 right-3 z-10">
|
||||||
<button
|
<button
|
||||||
onClick={() => openFileDialog()}
|
onClick={() => uploadImage()}
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex h-6 items-center justify-center rounded bg-zinc-900 px-3 text-sm font-medium text-zinc-300 ring-1 ring-zinc-800 hover:bg-zinc-800"
|
className="inline-flex h-6 items-center justify-center rounded bg-zinc-900 px-3 text-sm font-medium text-zinc-300 ring-1 ring-zinc-800 hover:bg-zinc-800"
|
||||||
>
|
>
|
||||||
@ -249,27 +208,7 @@ export function AddImageBlock() {
|
|||||||
className="shadow-button inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
className="shadow-button inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<svg
|
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||||
className="h-4 w-4 animate-spin text-black dark:text-zinc-100"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<title id="loading">Loading</title>
|
|
||||||
<circle
|
|
||||||
className="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
) : (
|
||||||
'Confirm'
|
'Confirm'
|
||||||
)}
|
)}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export const COMPOSE_SHORTCUT = 'meta+n';
|
export const COMPOSE_SHORTCUT = 'meta+n';
|
||||||
export const ADD_IMAGEBLOCK_SHORTCUT = 'meta+i';
|
export const ADD_IMAGEBLOCK_SHORTCUT = 'meta+i';
|
||||||
export const ADD_FEEDBLOCK_SHORTCUT = 'meta+f';
|
export const ADD_FEEDBLOCK_SHORTCUT = 'meta+f';
|
||||||
|
export const ADD_HASHTAGBLOCK_SHORTCUT = 'meta+t';
|
||||||
|
Loading…
Reference in New Issue
Block a user