+8
-7
doc/database_spec.md
+8
-7
doc/database_spec.md
···
27
27
28
28
> represents a public post
29
29
30
-
| name | type | desc |
31
-
|-------------|-----------|----------------------------------------|
32
-
| `id` | int | identifier for this post |
33
-
| `author_id` | int | id of the user that authored this post |
34
-
| `title` | string | the title of this post |
35
-
| `body` | string | the body of this post |
36
-
| `posted_at` | time.Time | a timestamp of when this post was made |
30
+
| name | type | desc |
31
+
|---------------|-----------|----------------------------------------------|
32
+
| `id` | int | identifier for this post |
33
+
| `author_id` | int | id of the user that authored this post |
34
+
| `replying_to` | ?int | id of the post that this post is replying to |
35
+
| `title` | string | the title of this post |
36
+
| `body` | string | the body of this post |
37
+
| `posted_at` | time.Time | a timestamp of when this post was made |
37
38
38
39
## `Like`
39
40
+1
-2
doc/todo.md
+1
-2
doc/todo.md
···
4
4
5
5
## in-progress
6
6
7
-
- [ ] post:replies
8
-
9
7
## planing
10
8
11
9
- [ ] post:tags ('hashtags')
···
30
28
- [x] post:mentioning ('tagging') other users in posts
31
29
- [x] post:mentioning:who mentioned you (send notifications when a user mentions you)
32
30
- [x] post:editing
31
+
- [x] post:replies
33
32
- [ ] ~~site:stylesheet (and a toggle for html-only mode)~~
34
33
- replaced with per-user optional stylesheets
35
34
- [x] site:message of the day (admins can add a welcome message displayed on index.html)
+18
-9
src/api.v
+18
-9
src/api.v
···
382
382
////// post //////
383
383
384
384
@['/api/post/new_post'; post]
385
-
fn (mut app App) api_post_new_post(mut ctx Context, title string, body string) veb.Result {
385
+
fn (mut app App) api_post_new_post(mut ctx Context, replying_to int, title string, body string) veb.Result {
386
386
user := app.whoami(mut ctx) or {
387
387
ctx.error('not logged in!')
388
-
return ctx.redirect('/')
388
+
return ctx.redirect('/login')
389
389
}
390
390
391
391
if user.muted {
392
392
ctx.error('you are muted!')
393
-
return ctx.redirect('/me')
393
+
return ctx.redirect('/post/new')
394
394
}
395
395
396
396
// validate title
397
397
if !app.validators.post_title.validate(title) {
398
398
ctx.error('invalid title')
399
-
return ctx.redirect('/me')
399
+
return ctx.redirect('/post/new')
400
400
}
401
401
402
402
// validate body
403
403
if !app.validators.post_body.validate(body) {
404
404
ctx.error('invalid body')
405
-
return ctx.redirect('/me')
405
+
return ctx.redirect('/post/new')
406
406
}
407
407
408
-
post := Post{
408
+
mut post := Post{
409
409
author_id: user.id
410
410
title: title
411
411
body: body
412
412
}
413
413
414
+
if replying_to != 0 {
415
+
// check if replying post exists
416
+
app.get_post_by_id(replying_to) or {
417
+
ctx.error('the post you are trying to reply to does not exist')
418
+
return ctx.redirect('/post/new')
419
+
}
420
+
post.replying_to = replying_to
421
+
}
422
+
414
423
sql app.db {
415
424
insert post into Post
416
425
} or {
417
426
ctx.error('failed to post!')
418
427
println('failed to post: ${post} from user ${user.id}')
419
-
return ctx.redirect('/me')
428
+
return ctx.redirect('/post/new')
420
429
}
421
430
422
431
// find the post's id to process mentions with
423
432
if x := app.get_post_by_author_and_timestamp(user.id, post.posted_at) {
424
433
app.process_post_mentions(x)
434
+
return ctx.redirect('/post/${x.id}')
425
435
} else {
426
436
ctx.error('failed to get_post_by_timestamp_and_author for ${post}')
437
+
return ctx.redirect('/me')
427
438
}
428
-
429
-
return ctx.redirect('/me')
430
439
}
431
440
432
441
@['/api/post/delete'; post]
+19
-3
src/app.v
+19
-3
src/app.v
···
154
154
}
155
155
}
156
156
157
+
pub fn (app &App) get_unknown_post() Post {
158
+
return Post{
159
+
title: 'unknown'
160
+
}
161
+
}
162
+
157
163
pub fn (app &App) logged_in_as(mut ctx Context, id int) bool {
158
164
if !ctx.is_logged_in() {
159
165
return false
···
308
314
}
309
315
author_name := author.get_name()
310
316
317
+
// used so we do not send more than one notification per post
318
+
mut notified_users := []int{}
319
+
320
+
// notify who we replied to, if applicable
321
+
if post.replying_to != none {
322
+
if x := app.get_post_by_id(post.replying_to) {
323
+
app.send_notification_to(x.author_id, '${author_name} replied to your post!', '${author_name} replied to *(${x.id})')
324
+
}
325
+
}
326
+
327
+
// find mentions
311
328
mut re := regex.regex_opt('@\\(${app.config.user.username_pattern}\\)') or {
312
329
eprintln('failed to compile regex for process_post_mentions (err: ${err})')
313
330
return
314
331
}
315
332
matches := re.find_all_str(post.body)
316
-
mut mentioned_users := []int{}
317
333
for mat in matches {
318
334
println('found mentioned user: ${mat}')
319
335
username := mat#[2..-1]
···
321
337
continue
322
338
}
323
339
324
-
if user.id in mentioned_users || user.id == author.id {
340
+
if user.id in notified_users || user.id == author.id {
325
341
continue
326
342
}
327
-
mentioned_users << user.id
343
+
notified_users << user.id
328
344
329
345
app.send_notification_to(
330
346
user.id,
+3
-2
src/entity/post.v
+3
-2
src/entity/post.v
+46
src/pages.v
+46
src/pages.v
···
77
77
}
78
78
ctx.title = '${app.config.instance.name} - ${post.title}'
79
79
user := app.whoami(mut ctx) or { User{} }
80
+
81
+
mut replying_to_post := app.get_unknown_post()
82
+
mut replying_to_user := app.get_unknown_user()
83
+
84
+
if post.replying_to != none {
85
+
replying_to_post = app.get_post_by_id(post.replying_to) or {
86
+
app.get_unknown_post()
87
+
}
88
+
replying_to_user = app.get_user_by_id(replying_to_post.author_id) or {
89
+
app.get_unknown_user()
90
+
}
91
+
}
92
+
80
93
return $veb.html()
81
94
}
82
95
···
97
110
ctx.title = '${app.config.instance.name} - editing ${post.title}'
98
111
return $veb.html()
99
112
}
113
+
114
+
@['/post/:post_id/reply']
115
+
fn (mut app App) reply(mut ctx Context, post_id int) veb.Result {
116
+
user := app.whoami(mut ctx) or {
117
+
ctx.error('not logged in')
118
+
return ctx.redirect('/login')
119
+
}
120
+
post := app.get_post_by_id(post_id) or {
121
+
ctx.error('no such post')
122
+
return ctx.redirect('/')
123
+
}
124
+
ctx.title = '${app.config.instance.name} - reply to ${post.title}'
125
+
replying := true
126
+
replying_to := post_id
127
+
replying_to_user := app.get_user_by_id(post.author_id) or {
128
+
ctx.error('no such post')
129
+
return ctx.redirect('/')
130
+
}
131
+
return $veb.html('templates/new_post.html')
132
+
}
133
+
134
+
@['/post/new']
135
+
fn (mut app App) new(mut ctx Context) veb.Result {
136
+
user := app.whoami(mut ctx) or {
137
+
ctx.error('not logged in')
138
+
return ctx.redirect('/login')
139
+
}
140
+
ctx.title = '${app.config.instance.name} - new post'
141
+
replying := false
142
+
replying_to := 0
143
+
replying_to_user := User{}
144
+
return $veb.html('templates/new_post.html')
145
+
}
+1
-1
src/templates/index.html
+1
-1
src/templates/index.html
+67
src/templates/new_post.html
+67
src/templates/new_post.html
···
1
+
@include 'partial/header.html'
2
+
3
+
@if ctx.is_logged_in()
4
+
5
+
@if replying
6
+
<h1>reply to @{replying_to_user.get_name()} with...</h1>
7
+
<p>(replying to <a href="/post/${replying_to}">this</a>)</p>
8
+
@else
9
+
<h2>make a post...</h2>
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>
63
+
@else
64
+
<p>uh oh, you need to be logged in to see this page</p>
65
+
@end
66
+
67
+
@include 'partial/footer.html'
+13
-1
src/templates/post.html
+13
-1
src/templates/post.html
···
4
4
<script src="/static/js/render_body.js"></script>
5
5
6
6
<div class="post post-full">
7
-
<h2><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> - @post.title</h2>
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>
9
+
-
10
+
@if replying_to_post.id == 0
11
+
@post.title
12
+
@else
13
+
replied to <a href="/user/@{replying_to_user.username}">@{replying_to_user.get_name()}</a>
14
+
@end
15
+
</h2>
8
16
<pre id="post-@{post.id}">@post.body</pre>
9
17
<p><em>likes: @{app.get_net_likes_for_post(post.id)}</em></p>
10
18
<p><em>posted at: @post.posted_at</em></p>
19
+
20
+
@if ctx.is_logged_in()
21
+
<p><a href="/post/@{post.id}/reply">reply</a></p>
22
+
@end
11
23
12
24
@if ctx.is_logged_in() && post.author_id == user.id
13
25
<p><a href="/post/@{post.id}/edit">edit post</a></p>