Compare commits
No commits in common. "f6b95b2ebd2ba3d9a90a55a0992931497ff592ec" and "ebf6e7ea783ac2279c9e45ec3ec3ed1addf2d3c3" have entirely different histories.
f6b95b2ebd
...
ebf6e7ea78
17 changed files with 629 additions and 573 deletions
10
build.js
10
build.js
|
@ -145,11 +145,9 @@ async function addJS(sourcePath, targetPath) {
|
||||||
await fs.promises.writeFile(pj(buildDir, targetPath), content)
|
await fs.promises.writeFile(pj(buildDir, targetPath), content)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addBundle(sourcePath, targetPath, module = false) {
|
async function addBundle(sourcePath, targetPath) {
|
||||||
let opts = {}
|
|
||||||
if (module) opts.standalone = sourcePath
|
|
||||||
const content = await new Promise(resolve => {
|
const content = await new Promise(resolve => {
|
||||||
browserify([], opts)
|
browserify()
|
||||||
.add(pj(".", sourcePath))
|
.add(pj(".", sourcePath))
|
||||||
.transform(file => {
|
.transform(file => {
|
||||||
let content = ""
|
let content = ""
|
||||||
|
@ -175,6 +173,7 @@ async function addBundle(sourcePath, targetPath, module = false) {
|
||||||
})
|
})
|
||||||
const writer = fs.promises.writeFile(pj(buildDir, targetPath), content)
|
const writer = fs.promises.writeFile(pj(buildDir, targetPath), content)
|
||||||
staticFiles.set(sourcePath, `${targetPath}?static=${hash(content)}`)
|
staticFiles.set(sourcePath, `${targetPath}?static=${hash(content)}`)
|
||||||
|
runHint(sourcePath, content)
|
||||||
await writer
|
await writer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,9 +287,6 @@ async function addBabel(sourcePath, targetPath) {
|
||||||
await addPug(item.source, item.target)
|
await addPug(item.source, item.target)
|
||||||
} else if (item.type === "bundle") {
|
} else if (item.type === "bundle") {
|
||||||
await addBundle(item.source, item.target)
|
await addBundle(item.source, item.target)
|
||||||
} else if (item.type === "module") {
|
|
||||||
// Creates a standalone bundle that can be imported on runtime
|
|
||||||
await addBundle(item.source, item.target, true)
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unknown item type: "+item.type)
|
throw new Error("Unknown item type: "+item.type)
|
||||||
}
|
}
|
||||||
|
|
721
package-lock.json
generated
721
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -11,18 +11,16 @@
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
"@babel/core": "^7.11.1",
|
"@babel/core": "^7.11.1",
|
||||||
"@babel/preset-env": "^7.11.0",
|
"@babel/preset-env": "^7.11.0",
|
||||||
"browserify": "^17.0.0",
|
"browserify": "^17.0.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"dompurify": "^2.2.0",
|
|
||||||
"highlight.js": "^10.3.2",
|
|
||||||
"http-server": "^0.12.3",
|
"http-server": "^0.12.3",
|
||||||
"jshint": "^2.12.0",
|
"jshint": "^2.12.0",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
"pug": "^3.0.0",
|
"pug": "^3.0.0",
|
||||||
"sass": "^1.26.10"
|
"sass": "^1.26.10"
|
||||||
},
|
}
|
||||||
"devDependencies": {}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
const dateFormatter = Intl.DateTimeFormat("default", {hour: "numeric", minute: "numeric", day: "numeric", month: "short", year: "numeric"})
|
|
||||||
|
|
||||||
module.exports = {dateFormatter}
|
|
|
@ -1,31 +0,0 @@
|
||||||
const {ElemJS} = require("../basic")
|
|
||||||
const {lazyLoad} = require("../lazy-load-module")
|
|
||||||
|
|
||||||
class HighlightedCode extends ElemJS {
|
|
||||||
constructor(element) {
|
|
||||||
super(element)
|
|
||||||
if (this.element.tagName === "PRE" && this.element.children.length === 1 && this.element.children[0].tagName === "CODE") {
|
|
||||||
// we shouldn't nest code inside a pre. put the text in the pre directly.
|
|
||||||
const code = this.element.children[0]
|
|
||||||
this.clearChildren()
|
|
||||||
for (const child of code.childNodes) {
|
|
||||||
this.element.appendChild(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.element.textContent.length > 80) {
|
|
||||||
/*
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
lazyLoad("https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10/build/highlight.min.js").then(hljs => hljs.highlightBlock(this.element))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {HighlightedCode}
|
|
|
@ -1,22 +0,0 @@
|
||||||
const {MatrixEvent} = require("./event")
|
|
||||||
const {ejs} = require("../basic")
|
|
||||||
|
|
||||||
class EncryptedMessage extends MatrixEvent {
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
canGroup() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = [EncryptedMessage]
|
|
|
@ -1,54 +0,0 @@
|
||||||
const {ElemJS, ejs} = require("../basic")
|
|
||||||
const {dateFormatter} = require("../date-formatter")
|
|
||||||
|
|
||||||
class MatrixEvent extends ElemJS {
|
|
||||||
constructor(data) {
|
|
||||||
super("div")
|
|
||||||
this.class("c-message")
|
|
||||||
this.data = null
|
|
||||||
this.group = null
|
|
||||||
this.editedAt = null
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {MatrixEvent}
|
|
|
@ -1,55 +0,0 @@
|
||||||
const {MatrixEvent} = require("./event")
|
|
||||||
const {ejs} = require("../basic")
|
|
||||||
|
|
||||||
class MembershipEvent extends MatrixEvent {
|
|
||||||
static canRender(eventData) {
|
|
||||||
return eventData.type === "m.room.member"
|
|
||||||
}
|
|
||||||
|
|
||||||
renderText(text) {
|
|
||||||
this.clearChildren()
|
|
||||||
this.child(
|
|
||||||
ejs("i").text(text)
|
|
||||||
)
|
|
||||||
super.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class JoinedEvent extends MembershipEvent {
|
|
||||||
static canRender(eventData) {
|
|
||||||
return super.canRender(eventData) && eventData.content.membership === "join"
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.renderText("joined the room")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InvitedEvent extends MembershipEvent {
|
|
||||||
static canRender(eventData) {
|
|
||||||
return super.canRender(eventData) && eventData.content.membership === "invite"
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.renderText(`invited ${this.data.content.displayname}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LeaveEvent extends MembershipEvent {
|
|
||||||
static canRender(eventData) {
|
|
||||||
return super.canRender(eventData) && eventData.content.membership === "leave"
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.renderText("left the room")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnknownMembership extends MembershipEvent {
|
|
||||||
render() {
|
|
||||||
this.renderText("unknown membership event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = [JoinedEvent, InvitedEvent, LeaveEvent, UnknownMembership]
|
|
|
@ -1,140 +0,0 @@
|
||||||
const {ejs, ElemJS} = require("../basic")
|
|
||||||
const {HighlightedCode} = require("./components")
|
|
||||||
const DOMPurify = require("dompurify")
|
|
||||||
const {resolveMxc} = require("../functions")
|
|
||||||
const {MatrixEvent} = 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"],
|
|
||||||
"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"
|
|
||||||
],
|
|
||||||
|
|
||||||
// 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(rootNode) {
|
|
||||||
const element = rootNode.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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class HTMLMessage extends MatrixEvent {
|
|
||||||
render() {
|
|
||||||
this.clearChildren()
|
|
||||||
|
|
||||||
let html = this.data.content.formatted_body
|
|
||||||
const content = ejs("div")
|
|
||||||
|
|
||||||
const fragment = cleanHTML(html)
|
|
||||||
content.element.appendChild(fragment)
|
|
||||||
postProcessElements(content)
|
|
||||||
|
|
||||||
this.child(content)
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
canGroup() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TextMessage extends MatrixEvent {
|
|
||||||
render() {
|
|
||||||
this.clearChildren()
|
|
||||||
this.text(this.data.content.body)
|
|
||||||
super.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
static canRender(event) {
|
|
||||||
return event.type === "m.room.message"
|
|
||||||
}
|
|
||||||
|
|
||||||
canGroup() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = [HTMLMessage, TextMessage]
|
|
|
@ -1,19 +0,0 @@
|
||||||
const messageEvent = require("./message")
|
|
||||||
const encryptedEvent = require("./encrypted")
|
|
||||||
const membershipEvent = require("./membership")
|
|
||||||
const unknownEvent = require("./unknown")
|
|
||||||
|
|
||||||
const events = [
|
|
||||||
...messageEvent,
|
|
||||||
...encryptedEvent,
|
|
||||||
...membershipEvent,
|
|
||||||
...unknownEvent,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
function renderEvent(eventData) {
|
|
||||||
const constructor = events.find(e => e.canRender(eventData))
|
|
||||||
return new constructor(eventData)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {renderEvent}
|
|
|
@ -1,19 +0,0 @@
|
||||||
const {MatrixEvent} = require("./event")
|
|
||||||
const {ejs} = require("../basic")
|
|
||||||
|
|
||||||
class UnknownEvent extends MatrixEvent {
|
|
||||||
static canRender() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.clearChildren()
|
|
||||||
this.child(
|
|
||||||
ejs("i").text(`Unknown event of type ${this.data.type}`)
|
|
||||||
)
|
|
||||||
super.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = [UnknownEvent]
|
|
||||||
|
|
|
@ -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}
|
|
|
@ -4,14 +4,13 @@ const {store} = require("./store/store.js")
|
||||||
const {Anchor} = require("./anchor.js")
|
const {Anchor} = require("./anchor.js")
|
||||||
const {Sender} = require("./sender.js")
|
const {Sender} = require("./sender.js")
|
||||||
const lsm = require("./lsm.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
|
let debug = false
|
||||||
|
|
||||||
const NO_MAX = Symbol("NO_MAX")
|
const NO_MAX = Symbol("NO_MAX")
|
||||||
|
|
||||||
|
const dateFormatter = Intl.DateTimeFormat("default", {hour: "numeric", minute: "numeric", day: "numeric", month: "short", year: "numeric"})
|
||||||
|
|
||||||
let sentIndex = 0
|
let sentIndex = 0
|
||||||
|
|
||||||
function getTxnId() {
|
function getTxnId() {
|
||||||
|
@ -39,6 +38,67 @@ function eventSearch(list, event, min = 0, max = NO_MAX) {
|
||||||
else return eventSearch(list, event, mid + 1, max)
|
else return eventSearch(list, event, mid + 1, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Event extends ElemJS {
|
||||||
|
constructor(data) {
|
||||||
|
super("div")
|
||||||
|
this.class("c-message")
|
||||||
|
this.data = null
|
||||||
|
this.group = null
|
||||||
|
this.editedAt = null
|
||||||
|
this.update(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// predicates
|
||||||
|
|
||||||
|
canGroup() {
|
||||||
|
return this.data.type === "m.room.message"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.data.type === "m.room.message") {
|
||||||
|
this.text(this.data.content.body)
|
||||||
|
} else if (this.data.type === "m.room.member") {
|
||||||
|
if (this.data.content.membership === "join") {
|
||||||
|
this.child(ejs("i").text("joined the room"))
|
||||||
|
} else if (this.data.content.membership === "invite") {
|
||||||
|
this.child(ejs("i").text(`invited ${this.data.content.displayname} to the room`))
|
||||||
|
} else if (this.data.content.membership === "leave") {
|
||||||
|
this.child(ejs("i").text("left the room"))
|
||||||
|
} else {
|
||||||
|
this.child(ejs("i").text("unknown membership event"))
|
||||||
|
}
|
||||||
|
} else if (this.data.type === "m.room.encrypted") {
|
||||||
|
this.child(ejs("i").text("Carbon does not yet support encrypted messages."))
|
||||||
|
} else {
|
||||||
|
this.child(ejs("i").text(`Unsupported event type ${this.data.type}`))
|
||||||
|
}
|
||||||
|
if (this.editedAt) {
|
||||||
|
this.child(ejs("span").class("c-message__edited").text("(edited)").attribute("title", "at " + dateFormatter.format(this.editedAt)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class EventGroup extends ElemJS {
|
class EventGroup extends ElemJS {
|
||||||
constructor(reactive, list) {
|
constructor(reactive, list) {
|
||||||
|
@ -254,7 +314,7 @@ class Timeline extends Subscribable {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// add new event
|
// add new event
|
||||||
const event = renderEvent(eventData)
|
const event = new Event(eventData)
|
||||||
this.map.set(id, event)
|
this.map.set(id, event)
|
||||||
this.reactiveTimeline.addEvent(event)
|
this.reactiveTimeline.addEvent(event)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,4 +5,3 @@ $mild: #393c42
|
||||||
$milder: #42454a
|
$milder: #42454a
|
||||||
$divider: #4b4e54
|
$divider: #4b4e54
|
||||||
$muted: #999
|
$muted: #999
|
||||||
$link: #57bffd
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
@use "../../../node_modules/highlight.js/scss/obsidian"
|
|
|
@ -1,6 +1,6 @@
|
||||||
@use "../colors" as c
|
@use "../colors" as c
|
||||||
|
|
||||||
.c-event-groups > *
|
.c-event-groups *
|
||||||
overflow-anchor: none
|
overflow-anchor: none
|
||||||
|
|
||||||
.c-message-group, .c-message-event
|
.c-message-group, .c-message-event
|
||||||
|
@ -67,33 +67,6 @@
|
||||||
&:hover
|
&:hover
|
||||||
background-color: c.$darker
|
background-color: c.$darker
|
||||||
|
|
||||||
// message formatting rules
|
|
||||||
|
|
||||||
code, pre
|
|
||||||
border-radius: 4px
|
|
||||||
font-size: 0.9em
|
|
||||||
|
|
||||||
pre
|
|
||||||
background-color: c.$darkest
|
|
||||||
padding: 8px
|
|
||||||
border: 1px solid c.$divider
|
|
||||||
|
|
||||||
code
|
|
||||||
background-color: c.$darker
|
|
||||||
padding: 2px 4px
|
|
||||||
|
|
||||||
a
|
|
||||||
color: c.$link
|
|
||||||
|
|
||||||
p, pre
|
|
||||||
margin: 16px 0px
|
|
||||||
|
|
||||||
&:first-child
|
|
||||||
margin-top: 0px
|
|
||||||
|
|
||||||
&:last-child
|
|
||||||
margin-bottom: 0px
|
|
||||||
|
|
||||||
.c-message-event
|
.c-message-event
|
||||||
padding-top: 10px
|
padding-top: 10px
|
||||||
padding-left: 6px
|
padding-left: 6px
|
||||||
|
|
|
@ -5,5 +5,4 @@
|
||||||
@use "./components/chat"
|
@use "./components/chat"
|
||||||
@use "./components/chat-input"
|
@use "./components/chat-input"
|
||||||
@use "./components/anchor"
|
@use "./components/anchor"
|
||||||
@use "./components/highlighted-code"
|
|
||||||
@use "./loading"
|
@use "./loading"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue