+3
config.maple
+3
config.maple
+3
src/entity/post.v
+3
src/entity/post.v
···
14
14
body string
15
15
16
16
pinned bool
17
+
nsfw bool
17
18
18
19
posted_at time.Time = time.now()
19
20
}
···
33
34
// this throws a cgen error when put in Post{}
34
35
//todo: report this
35
36
posted_at := time.parse(ct('posted_at')) or { panic(err) }
37
+
nsfw := util.map_or_throw[string, bool](ct('nsfw'), |it| it.bool())
36
38
37
39
return Post{
38
40
id: ct('id').int()
···
43
45
title: ct('title')
44
46
body: ct('body')
45
47
pinned: util.map_or_throw[string, bool](ct('pinned'), |it| it.bool())
48
+
nsfw: nsfw
46
49
posted_at: posted_at
47
50
}
48
51
}
+16
-2
src/static/style.css
+16
-2
src/static/style.css
···
4
4
padding: 8px;
5
5
}
6
6
7
-
.post p,
8
-
.notification p {
7
+
.post > p,
8
+
.notification > p {
9
9
margin: 0;
10
10
}
11
11
12
+
.post > pre,
13
+
.notification > pre {
14
+
margin: 0;
15
+
display: inline;
16
+
}
17
+
12
18
.post + .post,
13
19
.notification + .notification {
14
20
margin-top: 6px;
···
16
22
17
23
pre {
18
24
white-space: pre-wrap;
25
+
}
26
+
27
+
span.nsfw-indicator {
28
+
border: 2px solid red;
29
+
border-radius: 2px;
30
+
padding-left: 4px;
31
+
padding-right: 4px;
32
+
margin-left: 6px;
19
33
}
20
34
21
35
/*
+71
src/templates/components/new_post.html
+71
src/templates/components/new_post.html
···
1
+
<script src="/static/js/text_area_counter.js"></script>
2
+
<div>
3
+
<form action="/api/post/new_post" method="post">
4
+
<h2>new post:</h2>
5
+
6
+
@if replying
7
+
<input
8
+
type="number"
9
+
name="replying_to"
10
+
id="replying_to"
11
+
required aria-required
12
+
readonly aria-readonly
13
+
hidden aria-hidden
14
+
value="@replying_to"
15
+
>
16
+
@end
17
+
18
+
<p id="title_chars">0/@{app.config.post.title_max_len}</p>
19
+
@if replying
20
+
<input
21
+
type="text"
22
+
name="title"
23
+
id="title"
24
+
value="reply to @{replying_to_user.get_name()}"
25
+
required aria-required
26
+
readonly aria-readonly
27
+
hidden aria-hidden
28
+
>
29
+
@else
30
+
<input
31
+
type="text"
32
+
name="title"
33
+
id="title"
34
+
minlength="@app.config.post.title_min_len"
35
+
maxlength="@app.config.post.title_max_len"
36
+
pattern="@app.config.post.title_pattern"
37
+
placeholder="title"
38
+
required aria-required
39
+
>
40
+
@end
41
+
<br>
42
+
43
+
<p id="body_chars">0/@{app.config.post.body_max_len}</p>
44
+
<textarea
45
+
name="body"
46
+
id="body"
47
+
minlength="@app.config.post.body_min_len"
48
+
maxlength="@app.config.post.body_max_len"
49
+
rows="10"
50
+
cols="30"
51
+
placeholder="body"
52
+
required aria-required
53
+
autocomplete="off" aria-autocomplete="off"
54
+
></textarea>
55
+
<br>
56
+
57
+
@if app.config.post.allow_nsfw
58
+
<label for="nsfw">is nsfw:</label>
59
+
<input type="checkbox" name="nsfw" id="nsfw" />
60
+
@else
61
+
<input type="checkbox" name="nsfw" id="nsfw" hidden aria-hidden />
62
+
@end
63
+
64
+
<input type="submit" value="post!">
65
+
</form>
66
+
67
+
<script>
68
+
add_character_counter('title', 'title_chars', @{app.config.post.title_max_len})
69
+
add_character_counter('body', 'body_chars', @{app.config.post.body_max_len})
70
+
</script>
71
+
</div>
+3
src/templates/components/post_mini.html
+3
src/templates/components/post_mini.html
···
2
2
<p>
3
3
<a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}"><strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong></a>:
4
4
<a href="/post/@post.id">@post.title</a>
5
+
@if post.nsfw
6
+
<span class="nsfw-indicator">(<em>nsfw</em>)</span>
7
+
@end
5
8
</p>
6
9
</div>
+16
-4
src/templates/components/post_small.html
+16
-4
src/templates/components/post_small.html
···
1
1
<div class="post post-small">
2
-
<p><a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}"><strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong></a>: <span class="post-title">@post.title</span></p>
3
-
@if post.body.len > 50
4
-
<p>@{post.body[..50]}...</p>
2
+
<p>
3
+
<a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}"><strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong></a>:
4
+
<span class="post-title">@post.title</span>
5
+
@if post.nsfw
6
+
<span class="nsfw-indicator">(<em>nsfw</em>)</span>
7
+
@end
8
+
</p>
9
+
10
+
@if post.nsfw
11
+
<p>view full to see post body</p>
5
12
@else
6
-
<p>@post.body</p>
13
+
@if post.body.len > 50
14
+
<pre id="post-@{post.id}">@{post.body[..50]}...</pre>
15
+
@else
16
+
<pre id="post-@{post.id}">@post.body</pre>
17
+
@end
7
18
@end
19
+
8
20
<p>likes: @{app.get_net_likes_for_post(post.id)} | posted at: @post.posted_at | <a href="/post/@post.id">view full post</a></p>
9
21
</div>
+5
-5
src/templates/edit.html
+5
-5
src/templates/edit.html
···
49
49
50
50
<input type="submit" value="save">
51
51
</form>
52
-
53
-
<script>
54
-
add_character_counter('title', 'title_chars', @{app.config.post.title_max_len})
55
-
add_character_counter('body', 'body_chars', @{app.config.post.body_max_len})
56
-
</script>
57
52
</div>
58
53
59
54
<hr>
···
74
69
<input type="submit" value="delete">
75
70
</form>
76
71
</div>
72
+
73
+
<script>
74
+
add_character_counter('title', 'title_chars', @{app.config.post.title_max_len})
75
+
add_character_counter('body', 'body_chars', @{app.config.post.body_max_len})
76
+
</script>
77
77
78
78
@include 'partial/footer.html'
+1
-52
src/templates/new_post.html
+1
-52
src/templates/new_post.html
···
8
8
@else
9
9
<h2>make a post...</h2>
10
10
@end
11
-
12
-
<div>
13
-
<form action="/api/post/new_post" method="post">
14
-
@if replying
15
-
<input
16
-
type="number"
17
-
name="replying_to"
18
-
id="replying_to"
19
-
required aria-required
20
-
readonly aria-readonly
21
-
hidden aria-hidden
22
-
value="@replying_to"
23
-
>
24
-
<input
25
-
type="text"
26
-
name="title"
27
-
id="title"
28
-
value="reply to @{replying_to_user.get_name()}"
29
-
required aria-required
30
-
readonly aria-readonly
31
-
hidden aria-hidden
32
-
>
33
-
@else
34
-
<input
35
-
type="text"
36
-
name="title"
37
-
id="title"
38
-
minlength="@app.config.post.title_min_len"
39
-
maxlength="@app.config.post.title_max_len"
40
-
pattern="@app.config.post.title_pattern"
41
-
placeholder="title"
42
-
required aria-required
43
-
>
44
-
@end
45
-
46
-
<br>
47
-
<textarea
48
-
name="body"
49
-
id="body"
50
-
minlength="@app.config.post.body_min_len"
51
-
maxlength="@app.config.post.body_max_len"
52
-
rows="10"
53
-
cols="30"
54
-
placeholder="in reply to @{replying_to_user.get_name()}..."
55
-
required
56
-
></textarea>
57
-
58
-
<br>
59
-
60
-
<input type="submit" value="post!">
61
-
</form>
62
-
</div>
11
+
@include 'components/new_post.html'
63
12
@else
64
13
<p>uh oh, you need to be logged in to see this page</p>
65
14
@end
+15
-1
src/templates/post.html
+15
-1
src/templates/post.html
···
5
5
6
6
<div class="post post-full">
7
7
<h2>
8
-
<a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}"><strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong></a>
8
+
<a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}">
9
+
<strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong>
10
+
</a>
9
11
-
10
12
@if replying_to_post.id == 0
11
13
@post.title
12
14
@else
13
15
replied to <a href="/user/@{replying_to_user.username}">@{replying_to_user.get_name()}</a>
14
16
@end
17
+
@if post.nsfw
18
+
<span class="nsfw-indicator">(<em>nsfw</em>)</span>
19
+
@end
15
20
</h2>
21
+
22
+
@if post.nsfw
23
+
<details>
24
+
<summary>click to show post (nsfw)</summary>
25
+
<pre id="post-@{post.id}">@post.body</pre>
26
+
</details>
27
+
@else
16
28
<pre id="post-@{post.id}">@post.body</pre>
29
+
@end
30
+
17
31
<p><em>likes: @{app.get_net_likes_for_post(post.id)}</em></p>
18
32
<p><em>posted at: @post.posted_at</em></p>
19
33
+3
src/templates/saved_posts.html
+3
src/templates/saved_posts.html
···
16
16
<p>
17
17
<a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}"><strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong></a>:
18
18
<a href="/post/@post.id">@post.title</a>
19
+
@if post.nsfw
20
+
<span class="nsfw-indicator">(<em>nsfw</em>)</span>
21
+
@end
19
22
<button onclick="save(@post.id)" style="display: inline-block;">unsave</button>
20
23
</p>
21
24
</div>
+3
src/templates/saved_posts_for_later.html
+3
src/templates/saved_posts_for_later.html
···
16
16
<p>
17
17
<a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}"><strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong></a>:
18
18
<a href="/post/@post.id">@post.title</a>
19
+
@if post.nsfw
20
+
<span class="nsfw-indicator">(<em>nsfw</em>)</span>
21
+
@end
19
22
<button onclick="save_for_later(@post.id)" style="display: inline-block;">unsave</button>
20
23
</p>
21
24
</div>
+8
src/templates/search.html
+8
src/templates/search.html
···
67
67
post_link.innerText = result.post.title
68
68
p.appendChild(post_link)
69
69
70
+
if (result.post.nsfw)
71
+
{
72
+
const nsfw_indicator = document.createElement('span')
73
+
nsfw_indicator.classList.add('nsfw-indicator')
74
+
nsfw_indicator.innerHTML = '(<em>nsfw</em>)';
75
+
p.appendChild(nsfw_indicator)
76
+
}
77
+
70
78
element.appendChild(p)
71
79
results.appendChild(element)
72
80
}
+1
-42
src/templates/user.html
+1
-42
src/templates/user.html
···
23
23
24
24
@if app.logged_in_as(mut ctx, viewing.id)
25
25
<p>this is you!</p>
26
-
27
26
@if !user.automated
28
-
<script src="/static/js/text_area_counter.js"></script>
29
-
<div>
30
-
<form action="/api/post/new_post" method="post">
31
-
<h2>new post:</h2>
32
-
33
-
<p id="title_chars">0/@{app.config.post.title_max_len}</p>
34
-
<input
35
-
type="text"
36
-
name="title"
37
-
id="title"
38
-
minlength="@app.config.post.title_min_len"
39
-
maxlength="@app.config.post.title_max_len"
40
-
pattern="@app.config.post.title_pattern"
41
-
placeholder="title"
42
-
required aria-required
43
-
autocomplete="off" aria-autocomplete="off"
44
-
>
45
-
<br>
46
-
47
-
<p id="body_chars">0/@{app.config.post.body_max_len}</p>
48
-
<textarea
49
-
name="body"
50
-
id="body"
51
-
minlength="@app.config.post.body_min_len"
52
-
maxlength="@app.config.post.body_max_len"
53
-
rows="10"
54
-
cols="30"
55
-
placeholder="body"
56
-
required aria-required
57
-
autocomplete="off" aria-autocomplete="off"
58
-
></textarea>
59
-
<br>
60
-
61
-
<input type="submit" value="post!">
62
-
</form>
63
-
64
-
<script>
65
-
add_character_counter('title', 'title_chars', @{app.config.post.title_max_len})
66
-
add_character_counter('body', 'body_chars', @{app.config.post.body_max_len})
67
-
</script>
68
-
</div>
27
+
@include 'components/new_post.html'
69
28
<hr>
70
29
@end
71
30
@end
+7
src/webapp/api.v
+7
src/webapp/api.v
···
509
509
return ctx.redirect('/post/new')
510
510
}
511
511
512
+
nsfw := 'nsfw' in ctx.form
513
+
if nsfw && !app.config.post.allow_nsfw {
514
+
ctx.error('nsfw posts are not allowed on this instance')
515
+
return ctx.redirect('/post/new')
516
+
}
517
+
512
518
mut post := Post{
513
519
author_id: user.id
514
520
title: title
515
521
body: body
522
+
nsfw: nsfw
516
523
}
517
524
518
525
if replying_to != 0 {
+2
src/webapp/config.v
+2
src/webapp/config.v
···
45
45
body_min_len int
46
46
body_max_len int
47
47
body_pattern string
48
+
allow_nsfw bool
48
49
}
49
50
user struct {
50
51
pub mut:
···
111
112
config.post.body_min_len = loaded_post.get('body_min_len').to_int()
112
113
config.post.body_max_len = loaded_post.get('body_max_len').to_int()
113
114
config.post.body_pattern = loaded_post.get('body_pattern').to_str()
115
+
config.post.allow_nsfw = loaded_post.get('allow_nsfw').to_bool()
114
116
115
117
loaded_user := loaded.get('user')
116
118
config.user.username_min_len = loaded_user.get('username_min_len').to_int()
+6
src/webapp/pages.v
+6
src/webapp/pages.v
···
112
112
}
113
113
ctx.title = '${app.config.instance.name} - ${user.get_name()}'
114
114
posts := app.get_posts_from_user(viewing.id, 10)
115
+
116
+
// needed for new_post component
117
+
replying := false
118
+
replying_to := 0
119
+
replying_to_user := User{}
120
+
115
121
return $veb.html('../templates/user.html')
116
122
}
117
123