wip refactor publicmessageform

This commit is contained in:
Martti Malmi 2023-08-09 20:35:23 +03:00
parent 6da28a95f2
commit 9a4048a18d
13 changed files with 548 additions and 459 deletions

View File

@ -15,6 +15,8 @@ import {
} from '@heroicons/react/24/solid';
import { Link } from 'preact-router';
import CreateNoteForm from '@/components/create/CreateNoteForm';
import Icons from '../Icons';
import localState from '../LocalState';
import Key from '../nostr/Key';
@ -24,7 +26,6 @@ import Show from './helpers/Show';
import Modal from './modal/Modal';
import Avatar from './user/Avatar';
import Name from './user/Name';
import PublicMessageForm from './PublicMessageForm';
const MENU_ITEMS = [
{ url: '/', text: 'home', icon: HomeIcon, activeIcon: HomeIconFull },
@ -80,7 +81,7 @@ export default function Menu() {
const renderNewPostModal = () => (
<Modal centerVertically={true} showContainer={true} onClose={() => setShowNewPostModal(false)}>
<PublicMessageForm
<CreateNoteForm
onSubmit={() => setShowNewPostModal(false)}
placeholder={t('whats_on_your_mind')}
autofocus={true}

View File

@ -1,436 +0,0 @@
import { html } from 'htm/preact';
import $ from 'jquery';
import { Event } from 'nostr-tools';
import { createRef } from 'preact';
import { uploadFile } from '@/utils/uploadFile';
import Component from '../BaseComponent';
import Helpers from '../Helpers';
import Icons from '../Icons';
import localState from '../LocalState';
import Events from '../nostr/Events';
import Key from '../nostr/Key';
import { translate as t } from '../translations/Translation.mjs';
import SafeImg from './SafeImg';
import SearchBox from './SearchBox';
import Torrent from './Torrent';
const mentionRegex = /\B@[\u00BF-\u1FFF\u2C00-\uD7FF\w]*$/;
interface IProps {
replyingTo?: string;
forceAutofocusMobile?: boolean;
autofocus?: boolean;
onSubmit?: (msg: any) => void;
waitForFocus?: boolean;
class?: string;
index?: string;
placeholder?: string;
}
interface IState {
attachments?: any[];
torrentId?: string;
mentioning?: boolean;
focused?: boolean;
}
class PublicMessageForm extends Component<IProps, IState> {
newMsgRef = createRef();
componentDidMount() {
const textEl = $(this.newMsgRef.current);
if (
(!Helpers.isMobile || this.props.forceAutofocusMobile == true) &&
this.props.autofocus !== false
) {
textEl.focus();
}
if (!this.props.replyingTo) {
localState
.get('channels')
.get('public')
.get('msgDraft')
.once((t) => !textEl.val() && textEl.val(t));
} else {
const currentHistoryState = window.history.state;
if (currentHistoryState && currentHistoryState['replyTo' + this.props.replyingTo]) {
textEl.val(currentHistoryState['replyTo' + this.props.replyingTo]);
}
}
}
onMsgFormSubmit(event) {
event.preventDefault();
this.submit();
}
async submit() {
if (!this.props.replyingTo) {
localState.get('channels').get('public').get('msgDraft').put(null);
}
const textEl = $(this.newMsgRef.current);
const text = textEl.val();
if (!text.length) {
return;
}
const msg: any = { text };
if (this.props.replyingTo) {
msg.replyingTo = this.props.replyingTo;
}
if (this.state.attachments) {
msg.attachments = this.state.attachments;
}
await this.sendNostr(msg);
this.props.onSubmit && this.props.onSubmit(msg);
this.setState({ attachments: undefined, torrentId: undefined });
textEl.val('');
textEl.height('');
this.saveDraftToHistory();
}
setTextareaHeight(textarea) {
textarea.style.height = '';
textarea.style.height = `${textarea.scrollHeight}px`;
}
onMsgTextPaste(event) {
const clipboardData = event.clipboardData || window.clipboardData;
// Handling magnet links
const pasted = clipboardData.getData('text');
const magnetRegex = /(magnet:\?xt=urn:btih:.*)/gi;
const match = magnetRegex.exec(pasted);
console.log('magnet match', match);
if (match) {
this.setState({ torrentId: match[0] });
}
if (clipboardData.items) {
const items = clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.startsWith('image/')) {
const blob = items[i].getAsFile();
uploadFile(
blob,
(url) => {
const textEl = $(this.newMsgRef.current);
const currentVal = textEl.val();
if (currentVal) {
textEl.val(currentVal + '\n\n' + url);
} else {
textEl.val(url);
}
},
(errorMsg) => {
console.error(errorMsg);
},
);
}
}
}
}
onKeyUp(e) {
if ([37, 38, 39, 40].indexOf(e.keyCode) != -1) {
this.checkMention(e);
}
}
onKeyDown(e) {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
this.submit();
}
}
saveDraftToHistory() {
const text = $(this.newMsgRef.current).val();
const currentHistoryState = window.history.state;
const newHistoryState = {
...currentHistoryState,
};
newHistoryState['replyTo' + this.props.replyingTo] = text;
window.history.replaceState(newHistoryState, '');
}
onMsgTextInput(event) {
this.setTextareaHeight(event.target);
if (!this.props.replyingTo) {
localState.get('channels').get('public').get('msgDraft').put($(event.target).val());
}
this.checkMention(event);
this.saveDraftToHistory();
}
attachFileClicked(event) {
event.stopPropagation();
event.preventDefault();
$(this.base).find('.attachment-input').click();
}
attachmentsChanged(event) {
// TODO use Upload btn
const files = event.target.files || event.dataTransfer.files;
if (files) {
for (let i = 0; i < files.length; i++) {
const formData = new FormData();
formData.append('fileToUpload', files[i]);
const a = this.state.attachments || [];
a[i] = a[i] || {
type: files[i].type,
};
Helpers.getBase64(files[i]).then((base64) => {
a[i].data = base64;
this.setState({ attachments: a });
});
fetch('https://nostr.build/api/upload/iris.php', {
method: 'POST',
body: formData,
})
.then(async (response) => {
const url = await response.json();
console.log('upload response', url);
if (url) {
a[i].url = url;
this.setState({ attachments: a });
const textEl = $(this.newMsgRef.current);
const currentVal = textEl.val();
if (currentVal) {
textEl.val(currentVal + '\n\n' + url);
} else {
textEl.val(url);
}
}
})
.catch((error) => {
console.error('upload error', error);
a[i].error = 'upload failed';
this.setState({ attachments: a });
});
}
$(event.target).val(null);
$(this.newMsgRef.current).focus();
}
}
onSelectMention(item) {
const textarea = $(this.base).find('textarea').get(0);
const pos = textarea.selectionStart;
const join = [
textarea.value.slice(0, pos).replace(mentionRegex, 'nostr:'),
item.key,
textarea.value.slice(pos),
].join('');
textarea.value = `${join} `;
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = pos + item.key.length;
}
render() {
const textareaPlaceholder =
this.props.placeholder ||
(this.props.index === 'media' ? 'type_a_message_or_paste_a_magnet_link' : 'type_a_message');
return html`<form
autocomplete="off"
class="message-form ${this.props.class || ''} public"
onSubmit=${(e) => this.onMsgFormSubmit(e)}
>
<input
name="attachment-input"
type="file"
class="hidden attachment-input"
accept="image/*, video/*, audio/*"
multiple
onChange=${(e) => this.attachmentsChanged(e)}
/>
<textarea
onDragOver=${(e) => {
e.preventDefault();
e.stopPropagation();
}}
onDrop=${(e) => {
e.preventDefault();
e.stopPropagation();
this.attachmentsChanged(e);
}}
onKeyUp=${(e) => this.onKeyUp(e)}
onKeyDown=${(e) => this.onKeyDown(e)}
onPaste=${(e) => this.onMsgTextPaste(e)}
onInput=${(e) => this.onMsgTextInput(e)}
onFocus=${() => this.setState({ focused: true })}
ref=${this.newMsgRef}
className="p-2 mt-1 w-full h-12 bg-black focus:ring-blue-500 focus:border-blue-500 block w-full text-lg border-gray-700 rounded-md text-white"
type="text"
placeholder="${t(textareaPlaceholder)}"
autocomplete="off"
autocorrect="off"
autocapitalize="sentences"
spellcheck="off"
/>
${this.state.mentioning
? html`
<${SearchBox}
resultsOnly=${true}
query=${this.state.mentioning}
onSelect=${(item) => this.onSelectMention(item)}
/>
`
: ''}
${this.props.waitForFocus && !this.state.focused
? ''
: html`
<div className="flex items-center justify-between mt-4">
<button
type="button"
class="attach-file-btn btn"
onClick=${(e) => this.attachFileClicked(e)}
>
${Icons.attach}
</button>
<button type="submit" className="btn btn-primary">${t('post')}</button>
</div>
`}
<div class="attachment-preview">
${this.state.torrentId
? html` <${Torrent} preview=${true} torrentId=${this.state.torrentId} /> `
: ''}
${this.state.attachments && this.state.attachments.length
? html`
<p>
<a
href=""
onClick=${(e) => {
e.preventDefault();
this.setState({ attachments: undefined });
}}
>${t('remove_attachment')}</a
>
</p>
`
: ''}
${this.state.attachments &&
this.state.attachments.map((a) => {
const status = html` ${a.error
? html`<span class="error">${a.error}</span>`
: a.url || 'uploading...'}`;
// if a.url matches audio regex
if (a.type?.startsWith('audio')) {
return html`
${status}
<audio controls>
<source src=${a.data} />
</audio>
`;
}
// if a.url matches video regex
if (a.type?.startsWith('video')) {
return html`
${status}
<video controls loop=${true} autoplay=${true} muted=${true}>
<source src=${a.data} />
</video>
`;
}
// image regex
if (a.type?.startsWith('image')) {
return html`${status} <${SafeImg} src=${a.data} /> `;
}
return 'unknown attachment type';
})}
</div>
</form>`;
}
async sendNostr(msg: { text: string; replyingTo?: string }) {
const event = {
kind: 1,
content: msg.text,
} as any;
if (msg.replyingTo) {
const id = Key.toNostrHexAddress(msg.replyingTo);
if (!id) {
throw new Error('invalid replyingTo');
}
const replyingTo: Event = await new Promise((resolve) => {
Events.getEventById(id, true, (e) => resolve(e));
});
event.tags = replyingTo.tags.filter((tag) => tag[0] === 'p');
let rootTag = replyingTo.tags?.find((t) => t[0] === 'e' && t[3] === 'root');
if (!rootTag) {
rootTag = replyingTo.tags?.find((t) => t[0] === 'e');
}
if (rootTag) {
event.tags.unshift(['e', id, '', 'reply']);
event.tags.unshift(['e', rootTag[1], '', 'root']);
} else {
event.tags.unshift(['e', id, '', 'root']);
}
if (!event.tags?.find((t) => t[0] === 'p' && t[1] === replyingTo.pubkey)) {
event.tags.push(['p', replyingTo.pubkey]);
}
}
function handleTagged(regex, tagType) {
const taggedItems = [...msg.text.matchAll(regex)]
.map((m) => m[0])
.filter((m, i, a) => a.indexOf(m) === i);
if (taggedItems) {
event.tags = event.tags || [];
for (const tag of taggedItems) {
const match = tag.match(/npub[a-zA-Z0-9]{59,60}/)?.[0];
const hexTag = match && Key.toNostrHexAddress(match);
if (!hexTag) {
continue;
}
const newTag = [tagType, hexTag, '', 'mention'];
// add if not already present
if (!event.tags?.find((t) => t[0] === newTag[0] && t[1] === newTag[1])) {
event.tags.push(newTag);
}
}
}
}
handleTagged(Helpers.pubKeyRegex, 'p');
handleTagged(Helpers.noteRegex, 'e');
const hashtags = [...msg.text.matchAll(Helpers.hashtagRegex)].map((m) => m[0].slice(1));
if (hashtags.length) {
event.tags = event.tags || [];
for (const hashtag of hashtags) {
if (!event.tags?.find((t) => t[0] === 't' && t[1] === hashtag)) {
event.tags.push(['t', hashtag]);
}
}
}
console.log('sending event', event);
return Events.publish(event);
}
checkMention(event: any) {
const val = event.target.value.slice(0, event.target.selectionStart);
const matches = val.match(mentionRegex);
if (matches) {
const match = matches[0].slice(1);
if (!Key.toNostrHexAddress(match)) {
this.setState({ mentioning: match });
}
} else if (this.state.mentioning) {
this.setState({ mentioning: undefined });
}
}
}
export default PublicMessageForm;

View File

@ -0,0 +1,63 @@
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

@ -0,0 +1,279 @@
import $ from 'jquery';
import Component from '@/BaseComponent';
import FileAttachment from '@/components/create/FileAttachment.tsx';
import TextArea from '@/components/create/TextArea';
import { IProps, IState } from '@/components/create/types';
import { sendNostr } from '@/components/create/util.ts';
import Show from '@/components/helpers/Show.tsx';
import SearchBox from '@/components/SearchBox';
import Helpers from '@/Helpers';
import Icons from '@/Icons';
import localState from '@/LocalState';
import Key from '@/nostr/Key';
import { translate as t } from '@/translations/Translation.mjs';
import { uploadFile } from '@/utils/uploadFile';
import AttachmentPreview from './AttachmentPreview';
const mentionRegex = /\B@[\u00BF-\u1FFF\u2C00-\uD7FF\w]*$/;
class CreateNoteForm extends Component<IProps, IState> {
componentDidMount() {
const textEl = $(this.base).find('textarea');
if (
(!Helpers.isMobile || this.props.forceAutofocusMobile == true) &&
this.props.autofocus !== false
) {
textEl.focus();
}
if (!this.props.replyingTo) {
localState
.get('channels')
.get('public')
.get('msgDraft')
.once((text) => this.setState({ text }));
} else {
const currentHistoryState = window.history.state;
if (currentHistoryState && currentHistoryState['replyTo' + this.props.replyingTo]) {
textEl.val(currentHistoryState['replyTo' + this.props.replyingTo]);
}
}
}
onMsgFormSubmit(event) {
event.preventDefault();
this.submit();
}
async submit() {
if (!this.props.replyingTo) {
localState.get('channels').get('public').get('msgDraft').put(null);
}
const text = this.state.text;
if (!text.length) {
return;
}
const msg: any = { text };
if (this.props.replyingTo) {
msg.replyingTo = this.props.replyingTo;
}
if (this.state.attachments) {
msg.attachments = this.state.attachments;
}
await sendNostr(msg);
this.props.onSubmit && this.props.onSubmit(msg);
this.setState({ attachments: undefined, torrentId: undefined, text: '' });
$(this.base).find('textarea').height('');
this.saveDraftToHistory();
}
setTextareaHeight(textarea) {
textarea.style.height = '';
textarea.style.height = `${textarea.scrollHeight}px`;
}
onMsgTextPaste(event) {
const clipboardData = event.clipboardData || window.clipboardData;
// Handling magnet links
const pasted = clipboardData.getData('text');
const magnetRegex = /(magnet:\?xt=urn:btih:.*)/gi;
const match = magnetRegex.exec(pasted);
console.log('magnet match', match);
if (match) {
this.setState({ torrentId: match[0] });
}
if (clipboardData.items) {
const items = clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.startsWith('image/')) {
const blob = items[i].getAsFile();
uploadFile(
blob,
(url) => {
const currentVal = this.state.text;
if (currentVal) {
this.setState({ text: currentVal + '\n\n' + url });
} else {
this.setState({ text: url });
}
},
(errorMsg) => {
console.error(errorMsg);
},
);
}
}
}
}
onKeyUp(e) {
if ([37, 38, 39, 40].indexOf(e.keyCode) != -1) {
this.checkMention(e);
}
}
onKeyDown(e) {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
this.submit();
}
}
saveDraftToHistory() {
const text = this.state.text;
const currentHistoryState = window.history.state;
const newHistoryState = {
...currentHistoryState,
};
newHistoryState['replyTo' + this.props.replyingTo] = text;
window.history.replaceState(newHistoryState, '');
}
onMsgTextInput(event) {
this.setTextareaHeight(event.target);
if (!this.props.replyingTo) {
localState.get('channels').get('public').get('msgDraft').put($(event.target).val());
}
this.checkMention(event);
this.saveDraftToHistory();
this.setState({ text: $(event.target).val() });
}
attachFileClicked(event) {
event.stopPropagation();
event.preventDefault();
$(this.base).find('.attachment-input').click();
}
handleFileAttachments(files) {
if (files) {
for (let i = 0; i < files.length; i++) {
const formData = new FormData();
formData.append('fileToUpload', files[i]);
const a = this.state.attachments || [];
a[i] = a[i] || {
type: files[i].type,
};
Helpers.getBase64(files[i]).then((base64) => {
a[i].data = base64;
this.setState({ attachments: a });
});
fetch('https://nostr.build/api/upload/iris.php', {
method: 'POST',
body: formData,
})
.then(async (response) => {
const url = await response.json();
console.log('upload response', url);
if (url) {
a[i].url = url;
this.setState({ attachments: a });
const currentVal = this.state.text;
if (currentVal) {
this.setState({ text: currentVal + '\n\n' + url });
} else {
this.setState({ text: url });
}
}
})
.catch((error) => {
console.error('upload error', error);
a[i].error = 'upload failed';
this.setState({ attachments: a });
});
}
$(this.base).find('textarea').focus();
}
}
attachmentsChanged(event) {
// TODO use Upload btn
event.preventDefault();
const files = event.target.files || event.dataTransfer.files;
this.handleFileAttachments(files);
}
onSelectMention(item) {
const textarea = $(this.base).find('textarea').get(0);
const pos = textarea.selectionStart;
const join = [
textarea.value.slice(0, pos).replace(mentionRegex, 'nostr:'),
item.key,
textarea.value.slice(pos),
].join('');
textarea.value = `${join} `;
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = pos + item.key.length;
}
render() {
const textareaPlaceholder =
this.props.placeholder ||
(this.props.index === 'media' ? 'type_a_message_or_paste_a_magnet_link' : 'type_a_message');
return (
<form
autoComplete="off"
className={`message-form ${this.props.class || ''} public`}
onSubmit={(e) => this.onMsgFormSubmit(e)}
>
<FileAttachment onFilesChanged={(files) => this.handleFileAttachments(files)} />
<TextArea
onMsgTextPaste={(e) => this.onMsgTextPaste(e)}
onKeyUp={(e) => this.onKeyUp(e)}
onKeyDown={(e) => this.onKeyDown(e)}
onMsgTextInput={(e) => this.onMsgTextInput(e)}
attachmentsChanged={(e) => this.attachmentsChanged(e)}
placeholder={textareaPlaceholder}
value={this.state.text}
/>
<Show when={this.state.mentioning}>
<SearchBox
resultsOnly
query={this.state.mentioning}
onSelect={(item) => this.onSelectMention(item)}
/>
</Show>
<Show when={!(this.props.waitForFocus && !this.state.focused)}>
<div className="flex items-center justify-between mt-4">
<button
type="button"
className="attach-file-btn btn"
onClick={(e) => this.attachFileClicked(e)}
>
{Icons.attach}
</button>
<button type="submit" className="btn btn-primary">
{t('post')}
</button>
</div>
</Show>
<AttachmentPreview
attachments={this.state.attachments}
torrentId={this.state.torrentId}
removeAttachments={() => this.setState({ attachments: undefined })}
/>
</form>
);
}
checkMention(event: any) {
const val = event.target.value.slice(0, event.target.selectionStart);
const matches = val.match(mentionRegex);
if (matches) {
const match = matches[0].slice(1);
if (!Key.toNostrHexAddress(match)) {
this.setState({ mentioning: match });
}
} else if (this.state.mentioning) {
this.setState({ mentioning: undefined });
}
}
}
export default CreateNoteForm;

View File

@ -0,0 +1,31 @@
import $ from 'jquery';
import { FunctionalComponent } from 'preact';
type Props = {
onFilesChanged: (files: File[]) => void;
};
const FileAttachment: FunctionalComponent<Props> = ({ onFilesChanged }) => {
const attachmentsChanged = (event) => {
const files = event.target.files || event.dataTransfer.files;
if (files) {
onFilesChanged(Array.from(files));
}
$(event.target).val(null);
// If you want to focus the main textarea, you'd need to lift the ref to the parent component
// or pass a ref/callback to set the focus.
};
return (
<input
name="attachment-input"
type="file"
className="hidden attachment-input"
accept="image/*, video/*, audio/*"
multiple
onChange={attachmentsChanged}
/>
);
};
export default FileAttachment;

View File

@ -0,0 +1,43 @@
import { translate as t } from '@/translations/Translation.mjs';
interface TextAreaProps {
onMsgTextPaste: (event: any) => void;
onKeyUp: (e: any) => void;
onKeyDown: (e: any) => void;
onMsgTextInput: (event: any) => void;
attachmentsChanged: (e: any) => void;
placeholder: string;
value: string;
}
const TextArea = ({
onMsgTextPaste,
onKeyUp,
onKeyDown,
onMsgTextInput,
attachmentsChanged,
placeholder,
value,
}: TextAreaProps) => (
<textarea
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onDrop={attachmentsChanged}
onKeyUp={onKeyUp}
onKeyDown={onKeyDown}
onPaste={onMsgTextPaste}
onInput={onMsgTextInput}
className="p-2 mt-1 w-full h-12 bg-black focus:ring-blue-500 focus:border-blue-500 block w-full text-lg border-gray-700 rounded-md text-white"
type="text"
placeholder={t(placeholder)}
autoComplete="off"
autoCorrect="off"
autoCapitalize="sentences"
spellCheck={false}
value={value}
/>
);
export default TextArea;

View File

@ -0,0 +1,18 @@
export interface IProps {
replyingTo?: string;
forceAutofocusMobile?: boolean;
autofocus?: boolean;
onSubmit?: (msg: any) => void;
waitForFocus?: boolean;
class?: string;
index?: string;
placeholder?: string;
}
export interface IState {
attachments?: any[];
torrentId?: string;
mentioning?: string;
focused?: boolean;
text: string;
}

View File

@ -0,0 +1,79 @@
import { Event } from 'nostr-tools';
import Helpers from '@/Helpers';
import Events from '../../nostr/Events.js';
import Key from '../../nostr/Key.js';
function handleTags(tags = [] as any[], text) {
function handleTagged(regex, tagType) {
const taggedItems = [...text.matchAll(regex)]
.map((m) => m[0])
.filter((m, i, a) => a.indexOf(m) === i);
if (taggedItems) {
for (const tag of taggedItems) {
const match = tag.match(/npub[a-zA-Z0-9]{59,60}/)?.[0];
const hexTag = match && Key.toNostrHexAddress(match);
if (!hexTag) {
continue;
}
const newTag = [tagType, hexTag, '', 'mention'];
// add if not already present
if (!tags?.find((t) => t[0] === newTag[0] && t[1] === newTag[1])) {
tags.push(newTag);
}
}
}
}
handleTagged(Helpers.pubKeyRegex, 'p');
handleTagged(Helpers.noteRegex, 'e');
const hashtags = [...text.matchAll(Helpers.hashtagRegex)].map((m) => m[0].slice(1));
if (hashtags.length) {
for (const hashtag of hashtags) {
if (!tags?.find((t) => t[0] === 't' && t[1] === hashtag)) {
tags.push(['t', hashtag]);
}
}
}
return tags;
}
export async function sendNostr(msg: { text: string; replyingTo?: string }) {
const event = {
kind: 1,
content: msg.text,
} as any;
if (msg.replyingTo) {
const id = Key.toNostrHexAddress(msg.replyingTo);
if (!id) {
throw new Error('invalid replyingTo');
}
const replyingTo: Event = await new Promise((resolve) => {
Events.getEventById(id, true, (e) => resolve(e));
});
event.tags = replyingTo.tags.filter((tag) => tag[0] === 'p');
let rootTag = replyingTo.tags?.find((t) => t[0] === 'e' && t[3] === 'root');
if (!rootTag) {
rootTag = replyingTo.tags?.find((t) => t[0] === 'e');
}
if (rootTag) {
event.tags.unshift(['e', id, '', 'reply']);
event.tags.unshift(['e', rootTag[1], '', 'root']);
} else {
event.tags.unshift(['e', id, '', 'root']);
}
if (!event.tags?.find((t) => t[0] === 'p' && t[1] === replyingTo.pubkey)) {
event.tags.push(['p', replyingTo.pubkey]);
}
}
event.tags = handleTags(event.tags, msg.text);
console.log('sending event', event);
return Events.publish(event);
}

View File

@ -1,15 +1,15 @@
import { memo } from 'react';
import { useEffect, useState } from 'preact/hooks';
import Helpers from '../../../Helpers';
import localState from '../../../LocalState';
import SocialNetwork from '../../../nostr/SocialNetwork';
import { translate as t } from '../../../translations/Translation.mjs';
import Show from '../../helpers/Show';
import HyperText from '../../HyperText';
import PublicMessageForm from '../../PublicMessageForm';
import Torrent from '../../Torrent';
import Reactions from '../buttons/ReactionButtons';
import CreateNoteForm from '@/components/create/CreateNoteForm';
import Reactions from '@/components/events/buttons/ReactionButtons';
import Show from '@/components/helpers/Show';
import HyperText from '@/components/HyperText';
import Torrent from '@/components/Torrent';
import Helpers from '@/Helpers';
import localState from '@/LocalState';
import SocialNetwork from '@/nostr/SocialNetwork';
import { translate as t } from '@/translations/Translation.mjs';
import Author from './Author';
import Helmet from './Helmet';
@ -134,7 +134,7 @@ const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, meta })
</Show>
<Show when={standalone}>
<hr className="-mx-2 opacity-10 my-2" />
<PublicMessageForm
<CreateNoteForm
waitForFocus={true}
autofocus={!standalone}
replyingTo={event.id}

View File

@ -258,6 +258,7 @@ export default {
getFollowedByUser: function (
user: string,
cb?: (followedUsers: Set<string>) => void,
includeSelf = false,
): Unsubscribe {
const userId = ID(user);
const callback = () => {
@ -266,10 +267,15 @@ export default {
for (const id of this.followedByUser.get(userId) || []) {
set.add(PUB(id));
}
if (includeSelf) {
set.add(user);
}
cb(set);
}
};
this.followedByUser.has(userId) && callback();
if (this.followedByUser.has(userId) || includeSelf) {
callback();
}
return PubSub.subscribe({ kinds: [3], authors: [user] }, callback);
},
getFollowersByUser: function (

View File

@ -1,7 +1,7 @@
import { route } from 'preact-router';
import CreateNoteForm from '../components/create/CreateNoteForm';
import EventComponent from '../components/events/EventComponent';
import PublicMessageForm from '../components/PublicMessageForm';
import Key from '../nostr/Key';
import { translate as t } from '../translations/Translation.mjs';
@ -33,7 +33,7 @@ class Note extends View {
if (this.props.id === 'new') {
content = (
<div className="m-2">
<PublicMessageForm
<CreateNoteForm
placeholder={t('whats_on_your_mind')}
forceAutofocusMobile={true}
autofocus={true}

View File

@ -1,6 +1,6 @@
import CreateNoteForm from '@/components/create/CreateNoteForm';
import FeedComponent from '@/components/feed/Feed';
import OnboardingNotification from '@/components/OnboardingNotification';
import PublicMessageForm from '@/components/PublicMessageForm';
import Events from '@/nostr/Events';
import { translate as t } from '@/translations/Translation.mjs';
@ -24,7 +24,7 @@ class Feed extends View {
<div className="flex flex-col w-full">
<OnboardingNotification />
<div className="hidden md:block px-4">
<PublicMessageForm autofocus={false} placeholder={t('whats_on_your_mind')} />
<CreateNoteForm autofocus={false} placeholder={t('whats_on_your_mind')} />
</div>
<FeedComponent
filterOptions={[
@ -36,7 +36,7 @@ class Feed extends View {
},
{
name: t('posts_and_replies'),
filter: { kinds: [1], authors: this.state.followedUsers },
filter: { kinds: [1] },
eventProps: { showRepliedMsg: true, fullWidth: false },
},
]}

View File

@ -1,7 +1,7 @@
import CreateNoteForm from '@/components/create/CreateNoteForm';
import FeedComponent from '@/components/feed/Feed';
import Show from '@/components/helpers/Show';
import OnboardingNotification from '@/components/OnboardingNotification';
import PublicMessageForm from '@/components/PublicMessageForm';
import Events from '@/nostr/Events';
import Key from '@/nostr/Key';
import { Unsubscribe } from '@/nostr/PubSub';
@ -28,9 +28,14 @@ class Feed extends View {
componentDidMount() {
this.restoreScrollPosition();
this.unsub = SocialNetwork.getFollowedByUser(Key.getPubKey(), (followedUsers) => {
this.setState({ followedUsers: Array.from(followedUsers) });
});
this.unsub = SocialNetwork.getFollowedByUser(
Key.getPubKey(),
(followedUsers) => {
console.log('followedUsers', followedUsers);
this.setState({ followedUsers: Array.from(followedUsers) });
},
true,
);
}
componentWillUnmount() {
@ -44,7 +49,7 @@ class Feed extends View {
<div className="flex flex-col w-full">
<OnboardingNotification />
<div className="hidden md:block px-4">
<PublicMessageForm autofocus={false} placeholder={t('whats_on_your_mind')} />
<CreateNoteForm autofocus={false} placeholder={t('whats_on_your_mind')} />
</div>
<Show when={this.state.followedUsers.length}>
<FeedComponent