a mini social media app for small communities

display body all at once instead of over time

Changed files
+139 -42
doc
src
database
static
templates
+7 -13
.gitignore
··· 1 - # Binaries for programs and plugins 1 + # binaries 2 2 main 3 3 clockwork 4 4 beep ··· 7 7 *.so 8 8 *.dylib 9 9 *.dll 10 - 11 - # Ignore binary output folders 12 10 bin/ 13 11 14 - # Ignore common editor/system specific metadata 12 + # editor/system specific metadata 15 13 .DS_Store 16 14 .idea/ 17 15 .vscode/ 18 16 *.iml 19 17 20 - # ENV 18 + # secrets 19 + /config.real.maple 21 20 .env 22 21 23 - # vweb and database 24 - *.db 25 - 26 - # Local V install 22 + # local v and clockwork install (from gitpod stuffs) 27 23 /v/ 28 - 29 - # Local Clockwork install 30 24 /clockwork/ 31 25 32 - # "Real" config (contains secrets and such) 33 - /config.real.maple 26 + # quick notes i keep while developing 27 + /stickynote.md
+15 -2
doc/todo.md
··· 4 4 5 5 ## in-progress 6 6 7 + - [ ] post:embedded links (links added to a post will be embedded into the post 8 + as images, music links, etc) 9 + - should have special handling for spotify, apple music, youtube, 10 + discord, and other common links. we want those ones to look fancy! 11 + 7 12 ## planing 8 13 9 14 > p.s. when initially writing "planing," i made a typo. it should be "planning." 10 15 > however, i will not be fixing it, because it is funny. 11 16 12 - - [ ] post:images (should have a config.maple toggle to enable/disable) 13 17 - [ ] post:saving (add the post to a list of saved posts that a user can view later) 18 + - [ ] post:search for posts 19 + - [ ] user:search for users 20 + - [ ] user:follow other users (send notifications on new posts) 14 21 15 22 ## ideas 16 23 17 24 - [ ] user:per-user post pins 18 25 - could be used as an alternative for a bio to include more information perhaps 26 + - [ ] site:rss feed? 19 27 20 28 ## done 21 29 ··· 32 40 - [x] post:editing 33 41 - [x] post:replies 34 42 - [x] post:tags ('hashtags') 43 + - [x] site:message of the day (admins can add a welcome message displayed on index.html) 44 + 45 + ## graveyard 46 + 47 + - [ ] ~~post:images (should have a config.maple toggle to enable/disable)~~ 48 + - replaced with post:embedded links 35 49 - [ ] ~~site:stylesheet (and a toggle for html-only mode)~~ 36 50 - replaced with per-user optional stylesheets 37 - - [x] site:message of the day (admins can add a welcome message displayed on index.html)
+10 -1
src/database/post.v
··· 69 69 70 70 // get_posts_from_user returns a list of all posts from a user in descending 71 71 // order by posting date. 72 - pub fn (app &DatabaseAccess) get_posts_from_user(user_id int) []Post { 72 + pub fn (app &DatabaseAccess) get_posts_from_user(user_id int, limit int) []Post { 73 + posts := sql app.db { 74 + select from Post where author_id == user_id order by posted_at desc limit limit 75 + } or { [] } 76 + return posts 77 + } 78 + 79 + // get_all_posts_from_user returns a list of all posts from a user in descending 80 + // order by posting date. 81 + pub fn (app &DatabaseAccess) get_all_posts_from_user(user_id int) []Post { 73 82 posts := sql app.db { 74 83 select from Post where author_id == user_id order by posted_at desc 75 84 } or { [] }
+105 -24
src/static/js/render_body.js
··· 1 - // TODO: move this to the backend? 1 + const get_apple_music_iframe = src => 2 + `<iframe 3 + class="post-iframe iframe-music iframe-music-apple" 4 + style="border-radius:12px" 5 + width="100%" 6 + height="152" 7 + frameBorder="0" 8 + allowfullscreen="" 9 + allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" 10 + loading="lazy" 11 + src="${src}" 12 + ></iframe>` 13 + 14 + const get_spotify_iframe = src => 15 + `<iframe 16 + class="post-iframe iframe-music iframe-music-spotify" 17 + allow="autoplay *; encrypted-media *; fullscreen *; clipboard-write" 18 + frameborder="0" 19 + height="175" 20 + style="width:100%;overflow:hidden;border-radius:10px;" 21 + sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" 22 + loading="lazy" 23 + src="${src}" 24 + ></iframe>` 25 + 26 + const get_youtube_frame = src => 27 + `<iframe 28 + width="560" 29 + height="315" 30 + src="${src}" 31 + title="YouTube video player" 32 + frameborder="0" 33 + allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" 34 + referrerpolicy="strict-origin-when-cross-origin" 35 + allowfullscreen 36 + ></iframe>` 37 + 38 + const link_handlers = { 39 + 'https://music.apple.com/': link => { 40 + const embed_url = `https://embed.${link.substring(8)}` 41 + return get_apple_music_iframe(embed_url) 42 + }, 43 + 'https://open.spotify.com/': link => { 44 + const type = link.substring(link.indexOf('/', 8) + 1, link.indexOf('/', link.indexOf('/', 8) + 1)) 45 + const id = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?')) 46 + const embed_url = `https://open.spotify.com/embed/${type}/${id}?utm_source=generator&theme=0` 47 + return get_spotify_iframe(embed_url) 48 + }, 49 + 'https://youtu.be/': link => { 50 + const id = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?')) 51 + const embed_url = `https://www.youtube.com/embed/${id}` 52 + return get_youtube_frame(embed_url) 53 + }, 54 + } 55 + 2 56 const render_body = async id => { 3 57 const element = document.getElementById(id) 4 58 var body = element.innerText 59 + var html = element.innerHTML 60 + 61 + // give the body a loading """animation""" while we let the fetches cook 62 + element.innerText = 'loading...' 5 63 6 64 const matches = body.matchAll(/[@#*]\([a-zA-Z0-9_.-]*\)/g) 7 65 const cache = {} ··· 9 67 // mention 10 68 if (match[0][0] == '@') { 11 69 if (cache.hasOwnProperty(match[0])) { 12 - element.innerHTML = element.innerHTML.replace(match[0], cache[match[0]]) 70 + html = html.replace(match[0], cache[match[0]]) 13 71 continue 14 72 } 15 - (await fetch('/api/user/get_name?username=' + match[0].substring(2, match[0].length - 1))).text().then(s => { 16 - if (s == 'no such user') { 17 - return 18 - } 19 - const link = document.createElement('a') 20 - link.href = `/user/${match[0].substring(2, match[0].length - 1)}` 21 - link.innerText = '@' + s 22 - cache[match[0]] = link.outerHTML 23 - element.innerHTML = element.innerHTML.replace(match[0], link.outerHTML) 24 - }) 73 + const s = await (await fetch('/api/user/get_name?username=' + match[0].substring(2, match[0].length - 1))).text() 74 + const link = document.createElement('a') 75 + link.href = `/user/${match[0].substring(2, match[0].length - 1)}` 76 + link.innerText = '@' + s 77 + cache[match[0]] = link.outerHTML 78 + html = html.replace(match[0], link.outerHTML) 25 79 } 26 80 // tags 27 81 else if (match[0][0] == '#') { ··· 33 87 link.href = `/tag/${tag}` 34 88 link.innerText = '#' + tag 35 89 cache[match[0]] = link.outerHTML 36 - element.innerHTML = element.innerHTML.replace(match[0], link.outerHTML) 90 + html = html.replace(match[0], link.outerHTML) 37 91 } 38 92 // post reference 39 93 else if (match[0][0] == '*') { 40 94 if (cache.hasOwnProperty(match[0])) { 41 - element.innerHTML = element.innerHTML.replace(match[0], cache[match[0]]) 95 + html = html.replace(match[0], cache[match[0]]) 42 96 continue 43 97 } 44 - (await fetch('/api/post/get_title?id=' + match[0].substring(2, match[0].length - 1))).text().then(s => { 45 - if (s == 'no such post') { 46 - return 47 - } 48 - const link = document.createElement('a') 49 - link.href = `/post/${match[0].substring(2, match[0].length - 1)}` 50 - link.innerText = '*' + s 51 - cache[match[0]] = link.outerHTML 52 - element.innerHTML = element.innerHTML.replace(match[0], link.outerHTML) 53 - }) 98 + const s = await (await fetch('/api/post/get_title?id=' + match[0].substring(2, match[0].length - 1))).text() 99 + const link = document.createElement('a') 100 + link.href = `/post/${match[0].substring(2, match[0].length - 1)}` 101 + link.innerText = '*' + s 102 + cache[match[0]] = link.outerHTML 103 + html = html.replace(match[0], link.outerHTML) 54 104 } 55 105 } 106 + 107 + var handled_links = [] 108 + // i am not willing to write a url regex myself, so here is where i got 109 + // this: https://stackoverflow.com/a/3809435 110 + const links = html.matchAll(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g) 111 + for (const match of links) { 112 + const link = match[0] 113 + for (const entry of Object.entries(link_handlers)) { 114 + if (link.startsWith(entry[0])) { 115 + handled_links.push(entry[1](link)) 116 + break 117 + } 118 + } 119 + // sanatize the link before rendering it directly. no link 120 + // should ever have these three characters in them anyway. 121 + const sanatized = link 122 + .replace('<', '&gt;') 123 + .replace('>', '&lt;') 124 + .replace('"', '&quot;') 125 + html = html.replace(link, `<a href="${sanatized}">${sanatized}</a>`) 126 + } 127 + 128 + // append handled links 129 + if (handled_links.length > 0) { 130 + // element.innerHTML += '\n\nlinks:\n' 131 + for (const handled of handled_links) { 132 + html += `\n\n${handled}` 133 + } 134 + } 135 + 136 + element.innerHTML = html 56 137 }
+2 -2
src/templates/user.html
··· 61 61 @end 62 62 63 63 <div> 64 - <h2>posts:</h2> 65 - @for post in app.get_posts_from_user(viewing.id) 64 + <h2>recent posts:</h2> 65 + @for post in app.get_posts_from_user(viewing.id, 10) 66 66 @include 'components/post_small.html' 67 67 @end 68 68 </div>