forked from andyburke/autonomous.contact
		
	feature: essays
fix: multiple rendering of things when sending
This commit is contained in:
		
							parent
							
								
									376b7fdc24
								
							
						
					
					
						commit
						b6f661c6ec
					
				
					 10 changed files with 537 additions and 60 deletions
				
			
		|  | @ -93,7 +93,17 @@ export function VALIDATE_EVENT(event: EVENT) { | |||
| 			} | ||||
| 			break; | ||||
| 		case 'essay': | ||||
| 			if (event.data?.essay?.length <= 0) { | ||||
| 			if (event.data?.title?.length <= 0) { | ||||
| 				errors.push({ | ||||
| 					cause: 'essay_title_missing', | ||||
| 					message: 'An essay must have a title.' | ||||
| 				}); | ||||
| 			} else if (event.data?.title?.length > 2 ** 7) { | ||||
| 				errors.push({ | ||||
| 					cause: 'essay_title_length_limit_exceeded', | ||||
| 					message: 'An essay title cannot be longer than 128 characters.' | ||||
| 				}); | ||||
| 			} else if (event.data?.essay?.length <= 0) { | ||||
| 				errors.push({ | ||||
| 					cause: 'essay_missing', | ||||
| 					message: 'An essay cannot be empty.' | ||||
|  |  | |||
|  | @ -19,6 +19,9 @@ const DEFAULT_USER_PERMISSIONS: string[] = [ | |||
| 	'topics.blurbs.write', | ||||
| 	'topics.chat.write', | ||||
| 	'topics.chat.read', | ||||
| 	'topics.essays.create', | ||||
| 	'topics.essays.read', | ||||
| 	'topics.essays.write', | ||||
| 	'topics.posts.create', | ||||
| 	'topics.posts.write', | ||||
| 	'topics.posts.read', | ||||
|  |  | |||
|  | @ -313,6 +313,9 @@ body[data-perms*="topics.blurbs.write"] [data-requires-permission="topics.blurbs | |||
| body[data-perms*="topics.chat.create"] [data-requires-permission="topics.chat.create"], | ||||
| body[data-perms*="topics.chat.read"] [data-requires-permission="topics.chat.read"], | ||||
| body[data-perms*="topics.chat.write"] [data-requires-permission="topics.chat.write"], | ||||
| body[data-perms*="topics.essays.create"] [data-requires-permission="topics.essays.create"], | ||||
| body[data-perms*="topics.essays.read"] [data-requires-permission="topics.essays.read"], | ||||
| body[data-perms*="topics.essays.write"] [data-requires-permission="topics.essays.write"], | ||||
| body[data-perms*="topics.posts.create"] [data-requires-permission="topics.posts.create"], | ||||
| body[data-perms*="topics.posts.read"] [data-requires-permission="topics.posts.read"], | ||||
| body[data-perms*="topics.posts.write"] [data-requires-permission="topics.posts.write"], | ||||
|  |  | |||
|  | @ -24,6 +24,8 @@ | |||
| 		<script src="./js/embeds/vimeo.js" type="text/javascript"></script> | ||||
| 		<script src="./js/embeds/youtube.js" type="text/javascript"></script> | ||||
| 
 | ||||
| 		<script src="./js/external/md_to_html.js" type="text/javascript"></script> | ||||
| 
 | ||||
| 		<script src="./js/htmlify.js" type="text/javascript"></script> | ||||
| 
 | ||||
| 		<script src="./js/locationchange.js" type="text/javascript"></script> | ||||
|  |  | |||
							
								
								
									
										86
									
								
								public/js/external/md_to_html.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								public/js/external/md_to_html.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | |||
| // see: https://andyburke.dev/andyburke/serverus/src/branch/dev/handlers/markdown.ts
 | ||||
| 
 | ||||
| /* MARKDOWN TO HTML */ | ||||
| 
 | ||||
| /* order of these transforms matters, so we list them here in an array and build type from it after. */ | ||||
| const MD_TRANSFORM_NAMES = [ | ||||
| 	"characters", | ||||
| 	"headings", | ||||
| 	"horizontal_rules", | ||||
| 	"list_items", | ||||
| 	"bold", | ||||
| 	"italic", | ||||
| 	"strikethrough", | ||||
| 	"code", | ||||
| 	"images", | ||||
| 	"links", | ||||
| 	"breaks", | ||||
| ]; | ||||
| const MD_TRANSFORMS = { | ||||
| 	characters: [ | ||||
| 		[/&/g, "&"], | ||||
| 		[/</g, "<"], | ||||
| 		[/>/g, ">"], | ||||
| 		[/"/g, """], | ||||
| 		[/'/g, "'"], | ||||
| 	], | ||||
| 
 | ||||
| 	headings: [ | ||||
| 		[/^#\s(.+)$/gm, "<h1>$1</h1>\n"], | ||||
| 		[/^##\s(.+)$/gm, "<h2>$1</h2>\n"], | ||||
| 		[/^###\s(.+)$/gm, "<h3>$1</h3>\n"], | ||||
| 		[/^####\s(.+)$/gm, "<h4>$1</h4>\n"], | ||||
| 		[/^#####\s(.+)$/gm, "<h5>$1</h5>\n"], | ||||
| 	], | ||||
| 
 | ||||
| 	horizontal_rules: [[/^----*$/gm, "<hr>\n"]], | ||||
| 
 | ||||
| 	list_items: [ | ||||
| 		[/\n\n([ \t]*)([-\*\.].*?)\n\n/gs, "\n\n<ul>\n$1$2\n</ul>\n\n\n"], | ||||
| 		[/^([ \t]*)[-\*\.](\s+.*)$/gm, "<li>$1$2</li>\n"], | ||||
| 	], | ||||
| 
 | ||||
| 	bold: [[/\*([^\*]+)\*/gm, "<strong>$1</strong>"]], | ||||
| 
 | ||||
| 	italic: [[/_([^_]+)_/gm, "<i>$1</i>"]], | ||||
| 
 | ||||
| 	strikethrough: [[/~([^~]+)~/gm, "<s>$1</s>"]], | ||||
| 
 | ||||
| 	code: [ | ||||
| 		[/```\n([^`]+)\n```/gm, "<pre><code>$1</code></pre>"], | ||||
| 		[/```([^`]+)```/gm, "<code>$1</code>"], | ||||
| 	], | ||||
| 
 | ||||
| 	images: [[/!\[([^\]]+)\]\(([^\)]+)\)/g, '<img src="$2" alt="$1">']], | ||||
| 
 | ||||
| 	links: [[/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2">$1</a>']], | ||||
| 
 | ||||
| 	breaks: [ | ||||
| 		[/\s\s\n/g, "\n<br>\n"], | ||||
| 		[/\n\n/g, "\n<br>\n"], | ||||
| 	], | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Convert markdown to HTML. | ||||
|  * @param markdown The markdown string. | ||||
|  * @param options _(Optional)_ A record of transforms to disable. | ||||
|  * @returns The generated HTML string. | ||||
|  */ | ||||
| function md_to_html(markdown, transform_config) { | ||||
| 	let html = markdown; | ||||
| 	for (const transform_name of MD_TRANSFORM_NAMES) { | ||||
| 		const enabled = | ||||
| 			typeof transform_config === "undefined" || transform_config[transform_name] !== false; | ||||
| 		if (!enabled) { | ||||
| 			continue; | ||||
| 		} | ||||
| 
 | ||||
| 		const transforms = MD_TRANSFORMS[transform_name] ?? []; | ||||
| 		for (const markdown_transformer of transforms) { | ||||
| 			html = html.replace(...markdown_transformer); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return `<div class="html-from-markdown">${html}</div>`; | ||||
| } | ||||
|  | @ -148,14 +148,15 @@ | |||
| 			<script> | ||||
| 				const blurbs_list = document.getElementById("blurbs-list"); | ||||
| 
 | ||||
| 				async function render_blurb(blurb) { | ||||
| 				async function render_blurb(blurb, position = "afterbegin") { | ||||
| 					const creator = await USERS.get(blurb.creator_id); | ||||
| 					const existing_element = | ||||
| 						document.getElementById(blurb.id) ?? | ||||
| 						document.getElementById(blurb.meta?.temp_id ?? ""); | ||||
| 						document.getElementById(blurb.meta?.temp_id ?? "") ?? | ||||
| 						document.querySelector(`[data-temp_id="${blurb.meta?.temp_id ?? ""}"]`); | ||||
| 					const blurb_datetime = datetime_to_local(blurb.timestamps.created); | ||||
| 
 | ||||
| 					const html_content = `<div class="blurb-container" data-creator_id="${creator.id}" data-blurb_id="${blurb.id}"> | ||||
| 					const html_content = `<div class="blurb-container" data-creator_id="${creator.id}" data-blurb_id="${blurb.id}" data-temp_id="${blurb.meta?.temp_id ?? ""}"> | ||||
| 						<div class="media-preview-container"> | ||||
| 							${blurb.data?.media?.length ? blurb.data.media.map((url) => `<img src="${url}" />`).join("\n") : ""} | ||||
| 						</div> | ||||
|  | @ -187,26 +188,7 @@ | |||
| 							document.querySelector( | ||||
| 								`.blurb-container[data-blurb_id='${blurb.parent_id}'] > .replies-container`, | ||||
| 							) ?? blurbs_list; | ||||
| 						target_container.insertAdjacentHTML("beforeend", html_content); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				async function append_blurbs(blurbs) { | ||||
| 					let last_blurb_id = blurbs_list.dataset.last_blurb_id ?? ""; | ||||
| 					for await (const blurb of blurbs) { | ||||
| 						// if the last blurb is undefined, it becomes this event, otherwise, if this event's id is newer, | ||||
| 						// it becomes the latest blurb | ||||
| 						last_blurb_id = | ||||
| 							blurb.id > last_blurb_id && blurb.id.indexOf("TEMP") !== 0 | ||||
| 								? blurb.id | ||||
| 								: last_blurb_id; | ||||
| 
 | ||||
| 						// if the last blurb has been updated, update the content's dataset to reflect that | ||||
| 						if (last_blurb_id !== blurbs_list.dataset.last_blurb_id) { | ||||
| 							blurbs_list.dataset.last_blurb_id = last_blurb_id; | ||||
| 						} | ||||
| 
 | ||||
| 						await render_blurb(blurb); | ||||
| 						target_container.insertAdjacentHTML(position, html_content); | ||||
| 					} | ||||
| 
 | ||||
| 					const new_blurb_forms = document.querySelectorAll(".blurb-creation-form"); | ||||
|  | @ -241,7 +223,22 @@ | |||
| 					}) | ||||
| 						.then(async (new_events_response) => { | ||||
| 							const new_events = (await new_events_response.json()) ?? []; | ||||
| 							await append_blurbs(new_events.reverse()); | ||||
| 
 | ||||
| 							for await (const blurb of new_events.reverse()) { | ||||
| 								await render_blurb(blurb); | ||||
| 							} | ||||
| 
 | ||||
| 							const last_blurb_id = new_events.reduce((_last_blurb_id, blurb) => { | ||||
| 								return blurb.id > _last_blurb_id && blurb.id.indexOf("TEMP") < 0 | ||||
| 									? blurb.id | ||||
| 									: _last_blurb_id; | ||||
| 							}, blurb_list.dataset.last_blurb_id ?? ""); | ||||
| 
 | ||||
| 							// if the last blurb has been updated, update the content's dataset to reflect that | ||||
| 							if (last_blurb_id !== blurbs_list.dataset.last_blurb_id) { | ||||
| 								blurbs_list.dataset.last_blurb_id = last_blurb_id; | ||||
| 							} | ||||
| 
 | ||||
| 							poll_for_new_blurbs(topic_id); | ||||
| 						}) | ||||
| 						.catch((error) => { | ||||
|  | @ -289,7 +286,10 @@ | |||
| 
 | ||||
| 					const events = await events_response.json(); | ||||
| 
 | ||||
| 					await append_blurbs(events.reverse()); | ||||
| 					for await (const blurb of events.reverse()) { | ||||
| 						await render_blurb(blurb, "afterbegin"); | ||||
| 					} | ||||
| 
 | ||||
| 					poll_for_new_blurbs(); | ||||
| 				} | ||||
| 				document.addEventListener("topic_changed", load_active_topic_for_blurbs); | ||||
|  |  | |||
|  | @ -5,12 +5,13 @@ | |||
| 
 | ||||
| 	let last_creator_id = null; | ||||
| 	let user_tick_tock_class = "user-tock"; | ||||
| 	async function render_chat_event(event) { | ||||
| 	async function render_chat_event(event, position = "beforeend") { | ||||
| 		const creator = await USERS.get(event.creator_id); | ||||
| 
 | ||||
| 		const existing_element = | ||||
| 			document.getElementById(event.id) ?? | ||||
| 			(event.meta?.temp_id ? document.getElementById(event.meta?.temp_id ?? "") : undefined); | ||||
| 			document.getElementById(event.meta?.temp_id ?? "") ?? | ||||
| 			document.querySelector(`[data-temp_id="${event.meta?.temp_id ?? ""}" ]`); | ||||
| 
 | ||||
| 		const event_datetime = datetime_to_local(event.timestamps.created); | ||||
| 
 | ||||
|  | @ -24,7 +25,7 @@ | |||
| 			last_creator_id = creator.id; | ||||
| 		} | ||||
| 
 | ||||
| 		const html_content = `<div id="${event.id}" class="message-container ${user_tick_tock_class} ${time_tick_tock_class}" data-creator_id="${creator.id}"> | ||||
| 		const html_content = `<div id="${event.id}" class="message-container ${user_tick_tock_class} ${time_tick_tock_class}" data-creator_id="${creator.id}" data-temp_id="${event.meta?.temp_id ?? ""}"> | ||||
| 	<div class="message-actions-container"> | ||||
| 		<input | ||||
| 			type="checkbox" | ||||
|  | @ -64,35 +65,31 @@ | |||
| 			// TODO: threading | ||||
| 			document | ||||
| 				.getElementById("topic-chat-content") | ||||
| 				?.insertAdjacentHTML("beforeend", html_content); | ||||
| 				?.insertAdjacentHTML(position, html_content); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	async function append_chat_events(events) { | ||||
| 	async function handle_chat_events(events) { | ||||
| 		const topic_chat_content = document.getElementById("topic-chat-content"); | ||||
| 		let last_message_id = topic_chat_content.dataset.last_message_id ?? ""; | ||||
| 		for await (const event of events) { | ||||
| 			// if the last message is undefined, it becomes this event, otherwise, if this event's id is newer, | ||||
| 			// it becomes the latest message | ||||
| 			last_message_id = | ||||
| 				event.id > last_message_id && event.id.indexOf("TEMP") !== 0 | ||||
| 					? event.id | ||||
| 					: last_message_id; | ||||
| 
 | ||||
| 			// if the last message has been updated, update the content's dataset to reflect that | ||||
| 			if (last_message_id !== topic_chat_content.dataset.last_message_id) { | ||||
| 				topic_chat_content.dataset.last_message_id = last_message_id; | ||||
| 			} | ||||
| 
 | ||||
| 		const sorted = events.sort((lhs, rhs) => lhs.id.localeCompare(rhs.id)); | ||||
| 		for await (const event of sorted) { | ||||
| 			await render_chat_event(event); | ||||
| 		} | ||||
| 
 | ||||
| 		const last_message_id = sorted.reduce((_last_message_id, event) => { | ||||
| 			return event.id > _last_message_id && event.id.indexOf("TEMP") < 0 | ||||
| 				? event.id | ||||
| 				: _last_message_id; | ||||
| 		}, topic_chat_content.dataset.last_message_id ?? ""); | ||||
| 
 | ||||
| 		// if the last blurb has been updated, update the content's dataset to reflect that | ||||
| 		if (last_message_id !== topic_chat_content.dataset.last_message_id) { | ||||
| 			topic_chat_content.dataset.last_message_id = last_message_id; | ||||
| 		} | ||||
| 
 | ||||
| 		setTimeout(() => { | ||||
| 			topic_chat_content.scrollTop = topic_chat_content.scrollHeight; | ||||
| 			console.dir({ | ||||
| 				scrollHeight: topic_chat_content.scrollHeight, | ||||
| 				scrollTop: topic_chat_content.scrollTop, | ||||
| 			}); | ||||
| 		}, 50); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -118,8 +115,8 @@ | |||
| 			signal: topic_polling_request_abort_controller.signal, | ||||
| 		}) | ||||
| 			.then(async (new_events_response) => { | ||||
| 				const new_events = ((await new_events_response.json()) ?? []).reverse(); | ||||
| 				await append_chat_events(new_events); | ||||
| 				const new_events = await new_events_response.json(); | ||||
| 				handle_chat_events(new_events); | ||||
| 				poll_for_new_chat_events(topic_id); | ||||
| 			}) | ||||
| 			.catch((error) => { | ||||
|  | @ -163,9 +160,8 @@ | |||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		const events = (await events_response.json()).reverse(); | ||||
| 
 | ||||
| 		await append_chat_events(events); | ||||
| 		const events = await events_response.json(); | ||||
| 		handle_chat_events(events); | ||||
| 		poll_for_new_chat_events(); | ||||
| 	} | ||||
| 	document.addEventListener("topic_changed", load_active_topic_for_chat); | ||||
|  | @ -204,7 +200,7 @@ | |||
| 						width: 100%; | ||||
| 						transition: all 0.5s; | ||||
| 					" | ||||
| 					on_reply="async (event) => { await append_chat_events([event]); document.getElementById( 'chat-input' )?.focus(); }" | ||||
| 					on_reply="async (event) => { await render_chat_event(event); document.getElementById(event.id)?.classList.remove('sending'); document.getElementById( 'chat-input' )?.focus(); }" | ||||
| 					on_parsed="async (event) => { await render_chat_event(event); document.getElementById(event.id)?.classList.add('sending'); }" | ||||
| 				> | ||||
| 					<input type="hidden" name="type" value="chat" /> | ||||
|  |  | |||
|  | @ -1,16 +1,275 @@ | |||
| <style> | ||||
| 	#essays-container { | ||||
| 		padding: 1rem; | ||||
| 		max-width: 60rem; | ||||
| 		margin: 0 auto; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container { | ||||
| 		position: relative; | ||||
| 		display: grid; | ||||
| 		grid-template-rows: auto auto auto auto 1fr; | ||||
| 		grid-template-columns: 1fr; | ||||
| 		grid-template-areas: | ||||
| 			"preview" | ||||
| 			"title" | ||||
| 			"info" | ||||
| 			"content" | ||||
| 			"newessay" | ||||
| 			"replies"; | ||||
| 		padding: 1rem; | ||||
| 		border: 1px solid var(--border-subtle); | ||||
| 		overflow-y: hidden; | ||||
| 		transition: all ease-in-out 0.33s; | ||||
| 		margin-bottom: 2rem; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .media-preview-container { | ||||
| 		grid-area: preview; | ||||
| 		max-height: 600px; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .media-preview-container img { | ||||
| 		max-width: 100%; | ||||
| 		max-height: 100%; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .info-container { | ||||
| 		grid-area: info; | ||||
| 		display: flex; | ||||
| 		margin-left: 1.8rem; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .info-container .avatar-container { | ||||
| 		display: block; | ||||
| 		margin: 0.5rem; | ||||
| 		width: 3rem; | ||||
| 		height: 3rem; | ||||
| 		border-radius: 20%; | ||||
| 		overflow: hidden; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .info-container .avatar-container img { | ||||
| 		min-width: 100%; | ||||
| 		min-height: 100%; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .info-container .username-container { | ||||
| 		display: block; | ||||
| 		margin: 0 0.5rem; | ||||
| 		font-weight: bold; | ||||
| 		align-content: center; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .info-container .datetime-container { | ||||
| 		display: block; | ||||
| 		margin: 0 0.5rem; | ||||
| 		align-content: center; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .info-container .datetime-container .long { | ||||
| 		font-size: x-small; | ||||
| 		text-transform: uppercase; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .info-container .datetime-container .short { | ||||
| 		font-size: xx-small; | ||||
| 		visibility: hidden; | ||||
| 		display: none; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .title-container { | ||||
| 		grid-area: title; | ||||
| 		padding-left: 1rem; | ||||
| 		margin: 1rem 0; | ||||
| 		font-size: xx-large; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .content-container { | ||||
| 		grid-area: content; | ||||
| 		padding-left: 0.25rem; | ||||
| 		margin: 1rem 0; | ||||
| 		font-size: large; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .new-essay-container { | ||||
| 		grid-area: newessay; | ||||
| 		display: none; | ||||
| 	} | ||||
| 
 | ||||
| 	.essay-container .replies-container { | ||||
| 		grid-area: replies; | ||||
| 	} | ||||
| </style> | ||||
| 
 | ||||
| <div id="essays" class="tab"> | ||||
| 	<input | ||||
| 		type="radio" | ||||
| 		name="top-level-tabs" | ||||
| 		id="essays-tab-input" | ||||
| 		id="essay-tab-input" | ||||
| 		class="tab-switch" | ||||
| 		data-view="essays" | ||||
| 		data-view="essay" | ||||
| 	/> | ||||
| 	<label for="essays-tab-input" class="tab-label mockup" | ||||
| 	<label for="essay-tab-input" class="tab-label" | ||||
| 		><div class="icon essay"></div> | ||||
| 		<div class="label">Essays</div></label | ||||
| 	> | ||||
| 		<div class="label">Essays</div> | ||||
| 	</label> | ||||
| 	<div class="tab-content"> | ||||
| 		<!-- #include file="./README.md" --> | ||||
| 		<div id="essays-container" class="container"> | ||||
| 			<!-- #include file="./README.md" --> | ||||
| 			<!-- #include file="./new_essay.html" --> | ||||
| 
 | ||||
| 			<div id="essays-list"></div> | ||||
| 			<script> | ||||
| 				const essays_list = document.getElementById("essays-list"); | ||||
| 
 | ||||
| 				async function render_essay(essay, position = "beforeend") { | ||||
| 					const creator = await USERS.get(essay.creator_id); | ||||
| 					const existing_element = | ||||
| 						document.getElementById(essay.id) ?? | ||||
| 						document.getElementById(essay.meta?.temp_id ?? "") ?? | ||||
| 						document.querySelector(`[data-temp_id="${essay.meta?.temp_id ?? ""}"]`); | ||||
| 					const essay_datetime = datetime_to_local(essay.timestamps.created); | ||||
| 
 | ||||
| 					const html_content = `<div class="essay-container" data-creator_id="${creator.id}" data-essay_id="${essay.id}" data-temp_id="${essay.meta?.temp_id ?? ""}"> | ||||
| 						<div class="media-preview-container"> | ||||
| 							${essay.data?.media?.length ? essay.data.media.map((url) => `<img src="${url}" />`).join("\n") : ""} | ||||
| 						</div> | ||||
| 
 | ||||
| 						<div class="info-container"> | ||||
| 							<div class="avatar-container"> | ||||
| 								<img src="${creator.meta?.avatar ?? "/images/default_avatar.gif"}" alt="user avatar" /> | ||||
| 							</div> | ||||
| 							<div class="username-container"> | ||||
| 								<span class="username">${creator.username}</span> | ||||
| 							</div> | ||||
| 							<div class="datetime-container"> | ||||
| 								<span class="long">${essay_datetime.long}</span> | ||||
| 								<span class="short">${essay_datetime.short}</span> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						<div class="title-container">${essay.data.title}</div> | ||||
| 						<div class="content-container">${htmlify(md_to_html(essay.data.essay))}</div> | ||||
| 					</div>`; | ||||
| 
 | ||||
| 					if (existing_element) { | ||||
| 						const template = document.createElement("template"); | ||||
| 						template.innerHTML = html_content; | ||||
| 						existing_element.replaceWith(template.content.firstChild); | ||||
| 						existing_element.classList.remove("sending"); | ||||
| 					} else { | ||||
| 						const target_container = | ||||
| 							document.querySelector( | ||||
| 								`.essay-container[data-essay_id='${essay.parent_id}'] > .replies-container`, | ||||
| 							) ?? essays_list; | ||||
| 						target_container.insertAdjacentHTML(position, html_content); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				async function append_essays(essays) { | ||||
| 					let last_essay_id = essays_list.dataset.last_essay_id ?? ""; | ||||
| 					for await (const essay of essays) { | ||||
| 						// if the last essay is undefined, it becomes this event, otherwise, if this event's id is newer, | ||||
| 						// it becomes the latest essay | ||||
| 						last_essay_id = | ||||
| 							essay.id > last_essay_id && essay.id.indexOf("TEMP") !== 0 | ||||
| 								? essay.id | ||||
| 								: last_essay_id; | ||||
| 
 | ||||
| 						// if the last essay has been updated, update the content's dataset to reflect that | ||||
| 						if (last_essay_id !== essays_list.dataset.last_essay_id) { | ||||
| 							essays_list.dataset.last_essay_id = last_essay_id; | ||||
| 						} | ||||
| 
 | ||||
| 						await render_essay(essay); | ||||
| 					} | ||||
| 
 | ||||
| 					const new_essay_forms = document.querySelectorAll(".essay-creation-form"); | ||||
| 					for (const new_essay_form of new_essay_forms) { | ||||
| 						new_essay_form.action = | ||||
| 							"/api/topics/" + document.body.dataset?.topic + "/events"; | ||||
| 					} | ||||
| 
 | ||||
| 					essays_list.scrollTop = 0; | ||||
| 				} | ||||
| 
 | ||||
| 				// TODO: we need some abortcontroller handling here or something | ||||
| 				// similar for when we change topics, this is the most basic | ||||
| 				// first pass outline | ||||
| 				let essay_polling_abort_controller = null; | ||||
| 				async function poll_for_new_essays() { | ||||
| 					const essays_list = document.getElementById("essays-list"); | ||||
| 					const topic_id = document.body.dataset.topic; | ||||
| 					const last_essay_id = essays_list.dataset.last_essay_id; | ||||
| 
 | ||||
| 					if (!topic_id) { | ||||
| 						return; | ||||
| 					} | ||||
| 
 | ||||
| 					const message_polling_url = `/api/topics/${topic_id}/events?type=essay&limit=100&sort=newest&wait=true${last_essay_id ? `&after_id=${last_essay_id}` : ""}`; | ||||
| 
 | ||||
| 					essay_polling_abort_controller = | ||||
| 						essay_polling_abort_controller || new AbortController(); | ||||
| 
 | ||||
| 					api.fetch(message_polling_url, { | ||||
| 						signal: essay_polling_abort_controller.signal, | ||||
| 					}) | ||||
| 						.then(async (new_events_response) => { | ||||
| 							const new_events = (await new_events_response.json()) ?? []; | ||||
| 							await append_essays(new_events.reverse(), "afterbegin"); | ||||
| 							poll_for_new_essays(topic_id); | ||||
| 						}) | ||||
| 						.catch((error) => { | ||||
| 							// TODO: poll again? back off? | ||||
| 							console.error(error); | ||||
| 						}); | ||||
| 				} | ||||
| 
 | ||||
| 				async function load_active_topic_for_essays() { | ||||
| 					const topic_id = document.body.dataset.topic; | ||||
| 					if (!topic_id) return; | ||||
| 
 | ||||
| 					const user = document.body.dataset.user | ||||
| 						? JSON.parse(document.body.dataset.user) | ||||
| 						: null; | ||||
| 					if (!user) return; | ||||
| 
 | ||||
| 					const essays_list = document.getElementById("essays-list"); | ||||
| 
 | ||||
| 					if (essay_polling_abort_controller) { | ||||
| 						essay_polling_abort_controller.abort(); | ||||
| 						essay_polling_abort_controller = null; | ||||
| 						delete essays_list.dataset.last_essay_id; | ||||
| 					} | ||||
| 
 | ||||
| 					const topic_response = await api.fetch(`/api/topics/${topic_id}`); | ||||
| 					if (!topic_response.ok) { | ||||
| 						const error = await topic_response.json(); | ||||
| 						alert(error.message ?? JSON.stringify(error)); | ||||
| 						return; | ||||
| 					} | ||||
| 
 | ||||
| 					const topic = await topic_response.json(); | ||||
| 
 | ||||
| 					essays_list.innerHTML = ""; | ||||
| 
 | ||||
| 					const events_response = await api.fetch( | ||||
| 						`/api/topics/${topic_id}/events?type=essay&limit=100&sort=newest`, | ||||
| 					); | ||||
| 					if (!events_response.ok) { | ||||
| 						const error = await events_response.json(); | ||||
| 						alert(error.message ?? JSON.stringify(error)); | ||||
| 						return; | ||||
| 					} | ||||
| 
 | ||||
| 					const events = await events_response.json(); | ||||
| 
 | ||||
| 					await append_essays(events); | ||||
| 					poll_for_new_essays(); | ||||
| 				} | ||||
| 				document.addEventListener("topic_changed", load_active_topic_for_essays); | ||||
| 				document.addEventListener("user_logged_in", load_active_topic_for_essays); | ||||
| 			</script> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
|  |  | |||
							
								
								
									
										118
									
								
								public/tabs/essays/new_essay.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								public/tabs/essays/new_essay.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,118 @@ | |||
| <style> | ||||
| 	.new-essay-container input[type="file"] { | ||||
| 		display: none; | ||||
| 		visibility: hidden; | ||||
| 		opacity: 0; | ||||
| 	} | ||||
| 
 | ||||
| 	.new-essay-container .essay-title-limit-counter, | ||||
| 	.new-essay-container .essay-limit-counter { | ||||
| 		font-size: smaller; | ||||
| 		margin-left: 0.5rem; | ||||
| 	} | ||||
| 
 | ||||
| 	.new-essay-container .file-attach-label { | ||||
| 		display: block; | ||||
| 		text-align: right; | ||||
| 		margin-top: -2.5rem; | ||||
| 		padding: 0.5rem; | ||||
| 	} | ||||
| 
 | ||||
| 	.new-essay-container .file-attach-label .icon { | ||||
| 		display: inline-block; | ||||
| 	} | ||||
| </style> | ||||
| <div class="new-essay-container" data-requires-permission="topics.essays.create"> | ||||
| 	<label> | ||||
| 		<input type="checkbox" collapse-toggle /> | ||||
| 		<i class="icon plus" style="display: inline-block; margin-right: 0.5rem"></i> | ||||
| 		<span>New Essay</span> | ||||
| 	</label> | ||||
| 	<form | ||||
| 		data-smart="true" | ||||
| 		method="POST" | ||||
| 		class="essay-creation-form collapsible" | ||||
| 		style=" | ||||
| 			margin-top: 1rem | ||||
| 			width: 100%; | ||||
| 			transition: all 0.5s; | ||||
| 		" | ||||
| 		on_reply="async (essay) => { await render_essay(essay, 'afterbegin'); document.getElementById(essay.id)?.classList.remove('sending'); }" | ||||
| 		on_parsed="async (essay) => { await render_essay(essay, 'afterbegin'); document.getElementById(essay.id)?.classList.add('sending'); }" | ||||
| 	> | ||||
| 		<input type="hidden" name="type" value="essay" /> | ||||
| 
 | ||||
| 		<input | ||||
| 			type="hidden" | ||||
| 			name="id" | ||||
| 			generator="(_input, form) => 'TEMP-' + form.__submitted_at.toISOString()" | ||||
| 			reset-on-submit | ||||
| 		/> | ||||
| 		<input | ||||
| 			type="hidden" | ||||
| 			name="meta.temp_id" | ||||
| 			generator="(_input, form) => 'TEMP-' + form.__submitted_at.toISOString()" | ||||
| 			reset-on-submit | ||||
| 		/> | ||||
| 
 | ||||
| 		<input | ||||
| 			type="hidden" | ||||
| 			name="creator_id" | ||||
| 			generator="() => { return JSON.parse( document.body.dataset.user ?? '{}' ).id; }" | ||||
| 		/> | ||||
| 
 | ||||
| 		<input | ||||
| 			type="hidden" | ||||
| 			name="timestamps.created" | ||||
| 			generator="(_input, form) => form.__submitted_at.toISOString()" | ||||
| 			reset-on-submit | ||||
| 		/> | ||||
| 		<input | ||||
| 			type="hidden" | ||||
| 			name="timestamps.updated" | ||||
| 			generator="(_input, form) => form.__submitted_at.toISOString()" | ||||
| 			reset-on-submit | ||||
| 		/> | ||||
| 
 | ||||
| 		<input | ||||
| 			type="hidden" | ||||
| 			name="parent_id" | ||||
| 			generator="(_input, form) => { const parent_essay = form.closest( '.essay-container' ); return parent_essay?.dataset?.essay_id; }" | ||||
| 		/> | ||||
| 
 | ||||
| 		<textarea | ||||
| 			type="text" | ||||
| 			name="data.title" | ||||
| 			value="" | ||||
| 			maxlength="128" | ||||
| 			rows="2" | ||||
| 			placeholder="Essay title..." | ||||
| 			reset-on-submit | ||||
| 		></textarea> | ||||
| 		<div class="essay-title-limit-counter" data-limit-counter-for="data.title">0 / 128</div> | ||||
| 
 | ||||
| 		<textarea | ||||
| 			type="text" | ||||
| 			name="data.essay" | ||||
| 			value="" | ||||
| 			maxlength="65536" | ||||
| 			rows="16" | ||||
| 			placeholder="		Compose your thoughts here." | ||||
| 			reset-on-submit | ||||
| 		></textarea> | ||||
| 		<div class="essay-limit-counter" data-limit-counter-for="data.essay">0 / 65536</div> | ||||
| 
 | ||||
| 		<label class="file-attach-label"> | ||||
| 			<input | ||||
| 				aria-label="Upload and share file" | ||||
| 				type="file" | ||||
| 				multiple | ||||
| 				data-smartforms-save-to-home="true" | ||||
| 				name="data.media" | ||||
| 				reset-on-submit | ||||
| 			/> | ||||
| 			<div class="icon attachment"></div> | ||||
| 		</label> | ||||
| 		<input type="submit" value="Publish It!" /> | ||||
| 	</form> | ||||
| </div> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue