note preview

This commit is contained in:
Martti Malmi 2023-08-16 14:57:44 +03:00
parent 7e83722b49
commit eb8546a199
5 changed files with 102 additions and 141 deletions

View File

@ -1,63 +0,0 @@
import Show from '@/components/helpers/Show';
import SafeImg from '@/components/SafeImg';
import Torrent from '@/components/Torrent';
const AttachmentPreview = ({ attachments, torrentId, removeAttachments }) => {
return (
<>
<Show when={torrentId}>
<Torrent preview={true} torrentId={torrentId} />
</Show>
<Show when={attachments && attachments.length}>
<p>
<a
href=""
onClick={(e) => {
e.preventDefault();
removeAttachments();
}}
>
Remove Attachment
</a>
</p>
</Show>
{attachments &&
attachments.map((a) => {
const status = a.error ? <span class="error">{a.error}</span> : a.url || 'uploading...';
if (a.type?.startsWith('audio')) {
return (
<>
{status}
<audio controls>
<source src={a.data} />
</audio>
</>
);
}
if (a.type?.startsWith('video')) {
return (
<>
{status}
<video controls loop={true} autoPlay={true} muted={true}>
<source src={a.data} />
</video>
</>
);
}
if (a.type?.startsWith('image')) {
return (
<>
{status}
<SafeImg src={a.data} />
</>
);
}
return 'unknown attachment type';
})}
</>
);
};
export default AttachmentPreview;

View File

@ -3,20 +3,18 @@ import { useCallback, useState } from 'preact/hooks';
import TextArea from '@/components/create/TextArea';
import { sendNostr } from '@/components/create/util';
import EventContent from '@/components/events/note/Content';
import Show from '@/components/helpers/Show';
import localState from '@/LocalState';
import Key from '@/nostr/Key.ts';
import { translate as t } from '@/translations/Translation.mjs';
import Helpers from '@/utils/Helpers.tsx';
import Icons from '@/utils/Icons.tsx';
import AttachmentPreview from './AttachmentPreview';
import Icons from '@/utils/Icons';
type CreateNoteFormProps = {
replyingTo?: string;
onSubmit?: (text: string) => void;
placeholder?: string;
class?: string;
waitForFocus?: boolean;
autofocus?: boolean;
forceAutoFocusMobile?: boolean;
};
@ -26,13 +24,10 @@ function CreateNoteForm({
onSubmit: onFormSubmit,
placeholder = 'type_a_message',
class: className,
waitForFocus,
autofocus,
forceAutoFocusMobile,
}: CreateNoteFormProps) {
const [text, setText] = useState('');
const [attachments, setAttachments] = useState<any[]>([]);
const [torrentId, setTorrentId] = useState('');
const [focused, setFocused] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
@ -41,25 +36,26 @@ function CreateNoteForm({
event.preventDefault();
submit();
},
[text, attachments, torrentId],
[text],
);
const submit = useCallback(async () => {
const resetText = () => {
if (!replyingTo) {
localState.get('public').get('draft').put(null);
}
setText('');
};
const submit = useCallback(async () => {
if (!text.length) return;
const msg: any = { text };
if (replyingTo) msg.replyingTo = replyingTo;
if (attachments.length) msg.attachments = attachments;
await sendNostr(msg);
onFormSubmit?.(msg);
setText('');
setAttachments([]);
setTorrentId('');
}, [text, attachments, torrentId, replyingTo, onFormSubmit]);
resetText();
}, [text, replyingTo, onFormSubmit]);
const attachFileClicked = useCallback((event) => {
event.stopPropagation();
@ -71,48 +67,30 @@ function CreateNoteForm({
}
}, []);
const handleFileAttachments = useCallback(
(files) => {
if (!files) return;
const handleFileAttachments = useCallback((files) => {
if (!files) return;
for (let i = 0; i < files.length; i++) {
const file = files[i];
for (let i = 0; i < files.length; i++) {
const file = files[i];
// Initialize or use existing attachments array
const currentAttachments = [...attachments];
currentAttachments[i] = currentAttachments[i] || { type: file.type };
const formData = new FormData();
formData.append('fileToUpload', file);
// Get the base64 representation of the file
Helpers.getBase64(file).then((base64) => {
currentAttachments[i].data = base64;
setAttachments(currentAttachments);
});
const formData = new FormData();
formData.append('fileToUpload', file);
fetch('https://nostr.build/api/upload/iris.php', {
method: 'POST',
body: formData,
fetch('https://nostr.build/api/upload/iris.php', {
method: 'POST',
body: formData,
})
.then(async (response) => {
const url = await response.json();
if (url) {
setText((prevText) => (prevText ? `${prevText}\n\n${url}` : url));
}
})
.then(async (response) => {
const url = await response.json();
if (url) {
currentAttachments[i].url = url;
setAttachments(currentAttachments);
setText((prevText) => (prevText ? `${prevText}\n\n${url}` : url));
}
})
.catch((error) => {
console.error('upload error', error);
currentAttachments[i].error = 'upload failed';
setAttachments(currentAttachments);
});
}
},
[attachments, text],
);
.catch((error) => {
console.error('upload error', error);
});
}
}, []);
const attachmentsChanged = useCallback(
(event) => {
@ -123,6 +101,17 @@ function CreateNoteForm({
[handleFileAttachments],
);
const onClickCancel = useCallback(
(e) => {
e.preventDefault();
if (text?.split(' ').length < 10 || confirm(t('discard_changes'))) {
resetText();
setFocused(false);
}
},
[text],
);
return (
<form autoComplete="off" className={className || ''} onSubmit={(e) => onMsgFormSubmit(e)}>
<input
@ -135,7 +124,6 @@ function CreateNoteForm({
/>
<TextArea
onFocus={() => setFocused(true)}
setTorrentId={setTorrentId}
submit={submit}
setValue={setText}
value={text}
@ -145,21 +133,35 @@ function CreateNoteForm({
forceAutoFocusMobile={forceAutoFocusMobile}
replyingTo={replyingTo}
/>
<Show when={!waitForFocus || focused}>
<Show when={focused}>
<div className="flex items-center justify-between mt-4">
<button type="button" className="btn" onClick={attachFileClicked}>
{Icons.attach}
</button>
<button type="submit" className="btn btn-primary">
{t('post')}
</button>
<div className="flex flex-row gap-2">
<button className="btn btn-sm btn-neutral" onClick={onClickCancel}>
{t('cancel')}
</button>
<button type="submit" className="btn btn-sm btn-primary" disabled={!text?.length}>
{t('post')}
</button>
</div>
</div>
<Show when={text?.length}>
<div className="p-2 bg-neutral-900 rounded-sm my-4">
<div className="text-xs text-neutral-500 mb-2">{t('preview')}</div>
<EventContent
fullWidth={true}
isPreview={true}
event={{
content: text,
pubkey: Key.getPubKey(),
created_at: Math.floor(Date.now() / 1000),
}}
/>
</div>
</Show>
</Show>
<AttachmentPreview
attachments={attachments}
torrentId={torrentId}
removeAttachments={() => setAttachments([])}
/>
</form>
);
}

View File

@ -11,7 +11,6 @@ import { uploadFile } from '@/utils/uploadFile';
const mentionRegex = /\B@[\u00BF-\u1FFF\u2C00-\uD7FF\w]*$/;
interface TextAreaProps {
setTorrentId: (value: string) => void;
submit: () => void;
attachmentsChanged: (event) => void;
placeholder: string;
@ -24,7 +23,6 @@ interface TextAreaProps {
}
const TextArea: React.FC<TextAreaProps> = ({
setTorrentId,
submit,
attachmentsChanged,
placeholder,
@ -41,6 +39,7 @@ const TextArea: React.FC<TextAreaProps> = ({
useEffect(() => {
const el = ref.current;
if (el) {
el.style.height = 'auto'; // Resetting the height
el.style.height = `${el.scrollHeight}px`;
}
}, [value]);
@ -67,10 +66,6 @@ const TextArea: React.FC<TextAreaProps> = ({
const onPaste = useCallback((event) => {
const clipboardData = event.clipboardData || window.clipboardData;
const pasted = clipboardData.getData('text');
const magnetRegex = /(magnet:\?xt=urn:btih:.*)/gi;
const match = magnetRegex.exec(pasted);
if (match) setTorrentId(match[0]);
if (clipboardData.items) {
const items = clipboardData.items;

View File

@ -9,7 +9,16 @@ import EventDropdown from '../EventDropdown';
import Avatar from './Avatar';
const Author = ({ event, fullWidth, isQuote, standalone, setTranslatedText }) => {
type Props = {
event: any;
fullWidth?: boolean;
isQuote?: boolean;
standalone?: boolean;
setTranslatedText?: any;
isPreview?: boolean;
};
const Author = ({ event, fullWidth, isQuote, standalone, setTranslatedText, isPreview }: Props) => {
const { time, dateStr, timeStr } = useMemo(() => {
const t = new Date(event.created_at * 1000);
const dStr = t.toLocaleString(window.navigator.language, {
@ -26,7 +35,7 @@ const Author = ({ event, fullWidth, isQuote, standalone, setTranslatedText }) =>
}, [event.created_at]);
return (
<div className="flex items-center gap-2 justify-between">
<div className="flex items-center gap-2">
<Show when={fullWidth}>
<Avatar event={event} isQuote={isQuote} standalone={standalone} fullWidth={fullWidth} />
</Show>
@ -47,7 +56,7 @@ const Author = ({ event, fullWidth, isQuote, standalone, setTranslatedText }) =>
</small>
</div>
</div>
<Show when={!isQuote}>
<Show when={!isQuote && !isPreview}>
<div className="flex-1 flex items-center justify-end">
<EventDropdown id={event.id} event={event} onTranslate={setTranslatedText} />
</div>

View File

@ -28,7 +28,25 @@ localState.get('settings').on((s) => {
const MSG_TRUNCATE_LENGTH = 500;
const MSG_TRUNCATE_LINES = 8;
const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, meta }) => {
type Props = {
standalone?: boolean;
isQuote?: boolean;
fullWidth?: boolean;
asInlineQuote?: boolean;
event: any;
meta?: any;
isPreview?: boolean;
};
const Content = ({
standalone,
isQuote,
fullWidth,
asInlineQuote,
event,
meta = {},
isPreview,
}: Props) => {
const [translatedText, setTranslatedText] = useState('');
const [showMore, setShowMore] = useState(false);
const [name, setName] = useState('');
@ -63,13 +81,13 @@ const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, meta })
}
text =
text.length > MSG_TRUNCATE_LENGTH && !showMore && !standalone
text.length > MSG_TRUNCATE_LENGTH && !showMore && !standalone && !isPreview
? `${text.slice(0, MSG_TRUNCATE_LENGTH)}...`
: text;
const lines = text.split('\n');
text =
lines.length > MSG_TRUNCATE_LINES && !showMore && !standalone
lines.length > MSG_TRUNCATE_LINES && !showMore && !standalone && !isPreview
? `${lines.slice(0, MSG_TRUNCATE_LINES).join('\n')}...`
: text;
@ -84,6 +102,7 @@ const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, meta })
return (
<div className={`flex-grow`}>
<Author
isPreview={isPreview}
standalone={standalone}
event={event}
isQuote={isQuote}
@ -110,7 +129,7 @@ const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, meta })
</Show>
</div>
</Show>
<Show when={!asInlineQuote && !standalone && isTooLong()}>
<Show when={!isPreview && !asInlineQuote && !standalone && isTooLong()}>
<a
className="text-sm link mb-2"
onClick={(e) => {
@ -121,7 +140,7 @@ const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, meta })
{t(`show_${showMore ? 'less' : 'more'}`)}
</a>
</Show>
<Show when={!asInlineQuote && loadReactions}>
<Show when={!isPreview && !asInlineQuote && loadReactions}>
<Reactions
key={event.id + 'reactions'}
settings={{ showLikes, showZaps, showReposts }}
@ -135,7 +154,6 @@ const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, meta })
<Show when={standalone}>
<hr className="-mx-2 opacity-10 my-2" />
<CreateNoteForm
waitForFocus={true}
autofocus={!standalone}
replyingTo={event.id}
placeholder={t('write_your_reply')}