Allow scrolling on index page

This commit is contained in:
Robin Malley 2023-03-12 03:31:08 +00:00
parent 652e673d39
commit 76c462cb60
8 changed files with 288 additions and 17 deletions

107
assets/index_scroll.js Normal file
View File

@ -0,0 +1,107 @@
/*
Allows for lazy loading of stories on the main page as the user scrolls down
*/
function add_stories(stories){
var tbody_el = document.querySelector("table#story_list tbody")
for(var i = 0; i < stories.length; i++){
var story = stories[i];
/* This chunk should match /src/pages/parts/story_brief */
console.log("Adding story:",story)
var row = document.createElement("tr");
row.appendChild(
document.createElement("td")
); // unlisted cell
var link_cell = document.createElement("td");
var link = document.createElement("a");
link.textContent = story.title;
link.href = story.url;
link_cell.appendChild(link);
row.appendChild(link_cell);
var author_cell = document.createElement("td");
author_cell.appendChild(
document.createTextNode("By ")
);
if(story.isanon){
author_cell.appendChild(
document.createTextNode("Anonymous")
);
}else{
var author_page = document.createElement("a");
author_page.textContent = story.author;
author_page.href = story.author; // TODO: fix
author_cell.appendChild(author_page);
}
row.appendChild(author_cell);
var hits_cell = document.createElement("td")
hits_cell.appendChild(
document.createTextNode(story.hits + " hits")
);
row.appendChild(hits_cell);
var comments_cell = document.createElement("td");
comments_cell.appendChild(
document.createTextNode(story.ncomments + " comments")
);
row.appendChild(comments_cell);
var tag_cell = document.createElement("td");
var tag_list = document.createElement("ul");
tag_list.className = "row tag-list";
tag_cell.appendChild(tag_list);
for(var j = 0; j < Math.min(story.tags.length,5); j++){
var tag = story.tags[j];
var tag_item = document.createElement("li");
var tag_button = document.createElement("a");
tag_button.className = "tag button button-outline";
tag_button.textContent = tag;
tag_button.href = "/_search?q=%2B" + tag;
tag_item.appendChild(tag_button);
tag_list.appendChild(tag_item);
}
row.appendChild(tag_cell);
var date_cell = document.createElement("td");
date_cell.appendChild(
document.createTextNode(story.posted)
);
row.appendChild(date_cell);
tbody_el.appendChild(row);
}
}
/*
A tiny state machine:
0 - idle
1 - loading more stories (do not send another request)
2 - stories loaded, waiting for next scroll event to transition to idle
*/
var state = 0
var loaded = 50 // by default we load 50 stories on the site index
document.addEventListener("scroll",function(e){
var tobot = window.scrollMaxY - window.scrollY
if (tobot < 100){
if (state == 0){
//Ask the server for stories
// TODO: Finish this
var xhr = new XMLHttpRequest();
xhr.open("GET", "/_api?call=stories&data=" + loaded);
xhr.onreadystatechange = function(e){
if(xhr.readyState === 4){
console.log("response:",xhr.response)
resp = JSON.parse(xhr.response);
console.log("resp:",resp)
add_stories(resp.stories);
loaded += resp.stories.length;
}
state = 2
}
xhr.send()
state = 1
}else if (state == 1){
// Do nothing
}else if (state == 2){
state = 0
}
console.log("we should load more stories")
}
})

View File

@ -89,6 +89,11 @@ domain * {
handler asset_serve_intervine_deletion_js
methods get
}
route /_js/index_scroll.js {
handler asset_serve_index_scroll_js
methods get
}
route /favicon.ico {
handler asset_serve_favicon_ico

23
src/lua/addon.lua Normal file
View File

@ -0,0 +1,23 @@
--[[
Addon loader - Addons are either:
* A folder with at least two files:
- meta.lua - contains load order information
- init.lua - entrypoint that gets run to load the addon
* A zip file with the same
* A sqlite3 database with a table "files" that has at least the columns
* name :: TEXT - The filename
* data :: BINARY - The file data
And there are at least 2 rows with filenames `meta.lua` and `init.lua`
as described above. Addons should be placed in $SMR_ADDONS, defined in config.lua
The `meta.lua` file is run at worker init time (i.e. it will be run once for
each worker), and should return a table with at least the following information
{
name :: string - A name for the addon (all addons must have unique names)
desc :: string - A description for the addon.
entry :: table[number -> string] - Describes the load order for this
addon. Each addon's meta.lua is run, and sorted to get a load
order for entrypoints (the strings are the names files)
]]

View File

@ -3,16 +3,25 @@ local sql = require("lsqlite3")
local db = require("db")
local queries = require("queries")
local util = require("util")
local tags = require("tags")
require("global")
local stmnt_tags_get
local stmnt_tags_get, stmnt_stories_get
local oldconfigure = configure
function configure(...)
stmnt_tags_get = db.sqlassert(db.conn:prepare(queries.select_suggest_tags))
stmnt_stories_get = db.sqlassert(db.conn:prepare(queries.select_site_index))
return oldconfigure(...)
end
local function suggest_tags(req,data)
--[[
Prevent a malicious user from injecting '%' into the string
we're searching for, potentially causing a DoS with a
sufficiently backtrack-ey search/tag combination.
]]
assert(data:match("^[a-zA-Z0-9,%s-]+$"),string.format("Bad characters in tag: %q",data))
stmnt_tags_get:bind_names{
match = data .. "%"
}
@ -25,19 +34,93 @@ local function suggest_tags(req,data)
http_response(req,200,table.concat(tags,";"))
end
--[[
A poor mans json builder, since I don't need one big enough to pull in a
dependency for it (yet)
]]
local function poor_json(builder, ltbl)
local function write_bool(builder,bool)
table.insert(builder,bool and "true" or "false")
end
local function write_number(builder,num)
local number
if num % 1 == 0 then
num = string.format("%d",num)
else
num = string.format("%f",num)
end
table.insert(builder,num)
end
local function write_string(builder,s)
table.insert(builder, string.format("%q",s))
end
local function write_array(builder,tbl)
table.insert(builder,"[")
for _,item in ipairs(tbl) do
write_string(builder,item)
table.insert(builder,",")
end
if #tbl > 0 then
table.remove(builder,#builder) -- Remove the last comma
end
table.insert(builder,"]")
end
local lua_to_json = {
boolean = write_bool,
number = write_number,
string = write_string,
table = write_array
}
table.insert(builder,"{")
for k,v in pairs(ltbl) do
assert(type(k) == "string", "Field was not a string, was: " .. type(k))
table.insert(builder,string.format("%q",k))
table.insert(builder,":")
assert(lua_to_json[type(v)], "Unknown type for json:" .. type(v) .. " at " .. k)
lua_to_json[type(v)](builder,v)
table.insert(builder,",")
end
table.remove(builder,#builder) -- Remove the last comma before closing object
table.insert(builder,"}")
table.insert(builder,",") -- Can't do this on the same line as above
-- we need to remove the last comma, but not }
end
local function get_stories(req,data)
local nstories = tonumber(data)
stmnt_stories_get:bind_names{offset=nstories}
local builder = setmetatable({'{"stories":['},table)
for id, title, anon, time, author, hits, ncomments in db.sql_rows(stmnt_stories_get) do
local story = {
url = util.encode_id(id),
title = title,
isanon = tonumber(anon) == 1,
posted = os.date("%B %d %Y",tonumber(time)),
author = author,
tags = tags.get(id),
hits = hits,
ncomments = ncomments
}
poor_json(builder,story)
end
table.remove(builder,#builder) -- Remove last comma before closing list
table.insert(builder,"]}")
stmnt_stories_get:reset()
http_response_header(req,"Content-Type","text/plain")
http_response(req,200,table.concat(builder))
end
local api_points = {}
local function register_api(call,func)
api_points[call] = func
end
register_api("suggest",suggest_tags)
register_api("stories",get_stories)
local function api_get(req)
http_request_populate_qs(req)
local call = assert(http_argument_get_string(req,"call"))
local data = assert(http_argument_get_string(req,"data"))
local body
if call == "suggest" then
--[[
Prevent a malicious user from injecting '%' into the string
we're searching for, potentially causing a DoS with a
sufficiently backtrack-ey search/tag combination.
]]
assert(data:match("^[a-zA-Z0-9,%s-]+$"),string.format("Bad characters in tag: %q",data))
return suggest_tags(req,data)
end
assertf(api_points[call], "Unknown api endpoint: %s", call)
api_points[call](req,data)
end
return api_get

View File

@ -7,3 +7,49 @@ function assertf(bool, fmt, ...)
error(string.format(fmt,...),2)
end
end
function errorf(str, ...)
--try calling string.format, if it errors, try calling it with exactly
--1 less argument (the error level)
local args = {...}
local succ, ret = pcall(string.format,str,...)
if not succ and type(args[#args]) ~= "number" then
errorf("Failed displaying error that looks like %q",str)
elseif type(args[#args]) == "number" then
local errlevel = table.remove(args,#args)
local succ2, ret = pcall(string.format,str,unpack(args))
if not succ2 then
errorf("Failed displaying error that looks like %q",str)
end
error(ret, errlevel+1)
end
error(ret,2)
end
local oldtostring = tostring
function tostring(any)
--Pretty print tables by default
local printed_tables = {}
local function tostring_helper(a,tabs)
if type(a) ~= "table" then
return oldtostring(a)
end
if printed_tables[a] then
return oldtostring(a)
end
printed_tables[a] = true
local sbuilder = {"{\n"}
for k,v in pairs(a) do
table.insert(sbuilder,string.rep("\t",tabs))
table.insert(sbuilder,tostring_helper(k,tabs+1))
table.insert(sbuilder,":")
table.insert(sbuilder,tostring_helper(v,tabs+1))
table.insert(sbuilder,"\n")
end
table.insert(sbuilder,string.rep("\t",tabs-1))
table.insert(sbuilder,"}")
return table.concat(sbuilder)
end
return tostring_helper(any,1)
end

View File

@ -1,21 +1,24 @@
--[[
Global functions that smr exposes that can be overriden by addons
Global functions that smr exposes that can be detoured by addons
]]
local api = {}
-- Called before any request processing. Returning true "traps" the request, and
-- does not continue calling smr logic, or logic of any other addons.
-- does not continue calling smr logic. Well-behaved addons should check for
-- "true" from the detoured function, and return true immediately if the check
-- succeeds.
api.pre_request = function(req) end
-- Called after smr request processing. Returning true "traps" the request, and
-- does not continue calling any other addon logic. This will not stop smr from
-- Called after smr request processing. Returning true "traps" the request.
-- Well-behaved addons should check for true from the detoured function, and
-- immediately return true if the check succeeds. This will not stop smr from
-- responding to the request, since by this time http_request_response() has
-- already been called.
api.post_request = function(req) end
-- Called during startup of the worker process
-- Failures in this function will prevent kore from starting.
-- calling error() in this function will prevent kore from starting.
-- Return value is ignored.
api.worker_init = function() end
@ -42,4 +45,7 @@ api.get = {
page_reader = function(env) return {} end,
}
-- Called when the /_api endpoint is accessed
api.call = function() end
return api

View File

@ -3,6 +3,7 @@ Compiles all the pages under src/pages/ with etlua. See the etlua documentation
for more info (https://github.com/leafo/etlua)
]]
local et = require("etlua")
require("global")
local pagenames = {
"index",
"author_index",

View File

@ -28,7 +28,7 @@
<% if #stories == 0 then %>
No stories available.
<% else %>
<table>
<table id="story_list">
<% for k,story in pairs(stories) do %>
<{system cat src/pages/parts/story_breif.etlua}>
<% end %>