Compare commits
9 commits
0348fed18d
...
098ea88f5d
Author | SHA1 | Date | |
---|---|---|---|
|
098ea88f5d | ||
72b42e7b26 | |||
f4b368ea3e | |||
6da9f41519 | |||
df47c8a88a | |||
5ab182e615 | |||
08a0990bc8 | |||
c9dffc9d4a | |||
6227f6fa84 |
10 changed files with 393 additions and 238 deletions
67
build.js
67
build.js
|
@ -9,7 +9,8 @@ const babel = require("@babel/core")
|
|||
const fetch = require("node-fetch")
|
||||
const chalk = require("chalk")
|
||||
const hint = require("jshint").JSHINT
|
||||
const browserify = require('browserify')
|
||||
const browserify = require("browserify")
|
||||
const {Transform} = require("stream")
|
||||
|
||||
process.chdir(pj(__dirname, "src"))
|
||||
|
||||
|
@ -17,10 +18,10 @@ const buildDir = "../build"
|
|||
|
||||
const validationQueue = []
|
||||
const validationHost = os.hostname() === "future" ? "http://localhost:8888/" : "http://validator.w3.org/nu/"
|
||||
const static_files = new Map()
|
||||
const staticFiles = new Map()
|
||||
const links = new Map()
|
||||
const sources = new Map()
|
||||
const pugLocals = {static: static_files, links}
|
||||
const pugLocals = {static: staticFiles, links}
|
||||
|
||||
const spec = require("./spec.js")
|
||||
|
||||
|
@ -95,6 +96,7 @@ function runHint(filename, source) {
|
|||
globals: ["console", "URLSearchParams", "staticFiles"],
|
||||
browser: true,
|
||||
asi: true,
|
||||
node: true
|
||||
})
|
||||
const result = hint.data()
|
||||
let problems = 0
|
||||
|
@ -127,33 +129,52 @@ function runHint(filename, source) {
|
|||
|
||||
async function addFile(sourcePath, targetPath) {
|
||||
const contents = await fs.promises.readFile(pj(".", sourcePath), {encoding: null});
|
||||
static_files.set(sourcePath, `${targetPath}?static=${hash(contents)}`)
|
||||
staticFiles.set(sourcePath, `${targetPath}?static=${hash(contents)}`)
|
||||
await fs.promises.writeFile(pj(buildDir, targetPath), contents)
|
||||
}
|
||||
|
||||
async function loadJS(sourcePath, targetPath) {
|
||||
let content = await fs.promises.readFile(pj(".", sourcePath), {encoding: "utf8"})
|
||||
sources.set(sourcePath, content);
|
||||
static_files.set(sourcePath, `${targetPath}?static=${hash(content)}`)
|
||||
sources.set(sourcePath, content)
|
||||
staticFiles.set(sourcePath, `${targetPath}?static=${hash(content)}`)
|
||||
}
|
||||
|
||||
async function addJS(sourcePath, targetPath) {
|
||||
let content = sources.get(sourcePath)
|
||||
// resolve imports to hashed paths
|
||||
content = content.replace(/\$to_relative "([^"]+)"/g, function(_, file) {
|
||||
if (!static_files.get(file)) throw new Error(`Tried to relative import ${file} from ${sourcePath}, but import not found`)
|
||||
return '"' + getRelative(targetPath, static_files.get(file)) + '"'
|
||||
})
|
||||
runHint(sourcePath, content)
|
||||
await fs.promises.writeFile(pj(buildDir, targetPath), content)
|
||||
}
|
||||
|
||||
async function addBundle(sourcePath, targetPath) {
|
||||
await browserify()
|
||||
.add(pj(".", sourcePath))
|
||||
.bundle()
|
||||
.pipe(fs.createWriteStream(pj(buildDir, targetPath)));
|
||||
static_files.set(sourcePath, targetPath)
|
||||
const content = await new Promise(resolve => {
|
||||
browserify()
|
||||
.add(pj(".", sourcePath))
|
||||
.transform(file => {
|
||||
let content = ""
|
||||
const transform = new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
content += chunk.toString()
|
||||
callback(null, chunk)
|
||||
}
|
||||
})
|
||||
transform.on("finish", () => {
|
||||
const relativePath = path.relative(process.cwd(), file).replace(/^\/*/, "/")
|
||||
runHint(relativePath, content)
|
||||
})
|
||||
return transform
|
||||
})
|
||||
.bundle((err, res) => {
|
||||
if (err) {
|
||||
delete err.stream
|
||||
throw err // Quit; problem parsing file to bundle
|
||||
}
|
||||
resolve(res)
|
||||
})
|
||||
})
|
||||
const writer = fs.promises.writeFile(pj(buildDir, targetPath), content)
|
||||
staticFiles.set(sourcePath, `${targetPath}?static=${hash(content)}`)
|
||||
runHint(sourcePath, content)
|
||||
await writer
|
||||
}
|
||||
|
||||
async function addSass(sourcePath, targetPath) {
|
||||
|
@ -167,7 +188,7 @@ async function addSass(sourcePath, targetPath) {
|
|||
if (!(name instanceof sass.types.String)) {
|
||||
throw "$name: expected a string"
|
||||
}
|
||||
const result = getRelative(targetPath, static_files.get(name.getValue()))
|
||||
const result = getRelative(targetPath, staticFiles.get(name.getValue()))
|
||||
if (typeof result === "string") {
|
||||
return new sass.types.String(result)
|
||||
} else {
|
||||
|
@ -176,8 +197,8 @@ async function addSass(sourcePath, targetPath) {
|
|||
}
|
||||
}
|
||||
}).css;
|
||||
static_files.set(sourcePath, `${targetPath}?static=${hash(renderedCSS)}`)
|
||||
await validate(sourcePath, renderedCSS, "css")
|
||||
staticFiles.set(sourcePath, `${targetPath}?static=${hash(renderedCSS)}`)
|
||||
validate(sourcePath, renderedCSS, "css")
|
||||
await fs.promises.writeFile(pj(buildDir, targetPath), renderedCSS)
|
||||
}
|
||||
|
||||
|
@ -186,17 +207,17 @@ async function addPug(sourcePath, targetPath) {
|
|||
return getRelative(targetPath, staticTarget)
|
||||
}
|
||||
function getStatic(target) {
|
||||
return getRelativeHere(static_files.get(target))
|
||||
return getRelativeHere(staticFiles.get(target))
|
||||
}
|
||||
function getStaticName(target) {
|
||||
return getRelativeHere(static_files.get(target)).replace(/\?.*$/, "")
|
||||
return getRelativeHere(staticFiles.get(target)).replace(/\?.*$/, "")
|
||||
}
|
||||
function getLink(target) {
|
||||
return getRelativeHere(links.get(target))
|
||||
}
|
||||
const renderedHTML = pug.compileFile(pj(".", sourcePath), {pretty: true})({getStatic, getStaticName, getLink, ...pugLocals})
|
||||
let renderedWithoutPHP = renderedHTML.replace(/<\?(?:php|=).*?\?>/gsm, "")
|
||||
await validate(sourcePath, renderedWithoutPHP, "html")
|
||||
validate(sourcePath, renderedWithoutPHP, "html")
|
||||
await fs.promises.writeFile(pj(buildDir, targetPath), renderedHTML)
|
||||
}
|
||||
|
||||
|
@ -224,7 +245,7 @@ async function addBabel(sourcePath, targetPath) {
|
|||
|
||||
const filenameWithQuery = `${targetPath}?static=${hash(compiled.code)}`;
|
||||
|
||||
static_files.set(sourcePath, filenameWithQuery)
|
||||
staticFiles.set(sourcePath, filenameWithQuery)
|
||||
|
||||
await Promise.all([
|
||||
fs.promises.writeFile(pj(buildDir, targetPath), originalCode),
|
||||
|
|
347
package-lock.json
generated
347
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -12,12 +12,12 @@
|
|||
"author": "",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"browserify": "^17.0.0",
|
||||
"dompurify": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.1",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"browserify": "^17.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"http-server": "^0.12.3",
|
||||
"jshint": "^2.12.0",
|
||||
|
|
80
spec.js
80
spec.js
|
@ -19,86 +19,6 @@ module.exports = [
|
|||
source: "/js/main.js",
|
||||
target: "/static/bundle.js"
|
||||
},
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/main.js",
|
||||
// target: "/static/main.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/basic.js",
|
||||
// target: "/static/basic.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/groups.js",
|
||||
// target: "/static/groups.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/chat-input.js",
|
||||
// target: "/static/chat-input.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/room-picker.js",
|
||||
// target: "/static/room-picker.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/store/store.js",
|
||||
// target: "/static/store/store.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/store/subscribable.js",
|
||||
// target: "/static/store/subscribable.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/store/subscribe_value.js",
|
||||
// target: "/static/store/subscribe_value.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/store/subscribe_map_list.js",
|
||||
// target: "/static/store/subscribe_map_list.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/store/subscribe_set.js",
|
||||
// target: "/static/store/subscribe_set.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/sync/sync.js",
|
||||
// target: "/static/sync/sync.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/lsm.js",
|
||||
// target: "/static/lsm.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/timeline.js",
|
||||
// target: "/static/timeline.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/anchor.js",
|
||||
// target: "/static/anchor.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/chat.js",
|
||||
// target: "/static/chat.js",
|
||||
// },
|
||||
// {
|
||||
// type: "js",
|
||||
// source: "/js/functions.js",
|
||||
// target: "/static/functions.js",
|
||||
// },
|
||||
{
|
||||
type: "file",
|
||||
source: "/assets/fonts/whitney-500.woff",
|
||||
|
|
|
@ -27,7 +27,7 @@ class Chat extends ElemJS {
|
|||
// connect to the new room's timeline updater
|
||||
if (store.activeRoom.exists()) {
|
||||
const timeline = store.activeRoom.value().timeline
|
||||
const subscription = () => {
|
||||
const beforeChangeSubscription = () => {
|
||||
// scroll anchor does not work if the timeline is scrolled to the top.
|
||||
// at the start, when there are not enough messages for a full screen, this is the case.
|
||||
// once there are enough messages that scrolling is necessary, we initiate a scroll down to activate the scroll anchor.
|
||||
|
@ -40,12 +40,29 @@ class Chat extends ElemJS {
|
|||
}
|
||||
}, 0)
|
||||
}
|
||||
const name = "beforeChange"
|
||||
this.removableSubscriptions.push({name, target: timeline, subscription})
|
||||
timeline.subscribe(name, subscription)
|
||||
this.addSubscription("beforeChange", timeline, beforeChangeSubscription)
|
||||
|
||||
//Make sure after loading scrollback we don't move the scroll position
|
||||
const beforeScrollbackLoadSubscription = () => {
|
||||
const lastScrollHeight = chatMessages.scrollHeight;
|
||||
|
||||
const afterScrollbackLoadSub = () => {
|
||||
const scrollDiff = chatMessages.scrollHeight - lastScrollHeight;
|
||||
chatMessages.scrollTop += scrollDiff;
|
||||
|
||||
timeline.unsubscribe("afterScrollbackLoad", afterScrollbackLoadSub)
|
||||
}
|
||||
|
||||
timeline.subscribe("afterScrollbackLoad", afterScrollbackLoadSub)
|
||||
}
|
||||
this.addSubscription("beforeScrollbackLoad", timeline, beforeScrollbackLoadSubscription)
|
||||
}
|
||||
this.render()
|
||||
}
|
||||
addSubscription(name, target, subscription) {
|
||||
this.removableSubscriptions.push({name, target, subscription})
|
||||
target.subscribe(name, subscription)
|
||||
}
|
||||
|
||||
render() {
|
||||
this.clearChildren()
|
||||
|
|
|
@ -33,9 +33,9 @@ function eventSearch(list, event, min = 0, max = NO_MAX) {
|
|||
}
|
||||
}
|
||||
// recurse (below)
|
||||
if (list[mid].data.origin_server_ts > event.data.origin_server_ts) return eventSearch(list, event, min, mid-1)
|
||||
if (list[mid].data.origin_server_ts > event.data.origin_server_ts) return eventSearch(list, event, min, mid - 1)
|
||||
// recurse (above)
|
||||
else return eventSearch(list, event, mid+1, max)
|
||||
else return eventSearch(list, event, mid + 1, max)
|
||||
}
|
||||
|
||||
|
||||
|
@ -115,16 +115,43 @@ class EventGroup extends ElemJS {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/** Displays a spinner and creates an event to notify timeline to load more messages */
|
||||
class LoadMore extends ElemJS {
|
||||
constructor(id) {
|
||||
super("div")
|
||||
this.class("c-message-notice")
|
||||
this.id = id
|
||||
|
||||
this.child(
|
||||
ejs("div").class("c-message-notice__inner").child(
|
||||
ejs("span").class("loading-icon"),
|
||||
ejs("span").text("Loading more...")
|
||||
)
|
||||
)
|
||||
const intersection_observer = new IntersectionObserver(e => this.intersectionHandler(e))
|
||||
intersection_observer.observe(this.element)
|
||||
}
|
||||
|
||||
intersectionHandler(e) {
|
||||
if (e.some(e => e.isIntersecting)) {
|
||||
store.rooms.get(this.id).value().timeline.loadScrollback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ReactiveTimeline extends ElemJS {
|
||||
constructor(id, list) {
|
||||
super("div")
|
||||
this.class("c-event-groups")
|
||||
this.id = id
|
||||
this.list = list
|
||||
this.loadMore = new LoadMore(this.id)
|
||||
this.render()
|
||||
}
|
||||
|
||||
addEvent(event) {
|
||||
this.loadMore.remove()
|
||||
// if (debug) console.log("running search", this.list, event)
|
||||
// if (debug) debugger;
|
||||
const search = eventSearch(this.list, event)
|
||||
|
@ -132,7 +159,7 @@ class ReactiveTimeline extends ElemJS {
|
|||
if (!search.success) {
|
||||
if (search.i >= 1) {
|
||||
// add at end
|
||||
this.tryAddGroups(event, [search.i-1, search.i])
|
||||
this.tryAddGroups(event, [search.i - 1, search.i])
|
||||
} else {
|
||||
// add at start
|
||||
this.tryAddGroups(event, [0, -1])
|
||||
|
@ -140,6 +167,8 @@ class ReactiveTimeline extends ElemJS {
|
|||
} else {
|
||||
this.tryAddGroups(event, [search.i])
|
||||
}
|
||||
this.loadMore = new LoadMore(this.id)
|
||||
this.childAt(0, this.loadMore)
|
||||
}
|
||||
|
||||
tryAddGroups(event, indices) {
|
||||
|
@ -172,6 +201,7 @@ class ReactiveTimeline extends ElemJS {
|
|||
|
||||
render() {
|
||||
this.clearChildren()
|
||||
this.child(this.loadMore)
|
||||
this.list.forEach(group => this.child(group))
|
||||
this.anchor = new Anchor()
|
||||
this.child(this.anchor)
|
||||
|
@ -183,11 +213,15 @@ class Timeline extends Subscribable {
|
|||
super()
|
||||
Object.assign(this.events, {
|
||||
beforeChange: [],
|
||||
afterChange: []
|
||||
afterChange: [],
|
||||
beforeScrollbackLoad: [],
|
||||
afterScrollbackLoad: [],
|
||||
})
|
||||
Object.assign(this.eventDeps, {
|
||||
beforeChange: [],
|
||||
afterChange: []
|
||||
afterChange: [],
|
||||
beforeScrollbackLoad: [],
|
||||
afterScrollbackLoad: [],
|
||||
})
|
||||
this.room = room
|
||||
this.id = this.room.id
|
||||
|
@ -288,16 +322,21 @@ class Timeline extends Subscribable {
|
|||
url.searchParams.set("access_token", lsm.get("access_token"))
|
||||
url.searchParams.set("from", this.from)
|
||||
url.searchParams.set("dir", "b")
|
||||
url.searchParams.set("limit", 10)
|
||||
url.searchParams.set("limit", "20")
|
||||
const filter = {
|
||||
lazy_load_members: true
|
||||
}
|
||||
url.searchParams.set("filter", JSON.stringify(filter))
|
||||
|
||||
const root = await fetch(url.toString()).then(res => res.json())
|
||||
|
||||
this.broadcast("beforeScrollbackLoad")
|
||||
|
||||
this.from = root.end
|
||||
console.log(this.updateEvents, root.chunk)
|
||||
// console.log(this.updateEvents, root.chunk)
|
||||
if (root.state) this.updateStateEvents(root.state)
|
||||
this.updateEvents(root.chunk)
|
||||
this.broadcast("afterScrollbackLoad")
|
||||
}
|
||||
|
||||
send(body) {
|
||||
|
@ -324,33 +363,8 @@ class Timeline extends Subscribable {
|
|||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})/*.then(() => {
|
||||
const subscription = () => {
|
||||
this.removeEvent(id)
|
||||
this.unsubscribe("afterChange", subscription)
|
||||
}
|
||||
this.subscribe("afterChange", subscription)
|
||||
})*/
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
getGroupedEvents() {
|
||||
let currentSender = Symbol("N/A")
|
||||
let groups = []
|
||||
let currentGroup = []
|
||||
for (const event of this.list) {
|
||||
if (event.sender === currentSender) {
|
||||
currentGroup.push(event)
|
||||
} else {
|
||||
if (currentGroup.length) groups.push(currentGroup)
|
||||
currentGroup = [event]
|
||||
currentSender = event.sender
|
||||
}
|
||||
}
|
||||
if (currentGroup.length) groups.push(currentGroup)
|
||||
return groups
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = {Timeline}
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
|
||||
.c-message
|
||||
margin-top: 4px
|
||||
overflow-wrap: anywhere
|
||||
opacity: 1
|
||||
transition: opacity 0.2s ease-out
|
||||
|
||||
|
|
13
src/sass/loading.sass
Normal file
13
src/sass/loading.sass
Normal file
|
@ -0,0 +1,13 @@
|
|||
@keyframes spin
|
||||
0%
|
||||
transform: rotate(0deg)
|
||||
100%
|
||||
transform: rotate(180deg)
|
||||
|
||||
.loading-icon
|
||||
display: inline-block
|
||||
background-color: #ccc
|
||||
width: 12px
|
||||
height: 12px
|
||||
margin-right: 6px
|
||||
animation: spin 0.7s infinite
|
|
@ -1,6 +1,8 @@
|
|||
@use "./base"
|
||||
@use "./loading.sass"
|
||||
@use "./colors.sass" as c
|
||||
|
||||
|
||||
.main
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
@ -41,19 +43,6 @@
|
|||
.form-error
|
||||
color: red
|
||||
|
||||
@keyframes spin
|
||||
0%
|
||||
transform: rotate(0deg)
|
||||
100%
|
||||
transform: rotate(180deg)
|
||||
|
||||
.loading-icon
|
||||
display: inline-block
|
||||
background-color: #ccc
|
||||
width: 12px
|
||||
height: 12px
|
||||
margin-right: 6px
|
||||
animation: spin 0.7s infinite
|
||||
|
||||
input, button
|
||||
font-family: inherit
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
@use "./components/chat"
|
||||
@use "./components/chat-input"
|
||||
@use "./components/anchor"
|
||||
@use "./loading"
|
||||
|
|
Loading…
Reference in a new issue