Allow scrolling on index page
This commit is contained in:
parent
652e673d39
commit
76c462cb60
|
@ -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")
|
||||||
|
}
|
||||||
|
})
|
|
@ -90,6 +90,11 @@ domain * {
|
||||||
methods get
|
methods get
|
||||||
}
|
}
|
||||||
|
|
||||||
|
route /_js/index_scroll.js {
|
||||||
|
handler asset_serve_index_scroll_js
|
||||||
|
methods get
|
||||||
|
}
|
||||||
|
|
||||||
route /favicon.ico {
|
route /favicon.ico {
|
||||||
handler asset_serve_favicon_ico
|
handler asset_serve_favicon_ico
|
||||||
methods get
|
methods get
|
||||||
|
|
|
@ -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)
|
||||||
|
]]
|
||||||
|
|
||||||
|
|
|
@ -3,16 +3,25 @@ local sql = require("lsqlite3")
|
||||||
local db = require("db")
|
local db = require("db")
|
||||||
local queries = require("queries")
|
local queries = require("queries")
|
||||||
local util = require("util")
|
local util = require("util")
|
||||||
|
local tags = require("tags")
|
||||||
|
require("global")
|
||||||
|
|
||||||
local stmnt_tags_get
|
local stmnt_tags_get, stmnt_stories_get
|
||||||
|
|
||||||
local oldconfigure = configure
|
local oldconfigure = configure
|
||||||
function configure(...)
|
function configure(...)
|
||||||
stmnt_tags_get = db.sqlassert(db.conn:prepare(queries.select_suggest_tags))
|
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(...)
|
return oldconfigure(...)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function suggest_tags(req,data)
|
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{
|
stmnt_tags_get:bind_names{
|
||||||
match = data .. "%"
|
match = data .. "%"
|
||||||
}
|
}
|
||||||
|
@ -25,19 +34,93 @@ local function suggest_tags(req,data)
|
||||||
http_response(req,200,table.concat(tags,";"))
|
http_response(req,200,table.concat(tags,";"))
|
||||||
end
|
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)
|
local function api_get(req)
|
||||||
http_request_populate_qs(req)
|
http_request_populate_qs(req)
|
||||||
local call = assert(http_argument_get_string(req,"call"))
|
local call = assert(http_argument_get_string(req,"call"))
|
||||||
local data = assert(http_argument_get_string(req,"data"))
|
local data = assert(http_argument_get_string(req,"data"))
|
||||||
local body
|
assertf(api_points[call], "Unknown api endpoint: %s", call)
|
||||||
if call == "suggest" then
|
api_points[call](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))
|
|
||||||
return suggest_tags(req,data)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
return api_get
|
return api_get
|
||||||
|
|
|
@ -7,3 +7,49 @@ function assertf(bool, fmt, ...)
|
||||||
error(string.format(fmt,...),2)
|
error(string.format(fmt,...),2)
|
||||||
end
|
end
|
||||||
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
|
||||||
|
|
|
@ -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 = {}
|
local api = {}
|
||||||
|
|
||||||
-- Called before any request processing. Returning true "traps" the request, and
|
-- 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
|
api.pre_request = function(req) end
|
||||||
|
|
||||||
-- Called after smr request processing. Returning true "traps" the request, and
|
-- Called after smr request processing. Returning true "traps" the request.
|
||||||
-- does not continue calling any other addon logic. This will not stop smr from
|
-- 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
|
-- responding to the request, since by this time http_request_response() has
|
||||||
-- already been called.
|
-- already been called.
|
||||||
api.post_request = function(req) end
|
api.post_request = function(req) end
|
||||||
|
|
||||||
-- Called during startup of the worker process
|
-- 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.
|
-- Return value is ignored.
|
||||||
api.worker_init = function() end
|
api.worker_init = function() end
|
||||||
|
|
||||||
|
@ -42,4 +45,7 @@ api.get = {
|
||||||
page_reader = function(env) return {} end,
|
page_reader = function(env) return {} end,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Called when the /_api endpoint is accessed
|
||||||
|
api.call = function() end
|
||||||
|
|
||||||
return api
|
return api
|
||||||
|
|
|
@ -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)
|
for more info (https://github.com/leafo/etlua)
|
||||||
]]
|
]]
|
||||||
local et = require("etlua")
|
local et = require("etlua")
|
||||||
|
require("global")
|
||||||
local pagenames = {
|
local pagenames = {
|
||||||
"index",
|
"index",
|
||||||
"author_index",
|
"author_index",
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
<% if #stories == 0 then %>
|
<% if #stories == 0 then %>
|
||||||
No stories available.
|
No stories available.
|
||||||
<% else %>
|
<% else %>
|
||||||
<table>
|
<table id="story_list">
|
||||||
<% for k,story in pairs(stories) do %>
|
<% for k,story in pairs(stories) do %>
|
||||||
<{system cat src/pages/parts/story_breif.etlua}>
|
<{system cat src/pages/parts/story_breif.etlua}>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
Loading…
Reference in New Issue