posts: PostList,
following: UserList,
followers: UserList,
+
+ post_lists: PostListList,
+ feeds: UserListList,
};
const Post = struct {
text: PostText,
};
+const SavedPostList = struct {
+ name: Name,
+ list: PostList,
+};
+const SavedUserList = struct {
+ name: Name,
+ list: UserList,
+};
+
const Vote = struct {
const Kind = enum { Up, Down };
user: User,
session_token: SessionToken,
};
+const Name = std.BoundedArray(u8, 32);
const UserId = enum(u64) { _ };
const PostId = enum(u64) { _ };
const Timestamp = i64;
-const Username = std.BoundedArray(u8, 16);
+const Username = std.BoundedArray(u8, 32);
const DisplayName = std.BoundedArray(u8, 64);
const PasswordHash = std.BoundedArray(u8, 128);
const SessionToken = u64;
const PostList = db.Set(PostId);
const UserList = db.Set(UserId);
const VoteList = db.SetList(UserId, Vote);
+const PostListList = db.List(SavedPostList);
+const UserListList = db.List(SavedUserList);
fn parse_enum(comptime E: type, buf: []const u8, base: u8) !E {
return @enumFromInt(try std.fmt.parseUnsigned(@typeInfo(E).Enum.tag_type, buf, base));
const display_name = try DisplayName.fromSlice(username);
const txn = try env.txn();
- defer txn.commit() catch {};
+ defer txn.commit() catch |err| {
+ std.debug.print("error registering user: {}\n", .{err});
+ };
const users = try Db.users(txn);
const user_ids = try Db.user_ids(txn);
.posts = try PostList.init(txn),
.following = try UserList.init(txn),
.followers = try UserList.init(txn),
+ .post_lists = try PostListList.init(txn),
+ .feeds = try UserListList.init(txn),
});
try user_ids.put(username_array, user_id);
defer txn.commit() catch {};
var posts_view = try post_list.open(txn);
- try posts_view.append(post_id, {});
+ try posts_view.append(post_id);
}
if (quote_id != null) {
const posts = try Db.posts(txn);
const quote_post = try posts.get(quote_id.?);
var quotes = try quote_post.quotes.open(txn);
- try quotes.append(post_id, {});
+ try quotes.append(post_id);
}
}
try user_following.del(user_id_to_follow);
try user_to_follow_followers.del(user_id);
} else if (!(user_following.has(user_id_to_follow) catch true) and !(user_to_follow_followers.has(user_id) catch true)) {
- try user_following.append(user_id_to_follow, {});
- try user_to_follow_followers.append(user_id, {});
+ try user_following.append(user_id_to_follow);
+ try user_to_follow_followers.append(user_id);
} else {
std.debug.print("Something went wrong when trying to unfollow\n", .{});
}
try res.write(
\\<a href="/post">Post</a><br />
, .{});
- try res.write(
- \\<a href="/edit">Edit</a><br />
- , .{});
try html_form(res, "/logout", .{}, .{
\\type="submit" value="Logout"
});
\\ form {
\\ display: inline-block;
\\ }
- \\ .toggle > form > input:placeholder-shown {
- \\ display: none;
- \\ }
- \\ .toggle:hover > form > input {
- \\ display: inline-block;
- \\ }
- \\ .toggle > form > input:placeholder-shown + input {
- \\ display: none;
- \\ }
- \\ .toggle:hover > form > input + input {
- \\ display: inline-block;
- \\ }
+ // \\ .toggle > form > input:placeholder-shown {
+ // \\ display: none;
+ // \\ }
+ // \\ .toggle:hover > form > input {
+ // \\ display: inline-block;
+ // \\ }
+ // \\ .toggle > form > input:placeholder-shown + input {
+ // \\ display: none;
+ // \\ }
+ // \\ .toggle:hover > form > input + input {
+ // \\ display: inline-block;
+ // \\ }
\\</style>
\\</head>
\\<body>
fn write_end(res: *http.Response) !void {
try res.write("</body></html>", .{});
}
-fn write_post(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_id: PostId, recurse: enum { No, Once, Yes }) !void {
+fn write_post(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_id: PostId, options: struct { recurse: u8 = 0, show_comment_field: bool = false }) !void {
const posts = try Db.posts(txn);
const post = posts.get(post_id) catch {
res.redirect("/") catch {};
\\<span>{s}</span><br />
, .{ user.name.constSlice(), user.display_name.constSlice(), time_str(post.time).constSlice(), post.text.constSlice() });
- if (recurse != .No and post.quote_id != null) {
+ if (post.quote_id) |quote_id| {
try res.write("<div style=\"border: 1px solid black;\">", .{});
- try write_post(res, txn, logged_in, post.quote_id.?, .No);
+ if (options.recurse > 0) {
+ try write_post(res, txn, logged_in, quote_id, .{ .recurse = options.recurse - 1 });
+ } else {
+ try res.write("<a href=\"/post/{x}\">...</a>", .{@intFromEnum(quote_id)});
+ }
try res.write("</div>", .{});
}
// Comment Count
try res.write(
- \\<span class="toggle">
\\<a href="/post/{x}">💭 {}</a>
, .{ @intFromEnum(post.id), comments_view.len() });
- if (logged_in != null) {
- try html_form(res, "/comment", .{}, .{
- .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
- "type=\"text\" name=\"text\" placeholder=\"Text\"",
- "type=\"submit\" value=\"Comment\"",
- });
- }
- try res.write(
- \\</span>
- , .{});
// Quote
try res.write(
- \\<span class="toggle">
\\<a href="/quotes/{x}">🔁 {}</a>
, .{ @intFromEnum(post.id), quotes_view.len() });
- try html_form(res, "/quote", .{}, .{
- .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
- "type=\"text\" name=\"text\" placeholder=\"Text\"",
- "type=\"submit\" value=\"Quote\"",
- });
try res.write(
- \\</span>
\\<br />
, .{});
+ // Save to List
+ if (logged_in) |login| {
+ const lists_view = try login.user.post_lists.open(txn);
+ try res.write("<form action=\"/list_add\" method=\"post\">", .{});
+ try res.write("<select name=\"list_id\">", .{});
+ var it = lists_view.iterator();
+ while (it.next()) |kv| {
+ const name = kv.val.name;
+ const id = kv.val.list.idx.?;
+ try res.write("<option value=\"{x}\">{s}</option>", .{ id, name.constSlice() });
+ }
+ try res.write("</select>", .{});
+ try res.write("<input type=\"hidden\" name=\"post_id\" value=\"{x}\"></input>", .{@intFromEnum(post_id)});
+ try res.write("<input type=\"submit\" value=\"Save\"></input>", .{});
+ try res.write("</form>", .{});
+ }
+
+ // Comment field
+ // TODO: maybe always show comment field and prompt for login
+ if (options.show_comment_field and logged_in != null) {
+ try res.write("<br />", .{});
+ try html_form(res, "/comment", .{}, .{
+ .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
+ "type=\"text\" name=\"text\" placeholder=\"Text\"",
+ "type=\"submit\" value=\"Comment\"",
+ });
+ try res.write("<br />", .{});
+ }
+
// Comments
- if (recurse != .No and comments_view.len() > 0) {
+ if (options.recurse > 0 and comments_view.len() > 0) {
try res.write(
- \\<details>
+ \\<details{s}>
\\<summary>Comments</summary>
- , .{});
+ , .{if (options.recurse > 1) " open" else ""});
try res.write("<div style=\"margin: 10px;\">", .{});
var it = comments_view.iterator();
var count: u8 = 0;
- while (it.next()) |kv| {
- try write_post(res, txn, logged_in, kv.key, switch (recurse) {
- .Yes => .Yes,
- .Once => .No,
- else => unreachable,
- });
+ while (it.next()) |comment_id| {
+ try write_post(res, txn, logged_in, comment_id, .{ .recurse = options.recurse - 1 });
try res.write("<br />", .{});
- if (recurse == .Once) {
+ if (options.recurse == 1) {
count += 1;
if (count >= 3) break;
}
\\</div>
, .{});
}
-fn write_posts(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user: User) !void {
- const posts_view = try user.posts.open(txn);
+fn write_posts(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_list: PostList) !void {
+ const posts_view = try post_list.open(txn);
var it = posts_view.reverse_iterator();
- while (it.next()) |kv| {
- const post_id = kv.key;
-
- try write_post(res, txn, logged_in, post_id, .Once);
+ while (it.next()) |post_id| {
+ try write_post(res, txn, logged_in, post_id, .{ .recurse = 1 });
try res.write("<br />", .{});
}
}
-fn write_timeline(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user: User) !void {
+fn write_timeline(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user_list: UserList) !void {
const users = try Db.users(txn);
const posts = try Db.posts(txn);
var newest_post_ids = try std.BoundedArray(PostId, 10).init(0); // TODO: TimelinePostsCount
var prev_newest_post: ?Post = null;
- const following = try user.following.open(txn);
+ const following = try user_list.open(txn);
while (true) {
var newest_post: ?Post = null;
var it = following.iterator();
- while (it.next()) |kv| {
- const followed_user = try users.get(kv.key);
+ while (it.next()) |following_id| {
+ const followed_user = try users.get(following_id);
const followed_posts = try followed_user.posts.open(txn);
if (followed_posts.len() == 0) {
}
var followed_posts_it = followed_posts.reverse_iterator();
- while (followed_posts_it.next()) |followed_post_kv| {
- const last_post = try posts.get(followed_post_kv.key);
+ while (followed_posts_it.next()) |followed_post_id| {
+ const last_post = try posts.get(followed_post_id);
if ((prev_newest_post == null or last_post.time < prev_newest_post.?.time) and (newest_post == null or newest_post.?.time < last_post.time)) {
newest_post = last_post;
}
for (newest_post_ids.constSlice()) |post_id| {
- try write_post(res, txn, logged_in, post_id, .Once);
+ try write_post(res, txn, logged_in, post_id, .{ .recurse = 1 });
try res.write("<br />", .{});
}
}
const followers = try user.followers.open(txn);
try res.write(
- \\<a href="/user/{s}">{s}</a>
+ \\<h2 style="display: inline;"><a href="/user/{s}">{s}</a></h2>
, .{
user.name.constSlice(), user.display_name.constSlice(),
});
try res.write(
\\ <a href="/following/{s}">{} following</a>
\\ <a href="/followers/{s}">{} followers</a>
+ \\<br />
, .{
user.name.constSlice(), following.len(),
user.name.constSlice(), followers.len(),
});
- try write_posts(&res, txn, logged_in, user);
+ if (logged_in != null and user_id == logged_in.?.user.id) {
+ try res.write(
+ \\<a href="/lists">Lists</a>
+ \\<a href="/feeds">Feeds</a>
+ \\<a href="/edit">Edit</a><br />
+ \\<br />
+ , .{});
+ }
+
+ try res.write("<br />", .{});
+
+ try write_posts(&res, txn, logged_in, user.posts);
} else |err| {
try res.write(
\\<p>User not found [{}]</p>
\\<h2><a href="/user/{s}">{s}</a> follows:</h2>
, .{ user.name.constSlice(), user.display_name.constSlice() });
- while (it.next()) |kv| {
- const following_user = try users.get(kv.key);
+ while (it.next()) |following_id| {
+ const following_user = try users.get(following_id);
try res.write(
\\<a href="/user/{s}">{s}</a><br />
\\<h2><a href="/user/{s}">{s}</a> followers:</h2>
, .{ user.name.constSlice(), user.display_name.constSlice() });
- while (it.next()) |kv| {
- const follower_user = try users.get(kv.key);
+ while (it.next()) |follower_id| {
+ const follower_user = try users.get(follower_id);
try res.write(
\\<a href="/user/{s}">{s}</a><br />
const post_id_str = req.target[6..req.target.len];
const post_id = try parse_enum(PostId, post_id_str, 16);
- try write_post(&res, txn, logged_in, post_id, .Yes);
+ try write_post(&res, txn, logged_in, post_id, .{
+ .recurse = 3, // TODO: factor out
+ .show_comment_field = true,
+ });
} else if (std.mem.startsWith(u8, req.target, "/quotes/")) {
const post_id_str = req.target[8..req.target.len];
const post_id = try parse_enum(PostId, post_id_str, 16);
const posts = try Db.posts(txn);
const post = try posts.get(post_id);
+ const referer = if (req.get_header("Referer")) |ref| ref else req.target;
+
+ try html_form(&res, "/quote", .{}, .{
+ .{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} },
+ .{ "type=\"hidden\" name=\"post_id\" value=\"{x}\"", .{@intFromEnum(post.id)} },
+ "type=\"text\" name=\"text\" placeholder=\"Text\"",
+ "type=\"submit\" value=\"Quote\"",
+ });
+ try res.write("<br />", .{});
+
const quotes_view = try post.quotes.open(txn);
var it = quotes_view.iterator();
- while (it.next()) |kv| {
- try write_post(&res, txn, logged_in, kv.key, .Once);
+ while (it.next()) |quote_id| {
+ try write_post(&res, txn, logged_in, quote_id, .{ .recurse = 1 });
try res.write("<br />", .{});
}
+ } else if (std.mem.startsWith(u8, req.target, "/list/")) {
+ const list_id_str = req.target[6..req.target.len];
+ const list_id = try std.fmt.parseUnsigned(PostListList.Index, list_id_str, 16);
+
+ try write_posts(&res, txn, logged_in, PostList{ .idx = list_id });
+ } else if (std.mem.eql(u8, req.target, "/lists")) {
+ if (logged_in) |login| {
+ const post_lists_view = try login.user.post_lists.open(txn);
+
+ try html_form(&res, "/new_list", .{}, .{
+ "type=\"text\" name=\"name\"",
+ "type=\"submit\" value=\"Add\"",
+ });
+
+ try res.write("<br /><br />", .{});
+
+ var it = post_lists_view.iterator();
+ while (it.next()) |kv| {
+ const name = kv.val.name;
+ const post_list = kv.val.list;
+ try res.write(
+ \\<a href="/list/{x}">{s}</a><br />
+ , .{ post_list.idx.?, name.constSlice() });
+ }
+ } else {
+ try res.write("not logged in", .{});
+ }
} else if (std.mem.eql(u8, req.target, "/post")) {
if (logged_in) |login| {
_ = login;
- const referer = if (req.get_header("Referer")) |ref| ref else "/post";
+ const referer = if (req.get_header("Referer")) |ref| ref else req.target;
try html_form(&res, "/post", .{}, .{
.{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} },
}
} else if (std.mem.eql(u8, req.target, "/")) {
if (logged_in) |login| {
- try write_timeline(&res, txn, logged_in, login.user);
+ try write_timeline(&res, txn, logged_in, login.user.following);
} else {
// TODO: generic home
try res.write("Homepage", .{});
if (logged_in) |login| {
const text = req.get_value("text").?;
const has_referer = req.get_value("referer");
+
try Chirp.post(env, login.user.id, text);
+
if (has_referer) |r| {
const decoded = try decode(r);
try res.redirect(decoded.constSlice());
if (logged_in) |login| {
const text = req.get_value("text") orelse return error.NoText;
const post_id_str = req.get_value("post_id") orelse return error.NoPostId;
+ const has_referer = req.get_value("referer");
+
const post_id = try parse_enum(PostId, post_id_str, 16);
try Chirp.quote(env, login.user.id, post_id, text);
+
+ if (has_referer) |r| {
+ const decoded = try decode(r);
+ try res.redirect(decoded.constSlice());
+ }
+ }
+ } else if (std.mem.eql(u8, req.target, "/new_list")) {
+ if (logged_in) |login| {
+ const name_str = req.get_value("name") orelse return error.NoName;
+ const name = try Name.fromSlice(name_str);
+
+ var txn = try env.txn();
+
+ const postlist = try PostList.init(txn);
+ try txn.commit();
+
+ txn = try env.txn();
+ var post_lists_view = try login.user.post_lists.open(txn);
+ _ = try post_lists_view.append(.{ .name = name, .list = postlist });
+ try txn.commit();
+ }
+ } else if (std.mem.eql(u8, req.target, "/list_add")) {
+ if (logged_in) |login| {
+ _ = login;
+
+ const list_id_str = req.get_value("list_id") orelse return error.NoListId;
+ const post_id_str = req.get_value("post_id") orelse return error.NoPostId;
+ const list_id = try std.fmt.parseUnsigned(PostList.Index, list_id_str, 16);
+ const post_id = try parse_enum(PostId, post_id_str, 16);
+
+ const txn = try env.txn();
+ defer txn.commit() catch {};
+
+ const post_list = PostList{ .idx = list_id };
+ var post_list_view = try post_list.open(txn);
+ std.debug.print("adding {x} to {x}\n", .{ post_id, list_id });
+ if (try post_list_view.has(post_id)) {
+ try post_list_view.del(post_id);
+ } else {
+ try post_list_view.append(post_id);
+ }
}
} else if (std.mem.eql(u8, req.target, "/upvote")) {
const login = logged_in orelse return error.NotLoggedIn;