mirror of
https://github.com/irislib/iris-messenger.git
synced 2024-09-19 17:46:33 +00:00
note preview
This commit is contained in:
parent
7e83722b49
commit
eb8546a199
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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')}
|
||||
|
Loading…
Reference in New Issue
Block a user