forked from andyburke/autonomous.contact
		
	refactor: finish UX refactor and move events storage
This commit is contained in:
		
							parent
							
								
									4347d20263
								
							
						
					
					
						commit
						f760156651
					
				
					 10 changed files with 269 additions and 27 deletions
				
			
		|  | @ -37,13 +37,34 @@ type TOPIC_EVENT_CACHE_ENTRY = { | |||
| 	eviction_timeout: number; | ||||
| }; | ||||
| 
 | ||||
| const TOPIC_EVENT_ID_MATCHER = /^(?<event_type>.*):(?<event_id>.*)$/; | ||||
| 
 | ||||
| const TOPIC_EVENTS: Record<string, TOPIC_EVENT_CACHE_ENTRY> = {}; | ||||
| export function get_events_collection_for_topic(topic_id: string): FSDB_COLLECTION<EVENT> { | ||||
| 	TOPIC_EVENTS[topic_id] = TOPIC_EVENTS[topic_id] ?? { | ||||
| 		collection: new FSDB_COLLECTION<EVENT>({ | ||||
| 			name: `topics/${topic_id.slice(0, 14)}/${topic_id.slice(0, 34)}/${topic_id}/events`, | ||||
| 			id_field: 'id', | ||||
| 			organize: by_lurid, | ||||
| 			organize: (id) => { | ||||
| 				TOPIC_EVENT_ID_MATCHER.lastIndex = 0; | ||||
| 
 | ||||
| 				const { | ||||
| 					groups: { | ||||
| 						event_type, | ||||
| 						event_id | ||||
| 					} | ||||
| 				} = TOPIC_EVENT_ID_MATCHER.exec(id ?? '') ?? { | ||||
| 					groups: {} | ||||
| 				}; | ||||
| 
 | ||||
| 				return [ | ||||
| 					event_type, | ||||
| 					event_id.slice(0, 14), | ||||
| 					event_id.slice(0, 34), | ||||
| 					event_id, | ||||
| 					`${event_id}.json` | ||||
| 				]; | ||||
| 			}, | ||||
| 			indexers: { | ||||
| 				creator_id: new FSDB_INDEXER_SYMLINKS<EVENT>({ | ||||
| 					name: 'creator_id', | ||||
|  |  | |||
|  | @ -56,7 +56,12 @@ export async function GET(request: Request, meta: Record<string, any>): Promise< | |||
| 		limit: Math.min(parseInt(meta.query?.limit ?? '10'), 1_000), | ||||
| 		sort, | ||||
| 		filter: (entry: WALK_ENTRY<EVENT>) => { | ||||
| 			const [event_id, event_type] = path.basename(entry.path).replace(/\.json$/i, '').split(':'); | ||||
| 			const { | ||||
| 				groups: { | ||||
| 					event_type, | ||||
| 					event_id | ||||
| 				} | ||||
| 			} = /^.*\/events\/(?<event_type>.*?)\/.*\/(?<event_id>[A-Za-z-]+)\.json$/.exec(entry.path) ?? { groups: {} }; | ||||
| 
 | ||||
| 			if (meta.query.after_id && event_id <= meta.query.after_id) { | ||||
| 				return false; | ||||
|  | @ -165,7 +170,7 @@ export async function POST(req: Request, meta: Record<string, any>): Promise<Res | |||
| 			} | ||||
| 		}; | ||||
| 
 | ||||
| 		event.id = `${lurid()}:${event.type}`; | ||||
| 		event.id = `${event.type}:${lurid()}`; | ||||
| 
 | ||||
| 		await events.create(event); | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,6 +14,10 @@ const DEFAULT_USER_PERMISSIONS: string[] = [ | |||
| 	'self.read', | ||||
| 	'self.write', | ||||
| 	'topics.read', | ||||
| 	'topics.chat.write', | ||||
| 	'topics.chat.read', | ||||
| 	'topics.threads.write', | ||||
| 	'topics.threads.read', | ||||
| 	'users.read' | ||||
| ]; | ||||
| 
 | ||||
|  |  | |||
|  | @ -96,6 +96,18 @@ select { | |||
| 	font-size: inherit; | ||||
| } | ||||
| 
 | ||||
| input, | ||||
| textarea { | ||||
| 	width: 100%; | ||||
| 	color: inherit; | ||||
| 	border-radius: var(--border-radius); | ||||
| 	border: 1px solid rgba(128, 128, 128, 0.2); | ||||
| 	resize: none; | ||||
| 	background: rgba(0, 0, 0, 0.01); | ||||
| 	padding: 0.5rem; | ||||
| 	font-size: large; | ||||
| } | ||||
| 
 | ||||
| /* Make sure textareas without a rows attribute are not tiny */ | ||||
| textarea:not([rows]) { | ||||
| 	min-height: 10em; | ||||
|  | @ -124,7 +136,6 @@ body { | |||
| } | ||||
| 
 | ||||
| input[type="text"]:focus, | ||||
| input[type="textarea"]:focus, | ||||
| textarea:focus { | ||||
| 	border-color: rgb(from var(--border-highlight) r g b / 60%); | ||||
| 	outline: 0; | ||||
|  | @ -166,6 +177,10 @@ textarea:focus { | |||
| 	); | ||||
| } | ||||
| 
 | ||||
| .collapsed { | ||||
| 	height: 0; | ||||
| } | ||||
| 
 | ||||
| form div { | ||||
| 	position: relative; | ||||
| 	display: flex; | ||||
|  | @ -193,15 +208,6 @@ form input:valid ~ label { | |||
| 	font-size: 20px; | ||||
| } | ||||
| 
 | ||||
| form input { | ||||
| 	width: 100%; | ||||
| 	padding: 20px; | ||||
| 	border: 1px solid rgb(from var(--text) r g b / 60%); | ||||
| 	font-size: 20px; | ||||
| 	background-color: var(--bg); | ||||
| 	color: var(--text); | ||||
| } | ||||
| 
 | ||||
| form input:focus { | ||||
| 	outline: none; | ||||
| } | ||||
|  | @ -239,8 +245,20 @@ button.primary { | |||
| 	height: 0; | ||||
| } | ||||
| 
 | ||||
| body[data-perms*="self.read"] [data-requires-permission="self.read"], | ||||
| body[data-perms*="self.write"] [data-requires-permission="self.write"], | ||||
| body[data-perms*="topics.create"] [data-requires-permission="topics.create"], | ||||
| body[data-perms*="topics.read"] [data-requires-permission="topics.read"], | ||||
| body[data-perms*="topics.write"] [data-requires-permission="topics.write"], | ||||
| 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.threads.create"] [data-requires-permission="topics.threads.create"], | ||||
| body[data-perms*="topics.threads.read"] [data-requires-permission="topics.threads.read"], | ||||
| body[data-perms*="topics.threads.write"] [data-requires-permission="topics.threads.write"], | ||||
| body[data-perms*="users.read"] [data-requires-permission="users.read"], | ||||
| body[data-perms*="users.write"] [data-requires-permission="users.write"], | ||||
| body[data-perms*="topics.create"] [data-requires-permission="topics.create"] { | ||||
| body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] { | ||||
| 	visibility: visible; | ||||
| 	opacity: 1; | ||||
| 	height: unset; | ||||
|  |  | |||
							
								
								
									
										24
									
								
								public/js/external/fuzzysearch.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								public/js/external/fuzzysearch.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| function fuzzysearch(needle, haystack) { | ||||
| 	var hlen = haystack.length; | ||||
| 	var nlen = needle.length; | ||||
| 	if (nlen > hlen) { | ||||
| 		return false; | ||||
| 	} | ||||
| 	if (nlen === hlen) { | ||||
| 		return needle === haystack; | ||||
| 	} | ||||
| 	outer: for (var i = 0, j = 0; i < nlen; i++) { | ||||
| 		var nch = needle.charCodeAt(i); | ||||
| 		while (j < hlen) { | ||||
| 			if (haystack.charCodeAt(j++) === nch) { | ||||
| 				continue outer; | ||||
| 			} | ||||
| 		} | ||||
| 		return false; | ||||
| 	} | ||||
| 	return true; | ||||
| } | ||||
| 
 | ||||
| module.exports = fuzzysearch; | ||||
							
								
								
									
										18
									
								
								public/js/notifications.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								public/js/notifications.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| const NOTIFICATIONS = { | ||||
| 	send: function (message, options = {}) { | ||||
| 		if (!Notification || Notification.permission !== "granted") { | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		const notification = new Notification(message, options); | ||||
| 		return notification; | ||||
| 	}, | ||||
| 
 | ||||
| 	request_permissions: function () { | ||||
| 		if (Notification && Notification.permission === "granted") { | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		Notification.requestPermission(); | ||||
| 	}, | ||||
| }; | ||||
|  | @ -60,15 +60,8 @@ | |||
| } | ||||
| 
 | ||||
| #chat #topic-chat-entry-container form textarea { | ||||
| 	width: 100%; | ||||
| 	flex-grow: 1; | ||||
| 	background: inherit; | ||||
| 	color: inherit; | ||||
| 	border-radius: var(--border-radius); | ||||
| 	border: 1px solid rgba(128, 128, 128, 0.2); | ||||
| 	resize: none; | ||||
| 	background: rgba(0, 0, 0, 0.1); | ||||
| 	padding: 0.4rem; | ||||
| } | ||||
| 
 | ||||
| #chat .message-container { | ||||
|  | @ -84,8 +77,9 @@ | |||
| #chat .message-container.user-tick.time-tock + .message-container.user-tick.time-tock, | ||||
| #chat .message-container.user-tock.time-tick + .message-container.user-tock.time-tick, | ||||
| #chat .message-container.user-tock.time-tock + .message-container.user-tock.time-tock { | ||||
| 	padding-top: 0; | ||||
| 	padding-bottom: 0; | ||||
| 	margin-top: 0; | ||||
| 	padding: 0 2px; | ||||
| } | ||||
| 
 | ||||
| #chat | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| # Essays | ||||
| 
 | ||||
| Essays are long-form, authored posts intended to be effectively, a broadcast. | ||||
| Essays are long-form, authored posts intended to be less a conversation | ||||
| and more a broadcast. | ||||
|  |  | |||
|  | @ -16,8 +16,165 @@ | |||
| 		<div class="label">Forum</div></label | ||||
| 	> | ||||
| 	<div class="tab-content forum-container"> | ||||
| 		<button> | ||||
| 			<i class="icon plus" style="display: inline-block; margin-right: 1rem"></i>New Thread | ||||
| 		</button> | ||||
| 		<div id="forum-thread-list"></div> | ||||
| 
 | ||||
| 		<div id="thread-creation-container" data-requires-permission="topics.threads.write"> | ||||
| 			<button | ||||
| 				onclick="(() => { document.querySelector( '#thread-creation' ).classList.toggle( 'collapsed' ); })()" | ||||
| 				class="mockup" | ||||
| 			> | ||||
| 				<i class="icon plus" style="display: inline-block; margin-right: 1rem"></i>New | ||||
| 				Thread | ||||
| 			</button> | ||||
| 			<form | ||||
| 				id="thread-creation" | ||||
| 				data-smart="true" | ||||
| 				action="/api/topics" | ||||
| 				method="POST" | ||||
| 				class="collapsed mockup" | ||||
| 				style=" | ||||
| 					margin-top: 1rem; | ||||
| 					width: 100%; | ||||
| 					overflow: hidden; | ||||
| 					overflow: hidden; | ||||
| 					transition: all 0.5s; | ||||
| 				" | ||||
| 			> | ||||
| 				<input | ||||
| 					id="new-thread-subject" | ||||
| 					type="text" | ||||
| 					name="data.subject" | ||||
| 					value="" | ||||
| 					placeholder="Thread subject..." | ||||
| 					style="margin-bottom: 1rem" | ||||
| 					class="mockup" | ||||
| 				/> | ||||
| 
 | ||||
| 				<input | ||||
| 					id="file-upload-and-share-input-for-thread" | ||||
| 					aria-label="Upload and share file" | ||||
| 					type="file" | ||||
| 					multiple | ||||
| 				/> | ||||
| 				<label for="file-upload-and-share-input-for-thread" class="mockup"> | ||||
| 					<div class="icon attachment"></div> | ||||
| 				</label> | ||||
| 
 | ||||
| 				<textarea | ||||
| 					id="new-thread-content" | ||||
| 					type="text" | ||||
| 					name="data.content" | ||||
| 					value="" | ||||
| 					placeholder=" ... " | ||||
| 					class="mockup" | ||||
| 				></textarea> | ||||
| 
 | ||||
| 				<input type="submit" hidden /> | ||||
| 				<script> | ||||
| 					{ | ||||
| 						const form = document.currentScript.closest("form"); | ||||
| 						const file_input = document.querySelector('input[type="file"]'); | ||||
| 						const subject_input = document.getElementById('input[name="subject"]'); | ||||
| 						const content_input = document.getElementById('input[name="content"]'); | ||||
| 
 | ||||
| 						const parent_id_input = document.getElementById("parent-id"); | ||||
| 
 | ||||
| 						const forum_thread_list = document.getElementById("forum-thread-list"); | ||||
| 
 | ||||
| 						let threads_in_flight = {}; | ||||
| 
 | ||||
| 						form.on_submit = async (event) => { | ||||
| 							const user = JSON.parse(document.body.dataset.user); | ||||
| 							const topic_id = document.body.dataset.topic; | ||||
| 							if (!topic_id) { | ||||
| 								alert("Failed to get topic_id!"); | ||||
| 								return false; | ||||
| 							} | ||||
| 
 | ||||
| 							form.uploaded_urls = []; | ||||
| 							form.errors = []; | ||||
| 							for await (const file of file_input.files) { | ||||
| 								const body = new FormData(); | ||||
| 								body.append("file", file, encodeURIComponent(file.name)); | ||||
| 
 | ||||
| 								const file_path = `/files/users/${user.id}/${encodeURIComponent(file.name)}`; | ||||
| 
 | ||||
| 								const file_upload_response = await api.fetch(file_path, { | ||||
| 									method: "PUT", | ||||
| 									body, | ||||
| 								}); | ||||
| 
 | ||||
| 								if (!file_upload_response.ok) { | ||||
| 									const error = await file_upload_response.json(); | ||||
| 									form.errors.push(error?.error?.message ?? "Unknown error."); | ||||
| 									continue; | ||||
| 								} | ||||
| 
 | ||||
| 								const file_url = `${window.location.protocol}//${window.location.host}${file_path}`; | ||||
| 								form.uploaded_urls.push(file_url); | ||||
| 							} | ||||
| 
 | ||||
| 							if (form.errors.length) { | ||||
| 								const errors = form.errors.join("\n\n"); | ||||
| 								alert(errors); | ||||
| 								return false; | ||||
| 							} | ||||
| 
 | ||||
| 							const message = chat_input.value.trim(); | ||||
| 							if (form.uploaded_urls.length === 0 && message.length === 0) { | ||||
| 								return false; | ||||
| 							} | ||||
| 
 | ||||
| 							form.action = `/api/topics/${topic_id}/events`; | ||||
| 						}; | ||||
| 
 | ||||
| 						form.on_parsed = (json) => { | ||||
| 							const now = new Date().toISOString(); | ||||
| 
 | ||||
| 							const temp_id = `TEMP-${now}`; | ||||
| 							json.id = temp_id; | ||||
| 							json.type = "chat"; | ||||
| 							json.meta = { | ||||
| 								temp_id, | ||||
| 							}; | ||||
| 							json.timestamps = { | ||||
| 								created: now, | ||||
| 								updated: now, | ||||
| 							}; | ||||
| 
 | ||||
| 							if (form.uploaded_urls.length) { | ||||
| 								json.data = json.data ?? {}; | ||||
| 								json.data.content = | ||||
| 									(typeof json.data.content === "string" && | ||||
| 									json.data.content.trim().length | ||||
| 										? json.data.content.trim() + "\n" | ||||
| 										: "") + form.uploaded_urls.join("\n"); | ||||
| 							} | ||||
| 
 | ||||
| 							const user = JSON.parse(document.body.dataset.user); | ||||
| 							render_text_event(topic_chat_content, json, user); | ||||
| 							document.getElementById(`chat-${temp_id}`)?.classList.add("sending"); | ||||
| 						}; | ||||
| 
 | ||||
| 						form.on_error = (error) => { | ||||
| 							// TODO: mark the temporary message element with the failed class? | ||||
| 							alert(error); | ||||
| 							chat_input.focus(); | ||||
| 						}; | ||||
| 
 | ||||
| 						form.on_reply = (sent_message) => { | ||||
| 							document | ||||
| 								.getElementById(`chat-${sent_message.meta?.temp_id ?? ""}`) | ||||
| 								?.classList.remove("sending"); | ||||
| 
 | ||||
| 							append_topic_events([sent_message]); | ||||
| 							parent_id_input.value = ""; | ||||
| 							chat_input.value = ""; | ||||
| 							chat_input.focus(); | ||||
| 						}; | ||||
| 					} | ||||
| 				</script> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| # Resources | ||||
| 
 | ||||
| Resources should be a wiki for organizing community knowledge. | ||||
| Resources should be a wiki for organizing community knowledge on a topic. | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue