+7
-13
.gitignore
+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
+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
+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
+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('<', '>')
123
+
.replace('>', '<')
124
+
.replace('"', '"')
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
}