inside . put the text in directly.
- const code = this.element.children[0]
- this.clearChildren()
- while (code.firstChild) {
- this.element.appendChild(code.firstChild)
- }
- }
- let shouldHighlight = (
- // if there are child _elements_, it's already formatted, we shouldn't mess that up
- this.element.children.length === 0
- /*
- no need to highlight very short code blocks:
- - content inside might not be code, some users still use code blocks
- for plaintext quotes
- - language detection will almost certainly be incorrect
- - even if it's code and the language is detected, the user will
- be able to mentally format small amounts of code themselves
-
- feel free to change the threshold number
- */
- && this.element.textContent.length > 80
- )
- if (shouldHighlight) {
- lazyLoad("https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10/build/highlight.min.js").then(hljs => hljs.highlightBlock(this.element))
- }
- }
-}
-
-module.exports = {HighlightedCode}
diff --git a/src/js/events/encrypted.js b/src/js/events/encrypted.js
deleted file mode 100644
index efc3ccf..0000000
--- a/src/js/events/encrypted.js
+++ /dev/null
@@ -1,18 +0,0 @@
-const {GroupableEvent} = require("./event")
-const {ejs} = require("../basic")
-
-class EncryptedMessage extends GroupableEvent {
- render() {
- this.clearChildren()
- this.child(
- ejs("i").text("Carbon cannot render encrypted messages yet")
- )
- super.render()
- }
-
- static canRender(eventData) {
- return eventData.type === "m.room.encrypted"
- }
-}
-
-module.exports = [EncryptedMessage]
diff --git a/src/js/events/event.js b/src/js/events/event.js
deleted file mode 100644
index 040d94e..0000000
--- a/src/js/events/event.js
+++ /dev/null
@@ -1,72 +0,0 @@
-const {ElemJS, ejs} = require("../basic")
-const {dateFormatter} = require("../date-formatter")
-const {SubscribeSet} = require("../store/subscribe_set.js")
-
-class MatrixEvent extends ElemJS {
- constructor(data) {
- super("div")
- this.data = null
- this.group = null
- this.editedAt = null
- this.readBy = new SubscribeSet()
- this.update(data)
- }
-
- // predicates
-
- canGroup() {
- return false
- }
-
- // operations
-
- setGroup(group) {
- this.group = group
- }
-
- setEdited(time) {
- this.editedAt = time
- this.render()
- }
-
- update(data) {
- this.data = data
- this.render()
- }
-
- removeEvent() {
- if (this.group) this.group.removeEvent(this)
- else this.remove()
- }
-
- render() {
- this.element.classList[this.data.pending ? "add" : "remove"]("c-message--pending")
- if (this.editedAt) {
- this.child(ejs("span").class("c-message__edited").text("(edited)").attribute("title", "at " + dateFormatter.format(this.editedAt)))
- }
- return this
- }
-
- static canRender(eventData) {
- return false
- }
-}
-
-class GroupableEvent extends MatrixEvent {
- constructor(data) {
- super(data)
- this.class("c-message")
- }
-
- canGroup() {
- return true
- }
-}
-
-class UngroupableEvent extends MatrixEvent {
-}
-
-module.exports = {
- GroupableEvent,
- UngroupableEvent
-}
diff --git a/src/js/events/hidden.js b/src/js/events/hidden.js
deleted file mode 100644
index 372bb72..0000000
--- a/src/js/events/hidden.js
+++ /dev/null
@@ -1,18 +0,0 @@
-const {UngroupableEvent} = require("./event")
-
-class HiddenEvent extends UngroupableEvent {
- constructor(data) {
- super(data)
- this.class("c-hidden-event")
- this.clearChildren()
- }
-
- static canRender(eventData) {
- return ["m.reaction", "m.call.candidates"].includes(eventData.type)
- }
-
- render() {
- }
-}
-
-module.exports = [HiddenEvent]
diff --git a/src/js/events/image.js b/src/js/events/image.js
deleted file mode 100644
index 1a32bcf..0000000
--- a/src/js/events/image.js
+++ /dev/null
@@ -1,48 +0,0 @@
-const {ejs, ElemJS} = require("../basic")
-const {resolveMxc} = require("../functions")
-const {GroupableEvent} = require("./event")
-
-class Image extends GroupableEvent {
- render() {
- this.clearChildren()
- this.class("c-message--media")
- const image = (
- ejs("img")
- .class("c-message__image")
- .attribute("src", resolveMxc(this.data.content.url))
- )
- const info = this.data.content.info
- if (info && info.w && info.h) {
- image.attribute("width", info.w)
- image.attribute("height", info.h)
- }
- const wrapper = ejs("div").class("c-media__wrapper").child(
- image
- )
- if (this.data.content.body && this.data.content.body.startsWith("SPOILER")) {
- wrapper.attribute("tabindex", 0)
- wrapper.class("c-media--spoiler")
- const wall = ejs("div").class("c-media__spoiler").text("Spoiler")
- wrapper.child(wall)
- const toggle = () => {
- wrapper.element.classList.toggle("c-media--shown")
- }
- wrapper.on("click", toggle)
- wrapper.on("keydown", event => {
- if (event.key === "Enter") toggle()
- })
- }
- this.child(wrapper)
- super.render()
- }
-
- static canRender(event) {
- return event.type === "m.room.message" && event.content.msgtype === "m.image"
- }
-
- canGroup() {
- return true
- }
-}
-
-module.exports = [Image]
diff --git a/src/js/events/membership.js b/src/js/events/membership.js
deleted file mode 100644
index 5617d3e..0000000
--- a/src/js/events/membership.js
+++ /dev/null
@@ -1,127 +0,0 @@
-const {UngroupableEvent} = require("./event")
-const {ejs} = require("../basic")
-const {extractDisplayName, resolveMxc, extractLocalpart} = require("../functions")
-
-class MembershipEvent extends UngroupableEvent {
- constructor(data) {
- super(data)
- this.class("c-message-event")
- this.senderName = extractDisplayName(data)
- if (data.content.avatar_url) {
- this.smallAvatar = ejs("img")
- .attribute("width", "32")
- .attribute("height", "32")
- .attribute("src", resolveMxc(data.content.avatar_url, 32, "crop"))
- .class("c-message-event__avatar")
- } else {
- this.smallAvatar = ""
- }
- this.render()
- }
-
- static canRender(eventData) {
- return eventData.type === "m.room.member"
- }
-
- renderInner(iconURL, elements) {
- this.clearChildren()
- this.child(
- ejs("div").class("c-message-event__inner").child(
- iconURL ? ejs("img").class("c-message-event__icon").attribute("width", "20").attribute("height", "20").attribute("src", iconURL) : "",
- ...elements
- )
- )
- super.render()
- }
-}
-
-
-class JoinedEvent extends MembershipEvent {
- static canRender(eventData) {
- return super.canRender(eventData) && eventData.content.membership === "join"
- }
-
- render() {
- const changes = []
- const prev = this.data.unsigned.prev_content
- if (prev && prev.membership === "join") {
- if (prev.avatar_url !== this.data.content.avatar_url) {
- changes.push("changed their avatar")
- }
- if (prev.displayname !== this.data.content.displayname) {
- changes.push(`changed their display name (was ${this.data.unsigned.prev_content.displayname})`)
- }
- }
- let message
- let iconURL
- if (changes.length) {
- message = " " + changes.join(", ")
- iconURL = "static/profile-event.svg"
- } else {
- message = " joined the room"
- iconURL = "static/join-event.svg"
- }
- this.renderInner(iconURL, [
- this.smallAvatar,
- this.senderName,
- message
- ])
- }
-}
-
-class InvitedEvent extends MembershipEvent {
- static canRender(eventData) {
- return super.canRender(eventData) && eventData.content.membership === "invite"
- }
-
- render() {
- this.renderInner("static/invite-event.svg", [
- this.smallAvatar,
- `${extractLocalpart(this.data.sender)} invited ${this.data.state_key}` // full mxid for clarity
- ])
- }
-}
-
-class LeaveEvent extends MembershipEvent {
- static canRender(eventData) {
- return super.canRender(eventData) && eventData.content.membership === "leave"
- }
-
- render() {
- this.renderInner("static/leave-event.svg", [
- this.smallAvatar,
- this.senderName,
- " left the room"
- ])
- }
-}
-
-class BanEvent extends MembershipEvent {
- static canRender(eventData) {
- return super.canRender(eventData) && eventData.content.membership === "ban"
- }
-
- render() {
- let message =
- ` left (banned by ${this.data.sender}`
- + (this.data.content.reason ? `, reason: ${this.data.content.reason}` : "")
- + ")"
- this.renderInner("static/leave-event.svg", [
- this.smallAvatar,
- this.senderName,
- message
- ])
- }
-}
-
-class UnknownMembership extends MembershipEvent {
- render() {
- this.renderInner("", [
- this.smallAvatar,
- this.senderName,
- ejs("i").text(" unknown membership event")
- ])
- }
-}
-
-module.exports = [JoinedEvent, InvitedEvent, LeaveEvent, BanEvent, UnknownMembership]
diff --git a/src/js/events/message.js b/src/js/events/message.js
deleted file mode 100644
index ad885ff..0000000
--- a/src/js/events/message.js
+++ /dev/null
@@ -1,164 +0,0 @@
-const {ejs, ElemJS} = require("../basic")
-const {HighlightedCode} = require("./components")
-const DOMPurify = require("dompurify")
-const {resolveMxc} = require("../functions")
-const {GroupableEvent} = require("./event")
-
-const purifier = DOMPurify()
-
-purifier.addHook("uponSanitizeAttribute", (node, hookevent, config) => {
- // If purifier already rejected an attribute there is no point in checking it
- if (hookevent.keepAttr === false) return;
-
- const allowedElementAttributes = {
- "FONT": ["data-mx-bg-color", "data-mx-color", "color"],
- "SPAN": ["data-mx-bg-color", "data-mx-color", "data-mx-spoiler"],
- "A": ["name", "target", "href"],
- "IMG": ["width", "height", "alt", "title", "src", "data-mx-emoticon"],
- "OL": ["start"],
- "CODE": ["class"],
- }
-
- const allowedAttributes = allowedElementAttributes[node.tagName] || []
- hookevent.keepAttr = allowedAttributes.indexOf(hookevent.attrName) > -1
-})
-
-purifier.addHook("uponSanitizeElement", (node, hookevent, config) => {
- // Remove bad classes from our code element
- if (node.tagName === "CODE") {
- node.classList.forEach(c => {
- if (!c.startsWith("language-")) {
- node.classList.remove(c)
- }
- })
- }
- if (node.tagName === "A") {
- node.setAttribute("rel", "noopener") // prevent the opening page from accessing carbon
- node.setAttribute("target", "_blank") // open in a new tab instead of replacing carbon
- }
- return node
-})
-
-function cleanHTML(html) {
- const config = {
- ALLOWED_TAGS: [
- "font", "del", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p",
- "a", "ul", "ol", "sup", "sub", "li", "b", "i", "u", "strong", "em",
- "strike", "code", "hr", "br", "div", "table", "thead", "tbody", "tr",
- "th", "td", "caption", "pre", "span", "img",
- // matrix tags
- "mx-reply"
- ],
-
- // In case we mess up in the uponSanitizeAttribute hook
- ALLOWED_ATTR: [
- "color", "name", "target", "href", "width", "height", "alt", "title",
- "src", "start", "class", "noreferrer", "noopener",
- // matrix attrs
- "data-mx-emoticon", "data-mx-bg-color", "data-mx-color", "data-mx-spoiler"
- ],
-
- // Return a DOM fragment instead of a string, avoids potential future mutation XSS
- // should also be faster than the browser parsing HTML twice
- // https://research.securitum.com/mutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass/
- RETURN_DOM_FRAGMENT: true,
- RETURN_DOM_IMPORT: true
- }
- return purifier.sanitize(html, config)
-}
-
-// Here we put all the processing of the messages that isn't as likely to potentially lead to security issues
-function postProcessElements(element) {
- element.querySelectorAll("pre").forEach(n => {
- new HighlightedCode(n)
- })
-
- element.querySelectorAll("img").forEach(n => {
- let src = n.getAttribute("src")
- if (src) src = resolveMxc(src)
- n.setAttribute("src", src)
- })
-
- element.querySelectorAll("font, span").forEach(n => {
- const color = n.getAttribute("data-mx-color") || n.getAttribute("color")
- const bgColor = n.getAttribute("data-mx-bg-color")
- if (color) n.style.color = color
- if (bgColor) n.style.backgroundColor = bgColor
- })
-
- element.querySelectorAll("[data-mx-spoiler]").forEach(spoiler => {
- spoiler.classList.add("mx-spoiler")
- spoiler.setAttribute("tabindex", 0)
- function toggle() {
- spoiler.classList.toggle("mx-spoiler--shown")
- }
- spoiler.addEventListener("click", toggle)
- spoiler.addEventListener("keydown", event => {
- if (event.key === "Enter") toggle()
- })
- })
-}
-
-
-class HTMLMessage extends GroupableEvent {
- render() {
- this.clearChildren()
-
- let html = this.data.content.formatted_body
-
- const fragment = cleanHTML(html)
- postProcessElements(fragment)
-
- this.child(ejs(fragment))
-
- super.render()
- }
-
- static canRender(event) {
- const content = event.content
- return (
- event.type === "m.room.message"
- && (content.msgtype === "m.text" || content.msgtype === "m.notice")
- && content.format === "org.matrix.custom.html"
- && content.formatted_body
- )
- }
-}
-
-function autoLinkText(text) {
- const fragment = ejs(new DocumentFragment())
- let lastIndex = 0
- text.replace(/https?:\/\/(?:[A-Za-z-]+\.)+[A-Za-z]{1,10}(?::[0-9]{1,6})?(?:\/[^ ]*)?/g, (url, index) => {
- // add text before URL
- fragment.addText(text.slice(lastIndex, index))
- // add URL
- fragment.child(
- ejs("a")
- .attribute("target", "_blank")
- .attribute("noopener", "")
- .attribute("href", url)
- .addText(url)
- )
- // update state
- lastIndex = index + url.length
- })
- // add final text
- fragment.addText(text.slice(lastIndex))
- return fragment
-}
-
-class TextMessage extends GroupableEvent {
- render() {
- this.clearChildren()
- this.class("c-message--plain")
- const fragment = autoLinkText(this.data.content.body)
- this.child(fragment)
- super.render()
- }
-
- static canRender(event) {
- return event.type === "m.room.message"
- }
-}
-
-module.exports = [HTMLMessage, TextMessage]
diff --git a/src/js/events/render-event.js b/src/js/events/render-event.js
deleted file mode 100644
index 620da3e..0000000
--- a/src/js/events/render-event.js
+++ /dev/null
@@ -1,24 +0,0 @@
-const imageEvent = require("./image")
-const messageEvent = require("./message")
-const encryptedEvent = require("./encrypted")
-const membershipEvent = require("./membership")
-const unknownEvent = require("./unknown")
-const callEvent = require("./call")
-const hiddenEvent = require("./hidden")
-
-const events = [
- ...imageEvent,
- ...messageEvent,
- ...encryptedEvent,
- ...membershipEvent,
- ...callEvent,
- ...hiddenEvent,
- ...unknownEvent,
-]
-
-function renderEvent(eventData) {
- const constructor = events.find(e => e.canRender(eventData))
- return new constructor(eventData)
-}
-
-module.exports = {renderEvent}
diff --git a/src/js/events/unknown.js b/src/js/events/unknown.js
deleted file mode 100644
index 5133aa8..0000000
--- a/src/js/events/unknown.js
+++ /dev/null
@@ -1,19 +0,0 @@
-const {GroupableEvent} = require("./event")
-const {ejs} = require("../basic")
-
-class UnknownEvent extends GroupableEvent {
- static canRender() {
- return true
- }
-
- render() {
- this.clearChildren()
- this.child(
- ejs("i").text(`Unknown event of type ${this.data.type}`)
- )
- super.render()
- }
-}
-
-module.exports = [UnknownEvent]
-
diff --git a/src/js/focus.js b/src/js/focus.js
deleted file mode 100644
index 2413484..0000000
--- a/src/js/focus.js
+++ /dev/null
@@ -1,11 +0,0 @@
-document.body.classList.remove("show-focus")
-
-document.addEventListener("mousedown", () => {
- document.body.classList.remove("show-focus")
-})
-
-document.addEventListener("keydown", event => {
- if (event.key === "Tab") {
- document.body.classList.add("show-focus")
- }
-})
diff --git a/src/js/functions.js b/src/js/functions.js
index 3c346c4..299d8a9 100644
--- a/src/js/functions.js
+++ b/src/js/functions.js
@@ -1,10 +1,7 @@
-const lsm = require("./lsm.js")
+import * as lsm from $to_relative "/js/lsm.js"
function resolveMxc(url, size, method) {
- const match = url.match(/^mxc:\/\/([^/]+)\/(.*)/)
- if (!match) return url
- let [server, id] = match.slice(1)
- id = id.replace(/#.*$/, "")
+ const [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1)
if (size && method) {
return `${lsm.get("domain")}/_matrix/media/r0/thumbnail/${server}/${id}?width=${size}&height=${size}&method=${method}`
} else {
@@ -12,28 +9,4 @@ function resolveMxc(url, size, method) {
}
}
-function extractLocalpart(mxid) {
- // try to extract the localpart from the mxid
- let match = mxid.match(/^@([^:]+):/)
- if (match) {
- return match[1]
- }
- // localpart extraction failed, use the whole mxid
- return mxid
-}
-
-function extractDisplayName(stateEvent) {
- const mxid = stateEvent.state_key
- // see if a display name is set
- if (stateEvent.content.displayname) {
- return stateEvent.content.displayname
- }
- // fall back to the mxid
- return extractLocalpart(mxid)
-}
-
-module.exports = {
- resolveMxc,
- extractLocalpart,
- extractDisplayName
-}
+export {resolveMxc}
diff --git a/src/js/groups.js b/src/js/groups.js
index 38b6706..e49ade4 100644
--- a/src/js/groups.js
+++ b/src/js/groups.js
@@ -1,4 +1,4 @@
-const {q} = require("./basic.js")
+import {q} from $to_relative "/js/basic.js"
let state = "CLOSED"
diff --git a/src/js/lazy-load-module.js b/src/js/lazy-load-module.js
deleted file mode 100644
index efe642d..0000000
--- a/src/js/lazy-load-module.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// I hate this with passion
-async function lazyLoad(url) {
- const cache = window.lazyLoadCache || new Map()
- window.lazyLoadCache = cache
- if (cache.get(url)) return cache.get(url)
-
- const module = loadModuleWithoutCache(url)
- cache.set(url, module)
- return module
-}
-
-// Loads the module without caching
-async function loadModuleWithoutCache(url) {
- const src = await fetch(url).then(r => r.text())
- let module = {}
- eval(src)
- return module.exports
-}
-
-module.exports = {lazyLoad}
diff --git a/src/js/login.js b/src/js/login.js
deleted file mode 100644
index 4ae9eae..0000000
--- a/src/js/login.js
+++ /dev/null
@@ -1,167 +0,0 @@
-const {q, ElemJS, ejs} = require("./basic.js")
-
-const password = q("#password")
-const homeserver = q("#homeserver")
-
-class Username extends ElemJS {
- constructor() {
- super(q("#username"))
-
- this.on("change", this.updateServer.bind(this))
- }
-
- isValid() {
- return !!this.element.value.match(/^@?[a-z0-9._=\/-]+(?::[a-zA-Z0-9.:\[\]-]+)?$/)
- }
-
- getUsername() {
- return this.element.value.match(/^@?([a-z0-9._=\/-]+)/)[1]
- }
-
- getServer() {
- const server = this.element.value.match(/^@?[a-z0-9._=\?-]+:([a-zA-Z0-9.:\[\]-]+)$/)
- if (server && server[1]) return server[1]
- else return null
- }
-
- updateServer() {
- if (!this.isValid()) return
- if (this.getServer()) homeserver.value = this.getServer()
- }
-}
-
-const username = new Username()
-
-class Feedback extends ElemJS {
- constructor() {
- super(q("#feedback"))
- this.loading = false
- this.loadingIcon = ejs("span").class("loading-icon")
- this.messageSpan = ejs("span")
- this.child(this.messageSpan)
- }
-
- setLoading(state) {
- if (this.loading && !state) {
- this.loadingIcon.remove()
- } else if (!this.loading && state) {
- this.childAt(0, this.loadingIcon)
- }
- this.loading = state
- }
-
- message(content, isError) {
- this.removeClass("form-feedback")
- this.removeClass("form-error")
- if (content) this.class("form-feedback")
- if(isError) this.class("form-error")
-
- this.messageSpan.text(content)
- }
-}
-
-const feedback = new Feedback()
-
-class Form extends ElemJS {
- constructor() {
- super(q("#form"))
-
- this.processing = false
-
- this.on("submit", this.submit.bind(this))
- }
-
- async submit() {
- if (this.processing) return
- this.processing = true
- if (!username.isValid()) return this.cancel("Username is not valid.")
-
- // Resolve homeserver address
- let domain
- try {
- domain = await this.findHomeserver(homeserver.value)
- } catch(e) {
- return this.cancel(e.message)
- }
-
- // Request access token
- this.status("Logging in...")
- const root = await fetch(`${domain}/_matrix/client/r0/login`, {
- method: "POST",
- body: JSON.stringify({
- type: "m.login.password",
- user: username.getUsername(),
- password: password.value
- })
- }).then(res => res.json())
-
- if (!root.access_token) {
- if (root.error) {
- this.cancel(`Server said: ${root.error}`)
- } else {
- this.cancel("Login mysteriously failed.")
- console.error(root)
- }
- return
- }
-
- localStorage.setItem("mx_user_id", root.user_id)
- localStorage.setItem("domain", domain)
- localStorage.setItem("access_token", root.access_token)
-
- location.assign("../")
- }
-
- async findHomeserver(address, maxDepth = 5) {
-
- //Protects from servers sending us on a redirect loop
- maxDepth--
- if (maxDepth <= 0) throw new Error(`Failed to look up homeserver, maximum search depth reached`)
-
- //Normalise the address
- if (!address.match(/^https?:\/\//)) {
- console.warn(`${address} doesn't specify the protocol, assuming https`)
- address = "https://" + address
- }
- address = address.replace(/\/*$/, "")
-
- this.status(`Looking up homeserver... trying ${address}`)
-
- // Check if we found the actual matrix server
- try {
- const versionsReq = await fetch(`${address}/_matrix/client/versions`)
- if (versionsReq.ok) {
- const versions = await versionsReq.json()
- if (Array.isArray(versions.versions)) return address
- }
- } catch(e) {}
-
- // Find the next matrix server in the chain
- const root = await fetch(`${address}/.well-known/matrix/client`).then(res => res.json()).catch(e => {
- console.error(e)
- throw new Error(`Failed to look up server ${address}`)
- })
-
- let nextAddress = root["m.homeserver"].base_url
- nextAddress = nextAddress.replace(/\/*$/, "")
-
- if (address === nextAddress) {
- throw new Error(`Failed to look up server ${address}, /.well-known/matrix/client found a redirect loop`);
- }
-
- return this.findHomeserver(nextAddress, maxDepth)
- }
-
- status(message) {
- feedback.setLoading(true)
- feedback.message(message)
- }
-
- cancel(message) {
- this.processing = false
- feedback.setLoading(false)
- feedback.message(message, true)
- }
-}
-
-const form = new Form()
diff --git a/src/js/lsm.js b/src/js/lsm.js
index 7e9ad4d..7338343 100644
--- a/src/js/lsm.js
+++ b/src/js/lsm.js
@@ -8,4 +8,4 @@ function set(name, value) {
window.lsm = {get, set}
-module.exports = {get, set}
+export {get, set}
diff --git a/src/js/main.js b/src/js/main.js
deleted file mode 100644
index 1bc0be0..0000000
--- a/src/js/main.js
+++ /dev/null
@@ -1,11 +0,0 @@
-require("./focus.js")
-const groups = require("./groups.js")
-const chat_input = require("./chat-input.js")
-const room_picker = require("./room-picker.js")
-const sync = require("./sync/sync.js")
-const chat = require("./chat.js")
-require("./typing.js")
-
-if (!localStorage.getItem("access_token")) {
- location.assign("./login/")
-}
diff --git a/src/js/read-marker.js b/src/js/read-marker.js
deleted file mode 100644
index 53b4658..0000000
--- a/src/js/read-marker.js
+++ /dev/null
@@ -1,149 +0,0 @@
-const {ElemJS, ejs, q} = require("./basic.js")
-const {store} = require("./store/store.js")
-const lsm = require("./lsm.js")
-
-function markFullyRead(roomID, eventID) {
- return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${roomID}/read_markers?access_token=${lsm.get("access_token")}`, {
- method: "POST",
- body: JSON.stringify({
- "m.fully_read": eventID,
- "m.read": eventID
- })
- })
-}
-
-class ReadBanner extends ElemJS {
- constructor() {
- super(q("#c-chat-banner"))
-
- this.newMessages = ejs("span")
- this.child(
- ejs("div").class("c-chat-banner__inner").child(
- ejs("button").class("c-chat-banner__part").on("click", this.jumpTo.bind(this)).child(
- ejs("div").class("c-chat-banner__part-inner")
- .child(this.newMessages)
- .addText(" new messages")
- ),
- ejs("button").class("c-chat-banner__part", "c-chat-banner__last").on("click", this.markRead.bind(this)).child(
- ejs("div").class("c-chat-banner__part-inner").text("Mark as read")
- )
- )
- )
-
- store.activeRoom.subscribe("changeSelf", this.render.bind(this))
- store.notificationsChange.subscribe("changeSelf", this.render.bind(this))
- this.render()
- }
-
- async jumpTo() {
- if (!store.activeRoom.exists()) return
- const timeline = store.activeRoom.value().timeline
- const readMarker = timeline.readMarker
- while (true) {
- if (readMarker.attached) {
- readMarker.element.scrollIntoView({behavior: "smooth", block: "center"})
- return
- } else {
- q("#c-chat-messages").scrollTo({
- top: 0,
- left: 0,
- behavior: "smooth"
- })
- await new Promise(resolve => {
- const unsubscribe = timeline.subscribe("afterScrollbackLoad", () => {
- unsubscribe()
- resolve()
- })
- })
- }
- }
- }
-
- markRead() {
- if (!store.activeRoom.exists()) return
- const timeline = store.activeRoom.value().timeline
- markFullyRead(timeline.id, timeline.latestEventID)
- }
-
- render() {
- let count = 0
- if (store.activeRoom.exists()) {
- count = store.activeRoom.value().number.state.unreads
- }
- if (count !== 0) {
- this.newMessages.text(count)
- this.class("c-chat-banner--active")
- } else {
- this.removeClass("c-chat-banner--active")
- }
- }
-}
-const readBanner = new ReadBanner()
-
-class ReadMarker extends ElemJS {
- constructor(timeline) {
- super("div")
-
- this.class("c-read-marker")
- this.loadingIcon = ejs("div")
- .class("c-read-marker__loading", "loading-icon")
- .style("display", "none")
- this.child(
- ejs("div").class("c-read-marker__inner").child(
- ejs("div").class("c-read-marker__text").child(this.loadingIcon).addText("New")
- )
- )
-
- let processing = false
- const observer = new IntersectionObserver(entries => {
- const entry = entries[0]
- if (!entry.isIntersecting) return
- if (processing) return
- processing = true
- this.loadingIcon.style("display", "")
- markFullyRead(this.timeline.id, this.timeline.latestEventID).then(() => {
- this.loadingIcon.style("display", "none")
- processing = false
- })
- }, {
- root: document.getElementById("c-chat-messages"),
- rootMargin: "-80px 0px 0px 0px", // marker must be this distance inside the top of the screen to be counted as read
- threshold: 0.01
- })
- observer.observe(this.element)
-
- this.attached = false
- this.timeline = timeline
- this.timeline.userReads.get(lsm.get("mx_user_id")).subscribe("changeSelf", (_, eventID) => {
- // read marker updated, attach to it
- const event = this.timeline.map.get(eventID)
- this.attach(event)
- })
- this.timeline.subscribe("afterChange", () => {
- // timeline has new events, attach to last read one
- const eventID = this.timeline.userReads.get(lsm.get("mx_user_id")).value()
- const event = this.timeline.map.get(eventID)
- this.attach(event)
- })
- }
-
- attach(event) {
- if (event && event.data.origin_server_ts !== this.timeline.latest) {
- this.class("c-read-marker--attached")
- event.element.insertAdjacentElement("beforeend", this.element)
- this.attached = true
- } else {
- this.removeClass("c-read-marker--attached")
- this.attached = false
- }
- if (store.activeRoom.value() === this.timeline.room) {
- readBanner.render()
- }
- }
-}
-
-module.exports = {
- ReadMarker,
- readBanner,
- markFullyRead
-}
diff --git a/src/js/room-picker.js b/src/js/room-picker.js
index 9192ae9..8e696de 100644
--- a/src/js/room-picker.js
+++ b/src/js/room-picker.js
@@ -1,10 +1,10 @@
-const {q, ElemJS, ejs} = require("./basic.js")
-const {store} = require("./store/store.js")
-const {SubscribeMapList} = require("./store/subscribe_map_list.js")
-const {SubscribeValue} = require("./store/subscribe_value.js")
-const {Timeline} = require("./timeline.js")
-const lsm = require("./lsm.js")
-const {resolveMxc, extractLocalpart, extractDisplayName} = require("./functions.js")
+import {q, ElemJS, ejs} from $to_relative "/js/basic.js"
+import {store} from $to_relative "/js/store/store.js"
+import {SubscribeMapList} from $to_relative "/js/store/SubscribeMapList.js"
+import {SubscribeValue} from $to_relative "/js/store/SubscribeValue.js"
+import {Timeline} from $to_relative "/js/Timeline.js"
+import * as lsm from $to_relative "/js/lsm.js"
+import {resolveMxc} from $to_relative "/js/functions.js"
class ActiveGroupMarker extends ElemJS {
constructor() {
@@ -25,43 +25,12 @@ class ActiveGroupMarker extends ElemJS {
const activeGroupMarker = new ActiveGroupMarker()
-class GroupNotifier extends ElemJS {
- constructor() {
- super("div")
-
- this.class("c-group__number")
- this.state = {}
- this.render()
- }
-
- update(state) {
- Object.assign(this.state, state)
- this.render()
- }
-
- clear() {
- this.state = {}
- this.render()
- }
-
- render() {
- let total = Object.values(this.state).reduce((a, c) => a + c, 0)
- if (total > 0) {
- this.text(total)
- this.class("c-group__number--active")
- } else {
- this.removeClass("c-group__number--active")
- }
- }
-}
-
class Group extends ElemJS {
constructor(key, data) {
super("div")
this.data = data
this.order = this.data.order
- this.number = new GroupNotifier()
this.class("c-group")
this.child(
@@ -69,7 +38,6 @@ class Group extends ElemJS {
? ejs("img").class("c-group__icon").attribute("src", this.data.icon)
: ejs("div").class("c-group__icon")
),
- this.number,
ejs("div").class("c-group__name").text(this.data.name)
)
@@ -88,73 +56,12 @@ class Group extends ElemJS {
}
}
-class RoomNotifier extends ElemJS {
- constructor(room) {
- super("div")
-
- this.class("c-room__number")
-
- this.room = room
- this.classes = [
- "notifications",
- "unreads",
- "none"
- ]
- this.state = {
- notifications: 0,
- unreads: 0
- }
- this.render()
- }
-
- /**
- * @param {object} state
- * @param {number} [state.notifications]
- * @param {number} [state.unreads]
- */
- update(state) {
- Object.assign(this.state, state)
- this.informGroup()
- this.render()
- }
-
- informGroup() {
- this.room.getGroup().number.update({[this.room.id]: (
- this.state.notifications || (this.state.unreads ? 1 : 0)
- )})
- }
-
- render() {
- const display = {
- number: this.state.notifications || this.state.unreads,
- kind: this.state.notifications ? "notifications" : "unreads"
- }
- // set number
- if (display.number) {
- this.text(display.number)
- } else {
- this.text("")
- display.kind = "none"
- }
- // set class
- this.classes.forEach(c => {
- const name = "c-room__number--" + c
- if (c === display.kind) {
- this.class(name)
- } else {
- this.removeClass(name)
- }
- })
- }
-}
-
class Room extends ElemJS {
constructor(id, data) {
super("div")
this.id = id
this.data = data
- this.number = new RoomNotifier(this)
this.timeline = new Timeline(this)
this.group = null
this.members = new SubscribeMapList(SubscribeValue)
@@ -168,80 +75,43 @@ class Room extends ElemJS {
}
get order() {
- let string = ""
- if (this.number.state.notifications) {
- string += "N"
- } else if (this.number.state.unreads) {
- string += "U"
- } else {
- string += "_"
- }
if (this.group) {
- string += this.name
+ let chars = 36
+ let total = 0
+ const name = this.getName()
+ for (let i = 0; i < name.length; i++) {
+ const c = name[i]
+ let d = 0
+ if (c >= "A" && c <= "Z") d = c.charCodeAt(0) - 65 + 10
+ else if (c >= "a" && c <= "z") d = c.charCodeAt(0) - 97 + 10
+ else if (c >= "0" && c <= "9") d = +c
+ total += d * chars ** (-i)
+ }
+ return total
} else {
- string += (4000000000000 - this.timeline.latest) // good until 2065 :)
- }
- return string
- }
-
- getMemberName(mxid) {
- if (this.members.has(mxid)) {
- const state = this.members.get(mxid).value()
- return extractDisplayName(state)
- } else {
- return extractLocalpart(mxid)
- }
- }
-
- getHeroes() {
- if (this.data.summary) {
- return this.data.summary["m.heroes"]
- } else {
- const me = lsm.get("mx_user_id")
- return this.data.state.events.filter(e => e.type === "m.room.member" && e.content.membership === "join" && e.state_key !== me).map(e => e.state_key)
+ return -this.timeline.latest
}
}
getName() {
- // if the room has a name
let name = this.data.state.events.find(e => e.type === "m.room.name")
- if (name && name.content.name) {
- return name.content.name
+ if (name) {
+ name = name.content.name
+ } else {
+ const users = this.data.summary["m.heroes"]
+ const usernames = users.map(u => (u.match(/^@([^:]+):/) || [])[1] || u)
+ name = usernames.join(", ")
}
- // if the room has no name, use its canonical alias
- let canonicalAlias = this.data.state.events.find(e => e.type === "m.room.canonical_alias")
- if (canonicalAlias && canonicalAlias.content.alias) {
- return canonicalAlias.content.alias
- }
- // if the room has no alias, use the names of its members ("heroes")
- const users = this.getHeroes()
- if (users && users.length) {
- const usernames = users.map(mxid => this.getMemberName(mxid))
- return usernames.join(", ")
- }
- // the room is empty
- return "Empty room"
+ return name
}
getIcon() {
- // if the room has a normal avatar
const avatar = this.data.state.events.find(e => e.type === "m.room.avatar")
if (avatar) {
- const url = avatar.content.url || avatar.content.avatar_url
- if (url) {
- return resolveMxc(url, 32, "crop")
- }
+ return resolveMxc(avatar.content.url || avatar.content.avatar_url, 32, "crop")
+ } else {
+ return null
}
- // if the room has no avatar set, use a member's avatar
- const users = this.getHeroes()
- if (users && users[0] && this.members.has(users[0])) {
- // console.log(users[0], this.members.get(users[0]))
- const userAvatar = this.members.get(users[0]).value().content.avatar_url
- if (userAvatar) {
- return resolveMxc(userAvatar, 32, "crop")
- }
- }
- return null
}
isDirect() {
@@ -274,7 +144,6 @@ class Room extends ElemJS {
this.child(ejs("div").class("c-room__icon", "c-room__icon--no-icon"))
}
this.child(ejs("div").class("c-room__name").text(this.getName()))
- this.child(this.number)
// active
const active = store.activeRoom.value() === this
this.element.classList[active ? "add" : "remove"]("c-room--active")
@@ -294,7 +163,6 @@ class Rooms extends ElemJS {
store.activeGroup.subscribe("changeSelf", this.render.bind(this))
store.directs.subscribe("changeItem", this.render.bind(this))
store.newEvents.subscribe("changeSelf", this.sort.bind(this))
- store.notificationsChange.subscribe("changeSelf", this.sort.bind(this))
this.render()
}
@@ -355,12 +223,8 @@ class Groups extends ElemJS {
render() {
this.clearChildren()
store.groups.forEach((key, item) => {
- item.value().number.clear()
this.child(item.value())
})
- store.rooms.forEach((id, room) => {
- room.value().number.informGroup() // update group notification number
- })
}
}
const groups = new Groups()
diff --git a/src/js/sender.js b/src/js/sender.js
deleted file mode 100644
index 55943dc..0000000
--- a/src/js/sender.js
+++ /dev/null
@@ -1,120 +0,0 @@
-const {ElemJS, ejs} = require("./basic.js")
-const {store} = require("./store/store.js")
-const {resolveMxc} = require("./functions.js")
-
-function nameToColor(str) {
- // code from element's react sdk
- const colors = ["#55a7f0", "#da55ff", "#1bc47c", "#ea657e", "#fd8637", "#22cec6", "#8c8de3", "#71bf22"]
- let hash = 0
- let i
- let chr
- if (str.length === 0) {
- return hash
- }
- for (i = 0; i < str.length; i++) {
- chr = str.charCodeAt(i)
- hash = ((hash << 5) - hash) + chr
- hash |= 0
- }
- hash = Math.abs(hash) % 8
- return colors[hash]
-}
-
-class Avatar extends ElemJS {
- constructor() {
- super("div")
- this.class("c-message-group__avatar")
-
- this.mxc = undefined
- this.image = null
-
- this.update(null)
- }
-
- update(mxc) {
- if (mxc === this.mxc) return
- this.mxc = mxc
- this.hasImage = !!mxc
- if (this.hasImage) {
- const size = 96
- const url = resolveMxc(mxc, size, "crop")
- this.image = ejs("img").class("c-message-group__icon").attribute("src", url).attribute("width", size).attribute("height", size)
- this.image.on("error", this.onError.bind(this))
- }
- this.render()
- }
-
- onError() {
- this.hasImage = false
- this.render()
- }
-
- render() {
- this.clearChildren()
- if (this.hasImage) {
- this.child(this.image)
- } else {
- this.child(
- ejs("div").class("c-message-group__icon", "c-message-group__icon--no-icon")
- )
- }
- }
-}
-
-/** Must update at least once to render. */
-class Name extends ElemJS {
- constructor() {
- super("div")
- this.class("c-message-group__name")
-
- /**
- * Keeps track of whether we have the proper display name or not.
- * If we do, then we shoudn't override it with the mxid if the name becomes unavailable.
- */
- this.hasName = false
- this.name = ""
- this.mxid = ""
- }
-
- update(event) {
- this.mxid = event.state_key
- if (event.content.displayname) {
- this.hasName = true
- this.name = event.content.displayname
- } else if (!this.hasName) {
- this.name = this.mxid
- }
- this.render()
- }
-
- render() {
- // set text
- this.text(this.name)
- // set color
- this.style("color", nameToColor(this.mxid))
- }
-}
-
-class Sender {
- constructor(roomID, mxid) {
- this.sender = store.rooms.get(roomID).value().members.get(mxid)
- this.name = new Name()
- this.avatar = new Avatar()
- this.sender.subscribe("changeSelf", this.update.bind(this))
- this.update()
- }
-
- update() {
- if (this.sender.exists()) {
- // name
- this.name.update(this.sender.value())
-
- // avatar
- this.avatar.update(this.sender.value().content.avatar_url)
- }
- }
-}
-
-module.exports = {
- Sender
-}
diff --git a/src/js/store/subscribable.js b/src/js/store/Subscribable.js
similarity index 86%
rename from src/js/store/subscribable.js
rename to src/js/store/Subscribable.js
index 56bf971..6c7640e 100644
--- a/src/js/store/subscribable.js
+++ b/src/js/store/Subscribable.js
@@ -20,8 +20,6 @@ class Subscribable {
} else {
throw new Error(`Cannot subscribe to non-existent event ${event}, available events are: ${Object.keys(this.events).join(", ")}`)
}
- // return a function we can call to easily unsubscribe
- return () => this.unsubscribe(event, callback)
}
unsubscribe(event, callback) {
@@ -37,4 +35,4 @@ class Subscribable {
}
}
-module.exports = {Subscribable}
+export {Subscribable}
diff --git a/src/js/store/SubscribeMap.js b/src/js/store/SubscribeMap.js
new file mode 100644
index 0000000..8b0dc0c
--- /dev/null
+++ b/src/js/store/SubscribeMap.js
@@ -0,0 +1,41 @@
+import {Subscribable} from $to_relative "/js/store/Subscribable.js"
+import {SubscribeValue} from $to_relative "/js/store/SubscribeValue.js"
+
+class SubscribeMap extends Subscribable {
+ constructor() {
+ super()
+ Object.assign(this.events, {
+ addItem: [],
+ changeItem: [],
+ removeItem: []
+ })
+ this.map = new Map()
+ }
+
+ has(key) {
+ return this.map.has(key) && this.map.get(key).exists()
+ }
+
+ get(key) {
+ if (this.map.has(key)) {
+ return this.map.get(key)
+ } else {
+ this.map.set(key, new SubscribeValue())
+ }
+ }
+
+ set(key, value) {
+ let s
+ if (this.map.has(key)) {
+ s = this.map.get(key).set(value)
+ this.broadcast("changeItem", key)
+ } else {
+ s = new SubscribeValue().set(value)
+ this.map.set(key, s)
+ this.broadcast("addItem", key)
+ }
+ return s
+ }
+}
+
+export {SubscribeMap}
diff --git a/src/js/store/subscribe_map_list.js b/src/js/store/SubscribeMapList.js
similarity index 72%
rename from src/js/store/subscribe_map_list.js
rename to src/js/store/SubscribeMapList.js
index 794a8da..1883303 100644
--- a/src/js/store/subscribe_map_list.js
+++ b/src/js/store/SubscribeMapList.js
@@ -1,5 +1,5 @@
-const {Subscribable} = require("./subscribable.js")
-const {SubscribeValue} = require("./subscribe_value.js")
+import {Subscribable} from $to_relative "/js/store/Subscribable.js"
+import {SubscribeValue} from $to_relative "/js/store/SubscribeValue.js"
class SubscribeMapList extends Subscribable {
constructor(inner) {
@@ -54,15 +54,6 @@ class SubscribeMapList extends Subscribable {
}
sort() {
- const key = this.list[0]
- if (typeof this.map.get(key).value().order === "number") {
- this.sortByNumber()
- } else {
- this.sortByString()
- }
- }
-
- sortByNumber() {
this.list.sort((a, b) => {
const orderA = this.map.get(a).value().order
const orderB = this.map.get(b).value().order
@@ -71,17 +62,6 @@ class SubscribeMapList extends Subscribable {
this.broadcast("changeItem")
}
- sortByString() {
- this.list.sort((a, b) => {
- const orderA = this.map.get(a).value().order
- const orderB = this.map.get(b).value().order
- if (orderA < orderB) return -1
- else if (orderA > orderB) return 1
- else return 0
- })
- this.broadcast("changeItem")
- }
-
_add(key, value, start) {
let s
if (this.map.has(key)) {
@@ -103,4 +83,4 @@ class SubscribeMapList extends Subscribable {
}
}
-module.exports = {SubscribeMapList}
+export {SubscribeMapList}
diff --git a/src/js/store/subscribe_set.js b/src/js/store/SubscribeSet.js
similarity index 88%
rename from src/js/store/subscribe_set.js
rename to src/js/store/SubscribeSet.js
index 32c758c..789aaaf 100644
--- a/src/js/store/subscribe_set.js
+++ b/src/js/store/SubscribeSet.js
@@ -1,4 +1,4 @@
-const {Subscribable} = require("./subscribable.js")
+import {Subscribable} from $to_relative "/js/store/Subscribable.js"
class SubscribeSet extends Subscribable {
constructor() {
@@ -47,4 +47,4 @@ class SubscribeSet extends Subscribable {
}
}
-module.exports = {SubscribeSet}
+export {SubscribeSet}
diff --git a/src/js/store/subscribe_value.js b/src/js/store/SubscribeValue.js
similarity index 85%
rename from src/js/store/subscribe_value.js
rename to src/js/store/SubscribeValue.js
index 9c71959..6657e27 100644
--- a/src/js/store/subscribe_value.js
+++ b/src/js/store/SubscribeValue.js
@@ -1,4 +1,4 @@
-const {Subscribable} = require("./subscribable.js")
+import {Subscribable} from $to_relative "/js/store/Subscribable.js"
class SubscribeValue extends Subscribable {
constructor() {
@@ -30,7 +30,7 @@ class SubscribeValue extends Subscribable {
edit(f) {
if (this.exists()) {
- this.data = f(this.data)
+ f(this.data)
this.set(this.data)
} else {
throw new Error("Tried to edit a SubscribeValue that had no value")
@@ -44,4 +44,4 @@ class SubscribeValue extends Subscribable {
}
}
-module.exports = {SubscribeValue}
+export {SubscribeValue}
diff --git a/src/js/store/store.js b/src/js/store/store.js
index 8ef4511..1c0552d 100644
--- a/src/js/store/store.js
+++ b/src/js/store/store.js
@@ -1,7 +1,7 @@
-const {Subscribable} = require("./subscribable.js")
-const {SubscribeMapList} = require("./subscribe_map_list.js")
-const {SubscribeSet} = require("./subscribe_set.js")
-const {SubscribeValue} = require("./subscribe_value.js")
+import {Subscribable} from $to_relative "/js/store/Subscribable.js"
+import {SubscribeMapList} from $to_relative "/js/store/SubscribeMapList.js"
+import {SubscribeSet} from $to_relative "/js/store/SubscribeSet.js"
+import {SubscribeValue} from $to_relative "/js/store/SubscribeValue.js"
const store = {
groups: new SubscribeMapList(SubscribeValue),
@@ -9,10 +9,9 @@ const store = {
directs: new SubscribeSet(),
activeGroup: new SubscribeValue(),
activeRoom: new SubscribeValue(),
- newEvents: new Subscribable(),
- notificationsChange: new Subscribable()
+ newEvents: new Subscribable()
}
window.store = store
-module.exports = {store}
+export {store}
diff --git a/src/js/store/subscribe_map.js b/src/js/store/subscribe_map.js
deleted file mode 100644
index 6159597..0000000
--- a/src/js/store/subscribe_map.js
+++ /dev/null
@@ -1,74 +0,0 @@
-const {Subscribable} = require("./subscribable.js")
-
-class SubscribeMap extends Subscribable {
- constructor(inner) {
- super()
- this.inner = inner
- Object.assign(this.events, {
- addItem: [],
- editItem: [],
- deleteItem: [],
- changeItem: [],
- askSet: []
- })
- Object.assign(this.eventDeps, {
- addItem: ["changeItem"],
- editItem: ["changeItem"],
- deleteItem: ["changeItem"],
- changeItem: [],
- askSet: []
- })
- this.map = new Map()
- }
-
- has(key) {
- return this.map.has(key) && this.map.get(key).exists()
- }
-
- get(key) {
- if (this.map.has(key)) {
- return this.map.get(key)
- } else {
- const item = new this.inner()
- this.map.set(key, item)
- return item
- }
- }
-
- forEach(f) {
- for (const entry of this.map.entries()) {
- f(entry[0], entry[1])
- }
- }
-
- askSet(key, value) {
- this.broadcast("askSet", {key, value})
- }
-
- set(key, value) {
- let s
- if (this.map.has(key)) {
- const exists = this.map.get(key).exists()
- s = this.map.get(key).set(value)
- if (exists) {
- this.broadcast("editItem", key)
- } else {
- this.broadcast("addItem", key)
- }
- } else {
- s = new this.inner().set(value)
- this.map.set(key, s)
- this.broadcast("addItem", key)
- }
- return s
- }
-
- delete(key) {
- if (this.backing.has(key)) {
- this.backing.delete(key)
- this.broadcast("deleteItem", key)
- }
- }
-}
-
-module.exports = {SubscribeMap}
diff --git a/src/js/sync/sync.js b/src/js/sync/sync.js
index f8bfc52..bbd6b11 100644
--- a/src/js/sync/sync.js
+++ b/src/js/sync/sync.js
@@ -1,6 +1,6 @@
-const {store} = require("../store/store.js")
-const lsm = require("../lsm.js")
-const {resolveMxc} = require("../functions.js")
+import {store} from $to_relative "/js/store/store.js"
+import * as lsm from $to_relative "/js/lsm.js"
+import {resolveMxc} from $to_relative "/js/functions.js"
let lastBatch = null
@@ -11,7 +11,7 @@ function sync() {
room: {
// pulling more from the timeline massively increases download size
timeline: {
- limit: 1
+ limit: 5
},
// members are not currently needed
state: {
@@ -37,88 +37,63 @@ function sync() {
function manageSync(root) {
try {
let newEvents = false
- let notificationsChange = false
// set up directs
- if (root.account_data) {
- const directs = root.account_data.events.find(e => e.type === "m.direct")
- if (directs) {
- Object.values(directs.content).forEach(ids => {
- ids.forEach(id => store.directs.add(id))
- })
- }
- }
-
- // set up rooms
- if (root.rooms) {
- if (root.rooms.join) {
- Object.entries(root.rooms.join).forEach(([id, data]) => {
- if (!store.rooms.has(id)) {
- store.rooms.askAdd(id, data)
- }
- const room = store.rooms.get(id).value()
- const timeline = room.timeline
- if (data.state && data.state.events) timeline.updateStateEvents(data.state.events)
- if (data.timeline && data.timeline.events) {
- if (!timeline.from) timeline.from = data.timeline.prev_batch
- if (data.timeline.events.length) {
- newEvents = true
- timeline.updateEvents(data.timeline.events)
- }
- }
- if (data.ephemeral && data.ephemeral.events) timeline.updateEphemeral(data.ephemeral.events)
- if (data.unread_notifications) {
- timeline.updateNotificationCount(data.unread_notifications.notification_count)
- notificationsChange = true
- }
- if (data["org.matrix.msc2654.unread_count"] != undefined) {
- timeline.updateUnreadCount(data["org.matrix.msc2654.unread_count"])
- notificationsChange = true
- }
- })
- }
- }
-
- // set up groups
- if (root.groups) {
- Promise.all(
- Object.keys(root.groups.join).map(id => {
- if (!store.groups.has(id)) {
- return Promise.all(["profile", "rooms"].map(path => {
- const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/groups/${id}/${path}`)
- url.searchParams.append("access_token", lsm.get("access_token"))
- return fetch(url.toString()).then(res => res.json())
- })).then(([profile, rooms]) => {
- rooms = rooms.chunk
- let order = 999
- let orderEvent = root.account_data.events.find(e => e.type === "im.vector.web.tag_ordering")
- if (orderEvent) {
- if (orderEvent.content.tags.includes(id)) {
- order = orderEvent.content.tags.indexOf(id)
- }
- }
- const data = {
- name: profile.name,
- icon: resolveMxc(profile.avatar_url, 96, "crop"),
- order
- }
- store.groups.askAdd(id, data)
- rooms.forEach(groupRoom => {
- if (store.rooms.has(groupRoom.room_id)) {
- store.rooms.get(groupRoom.room_id).value().setGroup(id)
- }
- })
- store.newEvents.broadcast("changeSelf") // trigger a room list update
- })
- }
- })
- ).then(() => {
- store.rooms.sort()
+ const directs = root.account_data.events.find(e => e.type === "m.direct")
+ if (directs) {
+ Object.values(directs.content).forEach(ids => {
+ ids.forEach(id => store.directs.add(id))
})
}
+ // set up rooms
+ Object.entries(root.rooms.join).forEach(([id, data]) => {
+ if (!store.rooms.has(id)) {
+ store.rooms.askAdd(id, data)
+ }
+ const room = store.rooms.get(id).value()
+ const timeline = room.timeline
+ if (data.timeline.events.length) newEvents = true
+ timeline.updateStateEvents(data.state.events)
+ timeline.updateEvents(data.timeline.events)
+ })
+
+ // set up groups
+ Promise.all(
+ Object.keys(root.groups.join).map(id => {
+ if (!store.groups.has(id)) {
+ return Promise.all(["profile", "rooms"].map(path => {
+ const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/groups/${id}/${path}`)
+ url.searchParams.append("access_token", lsm.get("access_token"))
+ return fetch(url.toString()).then(res => res.json())
+ })).then(([profile, rooms]) => {
+ rooms = rooms.chunk
+ let order = 999
+ let orderEvent = root.account_data.events.find(e => e.type === "im.vector.web.tag_ordering")
+ if (orderEvent) {
+ if (orderEvent.content.tags.includes(id)) {
+ order = orderEvent.content.tags.indexOf(id)
+ }
+ }
+ const data = {
+ name: profile.name,
+ icon: resolveMxc(profile.avatar_url, 96, "crop"),
+ order
+ }
+ store.groups.askAdd(id, data)
+ rooms.forEach(groupRoom => {
+ if (store.rooms.has(groupRoom.room_id)) {
+ store.rooms.get(groupRoom.room_id).value().setGroup(id)
+ }
+ })
+ store.newEvents.broadcast("changeSelf") // trigger a room list update
+ })
+ }
+ })
+ ).then(() => {
+ store.rooms.sort()
+ })
if (newEvents) store.newEvents.broadcast("changeSelf")
- if (notificationsChange) store.notificationsChange.broadcast("changeSelf")
} catch (e) {
console.error(root)
throw e
@@ -146,6 +121,4 @@ function syncLoop() {
store.activeGroup.set(store.groups.get("directs").value())
-if (lsm.get("access_token")) {
- syncLoop()
-}
+syncLoop()
diff --git a/src/js/timeline.js b/src/js/timeline.js
deleted file mode 100644
index 0a36d14..0000000
--- a/src/js/timeline.js
+++ /dev/null
@@ -1,408 +0,0 @@
-const {ElemJS, ejs, q} = require("./basic.js")
-const {Subscribable} = require("./store/subscribable.js")
-const {SubscribeValue} = require("./store/subscribe_value.js")
-const {SubscribeMap} = require("./store/subscribe_map.js")
-const {store} = require("./store/store.js")
-const {Anchor} = require("./anchor.js")
-const {Sender} = require("./sender.js")
-const {ReadMarker, markFullyRead} = require("./read-marker.js")
-const lsm = require("./lsm.js")
-const {resolveMxc} = require("./functions.js")
-const {renderEvent} = require("./events/render-event")
-const {dateFormatter} = require("./date-formatter")
-
-let debug = false
-
-const NO_MAX = Symbol("NO_MAX")
-
-let sentIndex = 0
-
-function getTxnId() {
- return Date.now() + (sentIndex++)
-}
-
-function eventSearch(list, event, min = 0, max = NO_MAX) {
- if (list.length === 0) return {success: false, i: 0}
-
- if (max === NO_MAX) max = list.length - 1
- let mid = Math.floor((max + min) / 2)
- // success condition
- if (list[mid] && list[mid].data.event_id === event.data.event_id) return {success: true, i: mid}
- // failed condition
- if (min >= max) {
- while (mid !== -1 && (!list[mid] || list[mid].data.origin_server_ts > event.data.origin_server_ts)) mid--
- return {
- success: false,
- i: mid + 1
- }
- }
- // recurse (below)
- 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)
-}
-
-class EventGroup extends ElemJS {
- constructor(reactive, list) {
- super("div")
- this.class("c-message-group")
- this.reactive = reactive
- this.list = list
- this.data = {
- sender: list[0].data.sender,
- origin_server_ts: list[0].data.origin_server_ts
- }
- this.sender = new Sender(this.reactive.id, this.data.sender)
- this.child(
- this.sender.avatar,
- this.messages = ejs("div").class("c-message-group__messages").child(
- ejs("div").class("c-message-group__intro").child(
- this.sender.name,
- ejs("div").class("c-message-group__date").text(dateFormatter.format(this.data.origin_server_ts))
- ),
- ...this.list
- )
- )
- }
-
- canGroup() {
- if (this.list.length) return this.list[0].canGroup()
- else return true
- }
-
- addEvent(event) {
- const index = eventSearch(this.list, event).i
- event.setGroup(this)
- this.list.splice(index, 0, event)
- this.messages.childAt(index + 1, event)
- }
-
- removeEvent(event) {
- const search = eventSearch(this.list, event)
- if (!search.success) throw new Error(`Event ${event.data.event_id} not found in this group`)
- const index = search.i
- // actually remove the event
- this.list.splice(index, 1)
- event.remove() // should get everything else
- if (this.list.length === 0) this.reactive.removeGroup(this)
- }
-}
-
-/** 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)
- // console.log(search, this.list.map(l => l.data.sender), event.data)
- if (!search.success) {
- if (search.i >= 1) {
- // add at end
- this.tryAddGroups(event, [search.i - 1, search.i])
- } else {
- // add at start
- this.tryAddGroups(event, [0, -1])
- }
- } else {
- this.tryAddGroups(event, [search.i])
- }
- this.loadMore = new LoadMore(this.id)
- this.childAt(0, this.loadMore)
- }
-
- tryAddGroups(event, indices) {
- const createGroupAt = i => {
- // if (printed++ < 100) console.log("tryadd success, created group")
- if (i === -1) {
- // here, -1 means at the start, before the first group
- i = 0 // jank but it does the trick
- }
- if (event.canGroup()) {
- const group = new EventGroup(this, [event])
- this.list.splice(i, 0, group)
- this.childAt(i, group)
- event.setGroup(group)
- } else {
- this.list.splice(i, 0, event)
- this.childAt(i, event)
- }
- }
- const success = indices.some(i => {
- if (!this.list[i]) {
- createGroupAt(i)
- return true
- } else if (event.canGroup() && this.list[i] && this.list[i].canGroup() && this.list[i].data.sender === event.data.sender) {
- // if (printed++ < 100) console.log("tryadd success, using existing group")
- this.list[i].addEvent(event)
- return true
- }
- })
- // if (!success) console.log("tryadd failure", indices, this.list.map(l => l.data.sender), event.data) // I believe all the bugs are now fixed. Lol.
- if (!success) createGroupAt(indices[0])
- }
-
- removeGroup(group) {
- const index = this.list.indexOf(group)
- this.list.splice(index, 1)
- group.remove() // should get everything else
- }
-
- render() {
- this.clearChildren()
- this.child(this.loadMore)
- this.list.forEach(group => this.child(group))
- this.anchor = new Anchor()
- this.child(this.anchor)
- }
-}
-
-class Timeline extends Subscribable {
- constructor(room) {
- super()
- Object.assign(this.events, {
- beforeChange: [],
- afterChange: [],
- beforeScrollbackLoad: [],
- afterScrollbackLoad: [],
- })
- Object.assign(this.eventDeps, {
- beforeChange: [],
- afterChange: [],
- beforeScrollbackLoad: [],
- afterScrollbackLoad: [],
- })
- this.room = room
- this.id = this.room.id
- this.list = []
- this.map = new Map()
- this.reactiveTimeline = new ReactiveTimeline(this.id, [])
- this.latest = 0
- this.latestEventID = null
- this.pending = new Set()
- this.pendingEdits = []
- this.typing = new SubscribeValue().set([])
- this.userReads = new SubscribeMap(SubscribeValue)
- this.readMarker = new ReadMarker(this)
- this.from = null
- }
-
- updateStateEvents(events) {
- for (const eventData of events) {
- let id = eventData.event_id
- if (eventData.type === "m.room.member") {
- // update members
- if (eventData.membership !== "leave") {
- const member = this.room.members.get(eventData.state_key)
- // only use the latest state
- if (!member.exists() || eventData.origin_server_ts > member.data.origin_server_ts) {
- member.set(eventData)
- }
- }
- }
- }
- }
-
- updateEvents(events) {
- this.broadcast("beforeChange")
- // handle state events
- this.updateStateEvents(events)
- for (const eventData of events) {
- // set variables
- let id = eventData.event_id
- if (eventData.origin_server_ts > this.latest) {
- this.latest = eventData.origin_server_ts
- this.latestEventID = id
- }
- // handle local echoes
- if (eventData.sender === lsm.get("mx_user_id") && eventData.content && this.pending.has(eventData.content["chat.carbon.message.pending_id"])) {
- const pendingID = eventData.content["chat.carbon.message.pending_id"]
- if (id !== pendingID) {
- const target = this.map.get(pendingID)
- this.map.set(id, target)
- this.map.delete(pendingID)
- // update fully read marker - assume we have fully read up to messages we send
- markFullyRead(this.id, id)
- }
- }
- // handle timeline events
- if (this.map.has(id)) {
- // update existing event
- this.map.get(id).update(eventData)
- } else {
- // skip redacted events
- if (eventData.unsigned && eventData.unsigned.redacted_by) {
- continue
- }
- // handle redactions
- if (eventData.type === "m.room.redaction") {
- if (this.map.has(eventData.redacts)) this.map.get(eventData.redacts).removeEvent()
- continue
- }
- // handle edits
- if (eventData.type === "m.room.message" && eventData.content["m.relates_to"] && eventData.content["m.relates_to"].rel_type === "m.replace") {
- this.pendingEdits.push(eventData)
- continue
- }
- // add new event
- const event = renderEvent(eventData)
- this.map.set(id, event)
- this.reactiveTimeline.addEvent(event)
- // update read receipt for sender on their own event
- this.moveReadReceipt(eventData.sender, id)
- }
- }
- // apply edits
- this.pendingEdits = this.pendingEdits.filter(eventData => {
- const replaces = eventData.content["m.relates_to"].event_id
- if (this.map.has(replaces)) {
- const event = this.map.get(replaces)
- event.data.content = eventData.content["m.new_content"]
- event.setEdited(eventData.origin_server_ts)
- event.update(event.data)
- return false // handled; remove from list
- } else {
- return true // we don't have the event it edits yet; keep in list
- }
- })
- this.broadcast("afterChange")
- }
-
- updateEphemeral(events) {
- for (const eventData of events) {
- if (eventData.type === "m.typing") {
- this.typing.set(eventData.content.user_ids)
- }
- if (eventData.type === "m.receipt") {
- for (const eventID of Object.keys(eventData.content)) {
- for (const user of Object.keys(eventData.content[eventID]["m.read"])) {
- this.moveReadReceipt(user, eventID)
- }
- }
- // console.log("Updated read receipts:", this.userReads)
- }
- }
- }
-
- moveReadReceipt(user, eventID) {
- // check for a previous event to move from
- const prev = this.userReads.get(user)
- if (prev.exists()) {
- const prevID = prev.value()
- if (this.map.has(prevID) && this.map.has(eventID)) {
- // ensure new message came later
- if (this.map.get(eventID).data.origin_server_ts < this.map.get(prevID).data.origin_server_ts) return
- this.map.get(prevID).readBy.delete(user)
- }
- }
- // set on new message
- this.userReads.set(user, eventID)
- if (this.map.has(eventID)) this.map.get(eventID).readBy.add(user)
- }
-
- updateUnreadCount(count) {
- this.room.number.update({unreads: count})
- }
-
- updateNotificationCount(count) {
- this.room.number.update({notifications: count})
- }
-
- removeEvent(id) {
- if (!this.map.has(id)) throw new Error(`Tried to delete event ID ${id} which does not exist`)
- this.map.get(id).removeEvent()
- this.map.delete(id)
- }
-
- getTimeline() {
- return this.reactiveTimeline
- }
-
- async loadScrollback() {
- debug = true
- if (!this.from) return // no more scrollback for this timeline
- const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/rooms/${this.id}/messages`)
- url.searchParams.set("access_token", lsm.get("access_token"))
- url.searchParams.set("from", this.from)
- url.searchParams.set("dir", "b")
- 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)
- if (root.state) this.updateStateEvents(root.state)
- if (root.chunk.length) {
- // there are events to display
- this.updateEvents(root.chunk)
- }
- if (!root.chunk.length || !root.end) {
- // we reached the top of the scrollback
- this.reactiveTimeline.loadMore.remove()
- }
- this.broadcast("afterScrollbackLoad")
- }
-
- send(type, content) {
- const tx = getTxnId()
- const id = `pending$${tx}`
- this.pending.add(id)
- content["chat.carbon.message.pending_id"] = id
- const fakeEvent = {
- type,
- origin_server_ts: Date.now(),
- event_id: id,
- sender: lsm.get("mx_user_id"),
- content,
- pending: true
- }
- this.updateEvents([fakeEvent])
- return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${this.id}/send/m.room.message/${tx}?access_token=${lsm.get("access_token")}`, {
- method: "PUT",
- body: JSON.stringify(content),
- headers: {
- "Content-Type": "application/json"
- }
- })
- }
-}
-
-module.exports = {Timeline}
diff --git a/src/js/typing.js b/src/js/typing.js
deleted file mode 100644
index b6208ef..0000000
--- a/src/js/typing.js
+++ /dev/null
@@ -1,69 +0,0 @@
-const {ElemJS, ejs, q} = require("./basic")
-const {store} = require("./store/store")
-const lsm = require("./lsm")
-
-/**
- * Maximum number of typing users to display all names for.
- * More will be shown as "X users are typing".
- */
-const maxUsers = 4
-
-function getMemberName(mxid) {
- return store.activeRoom.value().getMemberName(mxid)
-}
-
-class Typing extends ElemJS {
- constructor() {
- super(q("#c-typing"))
-
- this.typingUnsubscribe = null
-
- this.message = ejs("span")
- this.child(this.message)
-
- store.activeRoom.subscribe("changeSelf", this.changeRoom.bind(this))
- }
-
- changeRoom() {
- if (this.typingUnsubscribe) {
- this.typingUnsubscribe()
- this.typingUnsubscribe = null
- }
- if (!store.activeRoom.exists()) return
- const room = store.activeRoom.value()
- this.typingUnsubscribe = room.timeline.typing.subscribe("changeSelf", this.render.bind(this))
- this.render()
- }
-
- render() {
- if (!store.activeRoom.exists()) return
- const room = store.activeRoom.value()
- let users = [...room.timeline.typing.value()]
- // don't show own typing status
- users = users.filter(u => u !== lsm.get("mx_user_id"))
- if (users.length === 0) {
- // nobody is typing
- this.removeClass("c-typing--typing")
- } else {
- let message = ""
- if (users.length === 1) {
- message = `${getMemberName(users[0])} is typing...`
- } else if (users.length <= maxUsers) {
- // feel free to rewrite this loop if you know a better way
- for (let i = 0; i < users.length; i++) {
- if (i < users.length-1) {
- message += `${getMemberName(users[i])}, `
- } else {
- message += `and ${getMemberName(users[i])} are typing...`
- }
- }
- } else {
- message = `${users.length} people are typing...`
- }
- this.class("c-typing--typing")
- this.message.text(message)
- }
- }
-}
-
-new Typing()
diff --git a/src/login.pug b/src/login.pug
index 470ee82..105b6bd 100644
--- a/src/login.pug
+++ b/src/login.pug
@@ -1,32 +1,21 @@
doctype html
html
- head
- meta(charset="utf-8")
- title Carbon
- meta(name="viewport" content="width=device-width, initial-scale=1")
- link(rel="stylesheet" type="text/css" href=getStatic("/sass/login.sass"))
- script(type="module" src=getStatic("/js/login.js"))
-
- body
- main.main
- .center-login-container
- h1 Welcome to Carbon!
- form.login-form(method="post" onsubmit="return false")#form
- .data-input
- .form-input-container
- label(for="username") Username
- input(type="text" name="username" autocomplete="username" placeholder="@username:server.tld" pattern="^@?[a-z0-9._=/-]+(?::[a-zA-Z0-9.:\\[\\]-]+)?$" required)#username
-
- .form-input-container
- label(for="password") Password
- input(name="password" autocomplete="current-password" type="password" required)#password
-
- .form-input-container
- label(for="homeserver") Homeserver
- input(type="text" name="homeserver" value="matrix.org" placeholder="matrix.org" required)#homeserver
-
- #feedback
-
- .form-input-container
- input(type="submit" value="Log in")#submit
+ head
+ meta(charset="utf-8")
+ link(rel="stylesheet" type="text/css" href=getStatic("/sass/main.sass"))
+ title Carbon
+ body
+ main.main
+ form
+ div
+ label(for="login") Username
+ input(type="text" name="login" autocomplete="username" placeholder="example:matrix.org" required)#login
+ div
+ label(for="password") Password
+ input(type="text" name="password" autocomplete="current-password" required)#password
+ div
+ label(for="homeserver") Homeserver
+ input(type="text" name="homeserver" value="matrix.org" required)#homeserver
+ div
+ input(type="submit" value="Login")
diff --git a/src/sass/base.sass b/src/sass/base.sass
index dfb9f7a..0232c2e 100644
--- a/src/sass/base.sass
+++ b/src/sass/base.sass
@@ -21,39 +21,3 @@ body
.main
height: 100vh
display: flex
-
-button
- appearance: none
- border: none
- background: none
- color: inherit
- font-family: inherit
- font-size: inherit
- font-style: inherit
- font-weight: inherit
- padding: 0
- margin: 0
- line-height: inherit
- cursor: inherit
-
-// focus resets
-
-:focus
- outline: none
-
-:-moz-focusring
- outline: none
-
-::-moz-focus-inner
- border: 0
-
-select:-moz-focusring
- color: transparent
- text-shadow: 0 0 0 #ddd
-
-body.show-focus
- a, select, button, input, video, div, span
- outline-color: #fff
-
- &:focus
- outline: 2px dotted
diff --git a/src/sass/colors.sass b/src/sass/colors.sass
index a603edd..278fc02 100644
--- a/src/sass/colors.sass
+++ b/src/sass/colors.sass
@@ -5,5 +5,3 @@ $mild: #393c42
$milder: #42454a
$divider: #4b4e54
$muted: #999
-$link: #57bffd
-$notify-highlight: #ffac4b
diff --git a/src/sass/components/chat-banner.sass b/src/sass/components/chat-banner.sass
deleted file mode 100644
index f17acce..0000000
--- a/src/sass/components/chat-banner.sass
+++ /dev/null
@@ -1,50 +0,0 @@
-@use "../colors" as c
-
-.c-chat-banner
- position: sticky
- z-index: 1
- top: 0
- left: 0
- right: 0
- margin-right: 12px
- outline-color: #000
- opacity: 0
- transform: translateY(-40px)
- transition: transform 0.2s ease, opacity 0.2s ease-out
-
- &--active
- opacity: 1
- transform: translateY(0px)
-
- &__inner
- display: grid
- grid-template-columns: 1fr auto
- background: c.$notify-highlight
- color: #000
- margin: 0px 12px
- padding: 0px 12px
- border-radius: 0px 0px 10px 10px
- line-height: 1
- box-shadow: 0px 5px 5px -2px rgba(0, 0, 0, 0.1)
- cursor: pointer
-
- &:hover
- box-shadow: 0px 5px 5px -2px rgba(0, 0, 0, 0.6)
-
- &__part
- padding: 6px 0px 8px
-
- &:hover
- text-decoration: underline
-
- &__part-inner
- display: block
- width: 100% // yes, really.
- text-align: left
-
- &__last
- margin-left: 8px
-
- &__last &__part-inner
- border-left: 1px solid #222
- padding-left: 8px
diff --git a/src/sass/components/chat-input.sass b/src/sass/components/chat-input.sass
index bb82dde..892fee6 100644
--- a/src/sass/components/chat-input.sass
+++ b/src/sass/components/chat-input.sass
@@ -6,14 +6,11 @@
-webkit-appearance: $value
.c-chat-input
- position: relative
width: 100%
border-top: 2px solid c.$divider
background-color: c.$dark
&__textarea
- position: relative
- z-index: 1
width: calc(100% - 40px)
height: 16px + (16px * 1.45)
box-sizing: border-box
diff --git a/src/sass/components/chat.sass b/src/sass/components/chat.sass
index c8bb391..5ca48e0 100644
--- a/src/sass/components/chat.sass
+++ b/src/sass/components/chat.sass
@@ -2,12 +2,11 @@
.c-chat
display: grid
- grid-template-rows: 0 1fr 82px // fixed so that input box height adjustment doesn't mess up scroll
+ grid-template-rows: 1fr 82px // fixed so that input box height adjustment doesn't mess up scroll
align-items: end
flex: 1
&__messages
- position: relative
height: 100%
overflow-y: scroll
scrollbar-color: c.$darkest c.$darker
diff --git a/src/sass/components/groups.sass b/src/sass/components/groups.sass
index 6eca6ec..e91f4cc 100644
--- a/src/sass/components/groups.sass
+++ b/src/sass/components/groups.sass
@@ -36,13 +36,11 @@ $out-width: $base-width + rooms.$list-width
box-sizing: border-box
.c-group
- position: relative
display: flex
align-items: center
padding: $icon-padding / 2 $icon-padding
cursor: pointer
border-radius: 8px
- background-color: c.$darkest
&:hover
background-color: c.$darker
@@ -64,29 +62,6 @@ $out-width: $base-width + rooms.$list-width
overflow: hidden
text-overflow: ellipsis
- &__number
- position: absolute
- right: 240px
- bottom: 0px
- background: #ddd
- color: #000
- font-size: 14px
- line-height: 1
- padding: 3px 4px
- border-radius: 7px
- border: 3px solid c.$darkest
- opacity: 0
- transform: translate(6px, 6px)
- transition: transform 0.15s ease-out, opacity 0.15s ease-out
- pointer-events: none
-
- @at-root .c-group:hover &
- border-color: c.$darker
-
- &--active
- opacity: 1
- transform: translate(0px, 0px)
-
.c-group-marker
position: absolute
top: 5px
diff --git a/src/sass/components/highlighted-code.sass b/src/sass/components/highlighted-code.sass
deleted file mode 100644
index 1ec7290..0000000
--- a/src/sass/components/highlighted-code.sass
+++ /dev/null
@@ -1 +0,0 @@
-@use "../../../node_modules/highlight.js/scss/obsidian"
diff --git a/src/sass/components/messages.sass b/src/sass/components/messages.sass
index 060dec4..7f56834 100644
--- a/src/sass/components/messages.sass
+++ b/src/sass/components/messages.sass
@@ -1,6 +1,6 @@
@use "../colors" as c
-.c-event-groups > *
+.c-event-groups *
overflow-anchor: none
.c-message-group, .c-message-event
@@ -9,8 +9,7 @@
border-top: 1px solid c.$divider
.c-message-group
- display: grid
- grid-template-columns: auto 1fr
+ display: flex
&__avatar
flex-shrink: 0
@@ -24,7 +23,7 @@
border-radius: 50%
&--no-icon
- background-color: #bbb
+ background-color: #48d
&__intro
display: flex
@@ -47,19 +46,9 @@
.c-message
margin-top: 4px
- overflow-wrap: anywhere
opacity: 1
transition: opacity 0.2s ease-out
- &--plain
- white-space: pre-wrap
-
- &--media
- // fix whitespace
- font-size: 0
- margin-top: 8px
- display: flex
-
&--pending
opacity: 0.5
@@ -77,70 +66,18 @@
&:hover
background-color: c.$darker
- &__image
- width: auto
- height: auto
- max-width: 400px
- max-height: 300px
-
- // message formatting rules
-
- code, pre
- border-radius: 4px
- font-size: 0.9em
-
- pre
- background-color: c.$darkest
- padding: 8px
- border: 1px solid c.$divider
- white-space: pre-wrap
-
- code
- background-color: c.$darker
- padding: 2px 4px
-
- a
- color: c.$link
-
- blockquote
- margin-left: 8px
- border-left: 4px solid c.$muted
- padding: 2px 0px 2px 12px
-
- p, pre, blockquote
- margin: 16px 0px
-
- &:first-child
- margin-top: 0px
-
- &:last-child
- margin-bottom: 0px
-
.c-message-event
- // closer spacing than normal messages
- padding-top: 2px
+ padding-top: 10px
padding-left: 6px
- margin-bottom: -4px
- line-height: 1.2
&__inner
- text-indent: -36px
- margin-left: 36px
-
- img
- // let me know if there's a smarter way to line this shit up
- position: relative
- top: -5px
- transform: translateY(50%)
+ display: flex
+ align-items: center
&__icon
margin-right: 8px
-
- &__avatar
- width: 16px
- height: 16px
- border-radius: 50%
- margin: 0px 6px
+ position: relative
+ top: 1px
.c-message-notice
padding: 12px
@@ -150,37 +87,3 @@
padding: 12px
background-color: c.$milder
border-radius: 8px
-
-.c-media
- &__wrapper
- overflow: hidden
- position: relative
-
- &--spoiler
- cursor: pointer
-
- img
- filter: blur(40px)
-
- &--shown img
- filter: none
-
- &__spoiler
- position: absolute
- top: 0
- bottom: 0
- left: 0
- right: 0
- display: flex
- align-items: center
- justify-content: center
- font-size: 18px
- font-weight: 500
- color: #fff
- text-transform: uppercase
- background: rgba(0, 0, 0, 0.3)
- cursor: pointer
- pointer-events: none
-
- &--shown &__spoiler
- display: none
diff --git a/src/sass/components/read-marker.sass b/src/sass/components/read-marker.sass
deleted file mode 100644
index 7e1c572..0000000
--- a/src/sass/components/read-marker.sass
+++ /dev/null
@@ -1,42 +0,0 @@
-@use "../colors" as c
-
-.c-read-marker
- display: none
- position: relative
-
- &--attached
- display: block
-
- &__inner
- position: absolute
- left: -64px
- right: 0px
- height: 2px
- top: 0px
- background-color: c.$notify-highlight
-
- @at-root .c-message:last-child &
- top: 11px
-
- @at-root .c-message-event &
- top: 7px
-
- &__text
- position: absolute
- right: -14px
- top: -9px
- display: flex
- align-items: center
- background-color: c.$notify-highlight
- color: #000
- font-size: 12px
- font-weight: 600
- line-height: 1
- padding: 4px
- border-radius: 5px
- text-transform: uppercase
-
- &__loading
- background-color: #000
- width: 10px
- height: 10px
diff --git a/src/sass/components/rooms.sass b/src/sass/components/rooms.sass
index 670d9ba..462e13c 100644
--- a/src/sass/components/rooms.sass
+++ b/src/sass/components/rooms.sass
@@ -43,23 +43,3 @@ $icon-padding: 8px
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
- flex: 1
-
- &__number
- flex-shrink: 0
- line-height: 1
- padding: 4px 5px
- border-radius: 5px
- font-size: 14px
- pointer-events: none
-
- &--none
- display: none
-
- &--unreads
- background-color: #ddd
- color: #111
-
- &--notifications
- background-color: #ffac4b
- color: #000
diff --git a/src/sass/components/spoilers.sass b/src/sass/components/spoilers.sass
deleted file mode 100644
index e8a1784..0000000
--- a/src/sass/components/spoilers.sass
+++ /dev/null
@@ -1,8 +0,0 @@
-.mx-spoiler
- color: #331911
- background-color: #331911
- outline-color: #fff !important
- cursor: pointer
-
- &--shown
- color: inherit
diff --git a/src/sass/components/typing.sass b/src/sass/components/typing.sass
deleted file mode 100644
index dad8d90..0000000
--- a/src/sass/components/typing.sass
+++ /dev/null
@@ -1,21 +0,0 @@
-@use "../colors" as c
-
-.c-typing
- height: 39px
- background: c.$divider
- position: absolute
- right: 0
- left: 0
- top: 0
- z-index: 0
- margin: 20px
- border-radius: 8px
- padding: 0px 12px
- font-size: 14px
- line-height: 19px
- transform: translateY(0px)
- transition: transform 0.15s ease
- color: #fff
-
- &--typing
- transform: translateY(-21px)
diff --git a/src/sass/loading.sass b/src/sass/loading.sass
deleted file mode 100644
index 9705bbe..0000000
--- a/src/sass/loading.sass
+++ /dev/null
@@ -1,13 +0,0 @@
-@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
diff --git a/src/sass/login.sass b/src/sass/login.sass
deleted file mode 100644
index 74d08ac..0000000
--- a/src/sass/login.sass
+++ /dev/null
@@ -1,73 +0,0 @@
-@use "./base"
-@use "./loading.sass"
-@use "./colors.sass" as c
-
-
-.main
- justify-content: center
- align-items: center
-
-.center-login-container
- display: flex
- flex-flow: column
- justify-content: center
- align-items: center
- width: min(100vw, 450px)
- padding: max(1rem,3vw) 2rem
- margin: 8px
- box-shadow: 0px 2px 10px c.$darkest
- background-color: c.$darker
- border-radius: 5px
-
-.login-form
- align-items: center
- flex: 1 1 auto
- width: 100%
- display: flex
- justify-content: space-around
- flex-flow: column
-
-.data-input
- width: 100%
-
-.form-input-container
- width: 100%
- display: flex
- flex-direction: column
- margin: 1em 0
-
-.form-feedback
- width: 100%
- margin: -0.5em 0 -0.8em
-
-.form-error
- color: red
-
-
-input, button
- font-family: inherit
- font-size: 17px
- background-color: c.$mild
- color: #eee
- width: 100%
- border-radius: 5px
- box-sizing: border-box
- transition: background-color 0.15s ease-out, border-color 0.15s ease-out
- padding: 4px 9px
- border: 0px
-
-input[type="text"],input[type="password"]
- border: 3px solid transparent
- margin: 0.4em 0px
-
- &:hover, &:focus
- border-color: c.$milder
-
-button, input[type="submit"]
- padding: 7px
-
- &:hover
- background-color: c.$milder
-
-label
- font-size: 18px
diff --git a/src/sass/main.sass b/src/sass/main.sass
index c050148..d342bbb 100644
--- a/src/sass/main.sass
+++ b/src/sass/main.sass
@@ -1,13 +1,7 @@
@use "./base"
-@use "./loading"
@use "./components/groups"
@use "./components/rooms"
@use "./components/messages"
@use "./components/chat"
@use "./components/chat-input"
-@use "./components/typing"
@use "./components/anchor"
-@use "./components/highlighted-code"
-@use "./components/read-marker"
-@use "./components/chat-banner"
-@use "./components/spoilers"