diff --git a/build/index.html b/build/index.html deleted file mode 100644 index 29b927a..0000000 --- a/build/index.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - Carbon - - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- - \ No newline at end of file diff --git a/build/static/Anchor.js b/build/static/Anchor.js deleted file mode 100644 index d07e239..0000000 --- a/build/static/Anchor.js +++ /dev/null @@ -1,15 +0,0 @@ -import {ElemJS} from "../static/basic.js?static=4212436742" - -class Anchor extends ElemJS { - constructor() { - super("div") - this.class("c-anchor") - } - - scroll() { - // console.log("anchor scrolled") - this.element.scrollIntoView({block: "start"}) - } -} - -export {Anchor} diff --git a/build/static/Timeline.js b/build/static/Timeline.js deleted file mode 100644 index 9383763..0000000 --- a/build/static/Timeline.js +++ /dev/null @@ -1,282 +0,0 @@ -import {ElemJS, ejs} from "../static/basic.js?static=4212436742" -import {Subscribable} from "../static/store/Subscribable.js?static=19b7e44aa6" -import {store} from "../static/store/store.js?static=ce3066287b" -import {Anchor} from "../static/Anchor.js?static=85efd1a836" -import * as lsm from "../static/lsm.js?static=aed2c7ca35" -import {resolveMxc} from "../static/functions.js?static=e3784c70ce" - -const dateFormatter = Intl.DateTimeFormat("default", {hour: "numeric", minute: "numeric", day: "numeric", month: "short", year: "numeric"}) - -let sentIndex = 0 - -function getTxnId() { - return Date.now() + (sentIndex++) -} - -function eventSearch(list, event, min = 0, max = -1) { - if (list.length === 0) return {success: false, i: 0} - - if (max === -1) 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 Event extends ElemJS { - constructor(data) { - super("div") - this.class("c-message") - this.data = null - this.group = null - this.update(data) - } - - setGroup(group) { - this.group = group - } - - 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") - this.text(this.data.content.body) - } -} - -class Sender { - constructor(roomID, mxid) { - this.sender = store.rooms.get(roomID).value().members.get(mxid) - this.sender.subscribe("changeSelf", this.update.bind(this)) - this.name = new ElemJS("div").class("c-message-group__name") - this.avatar = new ElemJS("div").class("c-message-group__avatar") - this.update() - } - - update() { - if (this.sender.exists()) { - // name - this.name.text(this.sender.value().content.displayname) - - // avatar - this.avatar.clearChildren() - if (this.sender.value().content.avatar_url) { - this.avatar.child( - ejs("img").class("c-message-group__icon").attribute("src", resolveMxc(this.sender.value().content.avatar_url, 96, "crop")) - ) - } else { - this.avatar.child( - ejs("div").class("c-message-group__icon", "c-message-group__icon--no-icon") - ) - } - } - } -} - -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 - ) - ) - } - - 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) - } -} - -class ReactiveTimeline extends ElemJS { - constructor(id, list) { - super("div") - this.class("c-event-groups") - this.id = id - this.list = list - this.render() - } - - addEvent(event) { - const search = eventSearch(this.list, event) - // console.log(search, this.list.map(l => l.data.sender), event.data) - if (!search.success && search.i >= 1) this.tryAddGroups(event, [search.i-1, search.i]) - else this.tryAddGroups(event, [search.i]) - } - - tryAddGroups(event, indices) { - const success = indices.some(i => { - if (!this.list[i]) { - // if (printed++ < 100) console.log("tryadd success, created group") - const group = new EventGroup(this, [event]) - this.list.splice(i, 0, group) - this.childAt(i, group) - return true - } else if (this.list[i] && 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) - } - - removeGroup(group) { - const index = this.list.indexOf(group) - this.list.splice(index, 1) - group.remove() // should get everything else - } - - render() { - this.clearChildren() - this.list.forEach(group => this.child(group)) - this.anchor = new Anchor() - this.child(this.anchor) - } -} - -class Timeline extends Subscribable { - constructor(id) { - super() - Object.assign(this.events, { - beforeChange: [], - afterChange: [] - }) - Object.assign(this.eventDeps, { - beforeChange: [], - afterChange: [] - }) - this.id = id - this.list = [] - this.map = new Map() - this.reactiveTimeline = new ReactiveTimeline(id, []) - this.latest = 0 - this.pending = new Set() - } - - updateEvents(events) { - this.broadcast("beforeChange") - for (const eventData of events) { - this.latest = Math.max(this.latest, eventData.origin_server_ts) - let id = eventData.event_id - if (eventData.sender === lsm.get("mx_user_id") && eventData.content && this.pending.has(eventData.content["chat.carbon.message.pending_id"])) { - id = eventData.content["chat.carbon.message.pending_id"] - } - if (this.map.has(id)) { - this.map.get(id).update(eventData) - } else { - const event = new Event(eventData) - this.map.set(id, event) - this.reactiveTimeline.addEvent(event) - } - } - this.broadcast("afterChange") - } - - 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 - } - - send(body) { - const tx = getTxnId() - const id = `pending$${tx}` - this.pending.add(id) - const content = { - msgtype: "m.text", - body, - "chat.carbon.message.pending_id": id - } - const fakeEvent = { - 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" - } - })/*.then(() => { - const subscription = () => { - this.removeEvent(id) - this.unsubscribe("afterChange", subscription) - } - this.subscribe("afterChange", subscription) - })*/ - } -/* - getGroupedEvents() { - let currentSender = Symbol("N/A") - let groups = [] - let currentGroup = [] - for (const event of this.list) { - if (event.sender === currentSender) { - currentGroup.push(event) - } else { - if (currentGroup.length) groups.push(currentGroup) - currentGroup = [event] - currentSender = event.sender - } - } - if (currentGroup.length) groups.push(currentGroup) - return groups - } - */ -} - -export {Timeline} diff --git a/build/static/basic.js b/build/static/basic.js deleted file mode 100644 index 1f3e695..0000000 --- a/build/static/basic.js +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Shortcut for querySelector. - * @template {HTMLElement} T - * @returns {T} - */ -const q = s => document.querySelector(s); -/** - * Shortcut for querySelectorAll. - * @template {HTMLElement} T - * @returns {T[]} - */ -const qa = s => document.querySelectorAll(s); - -/** - * An easier, chainable, object-oriented way to create and update elements - * and children according to related data. Subclass ElemJS to create useful, - * advanced data managers, or just use it inline to quickly make a custom element. - * Created by Cadence Ember in 2018. - */ -class ElemJS { - constructor(type) { - if (type instanceof HTMLElement) { - // If passed an existing element, bind to it - this.bind(type); - } else { - // Otherwise, create a new detached element to bind to - this.bind(document.createElement(type)); - } - this.children = []; - } - - /** Bind this construct to an existing element on the page. */ - bind(element) { - this.element = element; - this.element.js = this; - return this; - } - - /** Add a class. */ - class() { - for (let name of arguments) if (name) this.element.classList.add(name); - return this; - } - - /** Remove a class. */ - removeClass() { - for (let name of arguments) if (name) this.element.classList.remove(name); - return this; - } - - /** Set a JS property on the element. */ - direct(name, value) { - if (name) this.element[name] = value; - return this; - } - - /** Set an attribute on the element. */ - attribute(name, value) { - if (name) this.element.setAttribute(name, value != undefined ? value : ""); - return this; - } - - /** Set a style on the element. */ - style(name, value) { - if (name) this.element.style[name] = value; - return this; - } - - /** Set the element's ID. */ - id(name) { - if (name) this.element.id = name; - return this; - } - - /** Attach a callback function to an event on the element. */ - on(name, callback) { - this.element.addEventListener(name, callback); - return this; - } - - /** Set the element's text. */ - text(name) { - this.element.innerText = name; - return this; - } - - /** Create a text node and add it to the element. */ - addText(name) { - const node = document.createTextNode(name); - this.element.appendChild(node); - return this; - } - - /** Set the element's HTML content. */ - html(name) { - this.element.innerHTML = name; - return this; - } - - /** - * Add children to the element. - * Children can either be an instance of ElemJS, in - * which case the element will be appended as a child, - * or a string, in which case the string will be added as a text node. - * Each child should be a parameter to this method. - */ - child(...children) { - for (const toAdd of children) { - if (typeof toAdd === "object" && toAdd !== null) { - // Should be an instance of ElemJS, so append as child - toAdd.parent = this; - this.element.appendChild(toAdd.element); - this.children.push(toAdd); - } else if (typeof toAdd === "string") { - // Is a string, so add as text node - this.addText(toAdd); - } - } - return this; - } - - childAt(index, toAdd) { - if (typeof toAdd === "object" && toAdd !== null) { - toAdd.parent = this; - this.children.splice(index, 0, toAdd); - if (index >= this.element.childNodes.length) { - this.element.appendChild(toAdd.element) - } else { - this.element.childNodes[index].insertAdjacentElement("beforebegin", toAdd.element) - } - } - } - - /** - * Remove all children from the element. - */ - clearChildren() { - this.children.length = 0; - while (this.element.lastChild) this.element.removeChild(this.element.lastChild); - } - - /** - * Remove this element. - */ - remove() { - let index; - if (this.parent && (index = this.parent.children.indexOf(this)) !== -1) { - this.parent.children.splice(index, 1); - } - this.parent = null; - this.element.remove(); - } -} - -/** Shortcut for `new ElemJS`. */ -function ejs(tag) { - return new ElemJS(tag); -} - -export {q, qa, ElemJS, ejs} diff --git a/build/static/channels.svg b/build/static/channels.svg deleted file mode 100644 index 121c251..0000000 --- a/build/static/channels.svg +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - diff --git a/build/static/chat-input.js b/build/static/chat-input.js deleted file mode 100644 index ca01cc1..0000000 --- a/build/static/chat-input.js +++ /dev/null @@ -1,37 +0,0 @@ -import {q} from "../static/basic.js?static=4212436742" -import {store} from "../static/store/store.js?static=ce3066287b" -import * as lsm from "../static/lsm.js?static=aed2c7ca35" -import {chat} from "../static/chat.js?static=b01b3716ff" - -const input = q("#c-chat-textarea") - -store.activeRoom.subscribe("changeSelf", () => { - if (store.activeRoom.exists()) { - input.focus() - } -}) - -input.addEventListener("keydown", event => { - if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey) { - event.preventDefault() - const body = input.value - send(input.value) - input.value = "" - fixHeight() - } -}) - -input.addEventListener("input", () => { - fixHeight() -}) - -function fixHeight() { - input.style.height = "0px" - // console.log(input.clientHeight, input.scrollHeight) - input.style.height = (input.scrollHeight + 1) + "px" -} - -function send(body) { - if (!store.activeRoom.exists()) return - return store.activeRoom.value().timeline.send(body) -} diff --git a/build/static/chat.js b/build/static/chat.js deleted file mode 100644 index 8bb324d..0000000 --- a/build/static/chat.js +++ /dev/null @@ -1,65 +0,0 @@ -import {ElemJS, q, ejs} from "../static/basic.js?static=4212436742" -import {store} from "../static/store/store.js?static=ce3066287b" - -const chatMessages = q("#c-chat-messages") - -class Chat extends ElemJS { - constructor() { - super(q("#c-chat")) - - this.removableSubscriptions = [] - - store.activeRoom.subscribe("changeSelf", this.changeRoom.bind(this)) - - this.render() - } - - unsubscribe() { - this.removableSubscriptions.forEach(({name, target, subscription}) => { - target.unsubscribe(name, subscription) - }) - this.removableSubscriptions.length = 0 - } - - changeRoom() { - // disconnect from the previous room - this.unsubscribe() - // connect to the new room's timeline updater - if (store.activeRoom.exists()) { - const timeline = store.activeRoom.value().timeline - const subscription = () => { - // scroll anchor does not work if the timeline is scrolled to the top. - // at the start, when there are not enough messages for a full screen, this is the case. - // once there are enough messages that scrolling is necessary, we initiate a scroll down to activate the scroll anchor. - let oldDifference = chatMessages.scrollHeight - chatMessages.clientHeight - setTimeout(() => { - let newDifference = chatMessages.scrollHeight - chatMessages.clientHeight - // console.log("height difference", oldDifference, newDifference) - if (oldDifference < 24) { // this is jank - this.element.parentElement.scrollBy(0, 1000) - } - }, 0) - } - const name = "beforeChange" - this.removableSubscriptions.push({name, target: timeline, subscription}) - timeline.subscribe(name, subscription) - } - this.render() - } - - render() { - this.clearChildren() - if (store.activeRoom.exists()) { - const reactiveTimeline = store.activeRoom.value().timeline.getTimeline() - this.child(reactiveTimeline) - setTimeout(() => { - this.element.parentElement.scrollBy(0, 1) - reactiveTimeline.anchor.scroll() - }, 0) - } - } -} - -const chat = new Chat() - -export {chat} diff --git a/build/static/directs.svg b/build/static/directs.svg deleted file mode 100644 index b1ed08b..0000000 --- a/build/static/directs.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/build/static/groups.js b/build/static/groups.js deleted file mode 100644 index 3500e27..0000000 --- a/build/static/groups.js +++ /dev/null @@ -1,14 +0,0 @@ -import {q} from "../static/basic.js?static=4212436742" - -let state = "CLOSED" - -const groups = q("#c-groups-display") -const rooms = q("#c-rooms") - -groups.addEventListener("click", () => { - groups.classList.add("c-groups__display--closed") -}) - -rooms.addEventListener("mouseout", () => { - groups.classList.remove("c-groups__display--closed") -}) diff --git a/build/static/join-event.svg b/build/static/join-event.svg deleted file mode 100644 index 042e3bd..0000000 --- a/build/static/join-event.svg +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/build/static/lsm.js b/build/static/lsm.js deleted file mode 100644 index 7338343..0000000 --- a/build/static/lsm.js +++ /dev/null @@ -1,11 +0,0 @@ -function get(name) { - return localStorage.getItem(name) -} - -function set(name, value) { - return localStorage.setItem(name, value) -} - -window.lsm = {get, set} - -export {get, set} diff --git a/build/static/main.css b/build/static/main.css deleted file mode 100644 index e78cd1a..0000000 --- a/build/static/main.css +++ /dev/null @@ -1,261 +0,0 @@ -@font-face { - font-family: Whitney; - font-weight: 400; - src: url(/static/whitney-400.woff?static=0f823bc4b5) format("woff2"); -} -@font-face { - font-family: Whitney; - font-weight: 500; - src: url(/static/whitney-500.woff?static=ba33ed18fe) format("woff2"); -} -body { - font-family: sans-serif; - background-color: #36393e; - color: #ddd; - font-size: 16px; - font-family: Whitney; - line-height: 1.45; - margin: 0; - height: 100vh; - font-weight: 400; -} - -.main { - height: 100vh; - display: flex; -} - -.c-rooms { - background-color: #2f3135; - padding: 8px; - width: 240px; - font-size: 18px; - font-weight: 500; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: #202224 #2f3135; - flex-shrink: 0; -} - -.c-room { - display: flex; - align-items: center; - padding: 6px 8px; - margin: 2px 0; - cursor: pointer; - border-radius: 8px; -} -.c-room:not(.c-room--active):hover { - background-color: #393c42; -} -.c-room--active { - background-color: #42454a; -} -.c-room__icon { - width: 32px; - height: 32px; - margin-right: 8px; - border-radius: 50%; - flex-shrink: 0; -} -.c-room__icon--no-icon { - background-color: #bbb; -} -.c-room__name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.c-groups { - position: relative; - width: 80px; - flex-shrink: 0; - font-size: 22px; - font-weight: 500; -} -.c-groups__display { - background-color: #202224; - overflow: hidden; - width: 80px; - position: absolute; - left: 0; - top: 0; - bottom: 0; - right: 0; - transition: width ease-out 0.12s; - scrollbar-width: thin; - scrollbar-color: #42454a #202224; -} -.c-groups__display:not(.c-groups__display--closed):hover { - width: 320px; - overflow-y: auto; -} -.c-groups__container { - width: 320px; - padding: 8px; -} - -.c-group { - display: flex; - align-items: center; - padding: 4px 8px; - cursor: pointer; - border-radius: 8px; -} -.c-group:hover { - background-color: #2f3135; -} -.c-group__icon { - width: 48px; - height: 48px; - background-color: #393c42; - border-radius: 50%; - margin-right: 16px; - flex-shrink: 0; - transition: border-radius ease-out 0.12s; -} -.c-group--active .c-group__icon { - border-radius: 28%; -} -.c-group__name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.c-group-marker { - position: absolute; - top: 5px; - opacity: 0; - transform: translateY(8px); - transition: transform ease 0.12s, opacity ease-out 0.12s; - height: 46px; - width: 6px; - background-color: #ccc; - border-radius: 0px 6px 6px 0px; -} - -.c-event-groups * { - overflow-anchor: none; -} - -.c-message-group, .c-message-event { - margin-top: 12px; - padding-top: 12px; - border-top: 1px solid #4b4e54; -} - -.c-message-group { - display: flex; -} -.c-message-group__avatar { - flex-shrink: 0; - margin-right: 16px; - cursor: pointer; -} -.c-message-group__icon { - width: 40px; - height: 40px; - border-radius: 50%; -} -.c-message-group__icon--no-icon { - background-color: #48d; -} -.c-message-group__intro { - display: flex; - align-items: baseline; -} -.c-message-group__name { - color: #5bf; - margin: -2px 0px -3px; - font-size: 19px; - font-weight: 500; - cursor: pointer; -} -.c-message-group__name:hover { - text-decoration: underline; -} -.c-message-group__date { - font-size: 14px; - margin-left: 9px; - color: #999; -} - -.c-message { - margin-top: 4px; - opacity: 1; - transition: opacity 0.2s ease-out; -} -.c-message--pending { - opacity: 0.5; -} - -.c-message-event { - padding-top: 10px; - padding-left: 6px; -} -.c-message-event__inner { - display: flex; - align-items: center; -} -.c-message-event__icon { - margin-right: 8px; - position: relative; - top: 1px; -} - -.c-message-notice { - padding: 12px; -} -.c-message-notice__inner { - text-align: center; - padding: 12px; - background-color: #42454a; - border-radius: 8px; -} - -.c-chat { - display: grid; - grid-template-rows: 1fr 82px; - align-items: end; - flex: 1; -} -.c-chat__messages { - height: 100%; - overflow-y: scroll; - scrollbar-color: #202224 #2f3135; - display: grid; - align-items: end; -} -.c-chat__inner { - padding: 20px 20px 20px; -} - -.c-chat-input { - width: 100%; - border-top: 2px solid #4b4e54; - background-color: #36393e; -} -.c-chat-input__textarea { - width: calc(100% - 40px); - height: 39.2px; - box-sizing: border-box; - margin: 20px; - padding: 8px; - font-family: inherit; - font-size: inherit; - background-color: #42454a; - color: #fff; - appearance: none; - -moz-appearance: none; - -webkit-appearance: none; - border: none; - border-radius: 8px; - resize: none; -} - -.c-anchor { - overflow-anchor: auto; - height: 1px; -} \ No newline at end of file diff --git a/build/static/room-picker.js b/build/static/room-picker.js deleted file mode 100644 index 75e6c0b..0000000 --- a/build/static/room-picker.js +++ /dev/null @@ -1,230 +0,0 @@ -import {q, ElemJS, ejs} from "../static/basic.js?static=4212436742" -import {store} from "../static/store/store.js?static=ce3066287b" -import {SubscribeMapList} from "../static/store/SubscribeMapList.js?static=b2732c5460" -import {SubscribeValue} from "../static/store/SubscribeValue.js?static=215b6a5099" -import {Timeline} from "../static/Timeline.js?static=1393b78916" -import * as lsm from "../static/lsm.js?static=aed2c7ca35" -import {resolveMxc} from "../static/functions.js?static=e3784c70ce" - -class ActiveGroupMarker extends ElemJS { - constructor() { - super(q("#c-group-marker")) - store.activeGroup.subscribe("changeSelf", this.render.bind(this)) - } - - render() { - if (store.activeGroup.exists()) { - const group = store.activeGroup.value() - this.style("opacity", 1) - this.style("transform", `translateY(${group.element.offsetTop}px)`) - } else { - this.style("opacity", 0) - } - } -} - -const activeGroupMarker = new ActiveGroupMarker() - -class Group extends ElemJS { - constructor(key, data) { - super("div") - - this.data = data - this.order = this.data.order - - this.class("c-group") - this.child( - (this.data.icon - ? ejs("img").class("c-group__icon").attribute("src", this.data.icon) - : ejs("div").class("c-group__icon") - ), - ejs("div").class("c-group__name").text(this.data.name) - ) - - this.on("click", this.onClick.bind(this)) - - store.activeGroup.subscribe("changeSelf", this.render.bind(this)) - } - - render() { - const active = store.activeGroup.value() === this - this.element.classList[active ? "add" : "remove"]("c-group--active") - } - - onClick() { - store.activeGroup.set(this) - } -} - -class Room extends ElemJS { - constructor(id, data) { - super("div") - - this.id = id - this.data = data - this.timeline = new Timeline(this.id) - this.group = null - this.members = new SubscribeMapList(SubscribeValue) - - this.class("c-room") - - this.on("click", this.onClick.bind(this)) - store.activeRoom.subscribe("changeSelf", this.render.bind(this)) - - this.render() - } - - get order() { - if (this.group) { - 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 { - return -this.timeline.latest - } - } - - getName() { - let name = this.data.state.events.find(e => e.type === "m.room.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(", ") - } - return name - } - - getIcon() { - const avatar = this.data.state.events.find(e => e.type === "m.room.avatar") - if (avatar) { - return resolveMxc(avatar.content.url || avatar.content.avatar_url, 32, "crop") - } else { - return null - } - } - - isDirect() { - return store.directs.has(this.id) - } - - setGroup(id) { - this.group = id - } - - getGroup() { - if (this.group) { - return store.groups.get(this.group).value() - } else { - return this.isDirect() ? store.groups.get("directs").value() : store.groups.get("channels").value() - } - } - - onClick() { - store.activeRoom.set(this) - } - - render() { - this.clearChildren() - // data - const icon = this.getIcon() - if (icon) { - this.child(ejs("img").class("c-room__icon").attribute("src", icon)) - } else { - this.child(ejs("div").class("c-room__icon", "c-room__icon--no-icon")) - } - this.child(ejs("div").class("c-room__name").text(this.getName())) - // active - const active = store.activeRoom.value() === this - this.element.classList[active ? "add" : "remove"]("c-room--active") - } -} - -class Rooms extends ElemJS { - constructor() { - super(q("#c-rooms")) - - this.roomData = [] - this.rooms = [] - - store.rooms.subscribe("askAdd", this.askAdd.bind(this)) - store.rooms.subscribe("addItem", this.addItem.bind(this)) - // store.rooms.subscribe("changeItem", this.render.bind(this)) - store.activeGroup.subscribe("changeSelf", this.render.bind(this)) - store.directs.subscribe("changeItem", this.render.bind(this)) - store.newEvents.subscribe("changeSelf", this.sort.bind(this)) - - this.render() - } - - sort() { - store.rooms.sort() - this.render() - } - - askAdd(event, {key, data}) { - const room = new Room(key, data) - store.rooms.addEnd(key, room) - } - - addItem(event, key) { - const room = store.rooms.get(key).value() - if (room.getGroup() === store.activeGroup.value()) { - this.child(room) - } - } - - render() { - this.clearChildren() - let first = null - // set room list - store.rooms.forEach((id, room) => { - if (room.value().getGroup() === store.activeGroup.value()) { - if (!first) first = room.value() - this.child(room.value()) - } - }) - // if needed, change the active room to be an item in the room list - if (!store.activeRoom.exists() || store.activeRoom.value().getGroup() !== store.activeGroup.value()) { - if (first) { - store.activeRoom.set(first) - } else { - store.activeRoom.delete() - } - } - } -} -const rooms = new Rooms() - -class Groups extends ElemJS { - constructor() { - super(q("#c-groups-list")) - - store.groups.subscribe("askAdd", this.askAdd.bind(this)) - store.groups.subscribe("changeItem", this.render.bind(this)) - } - - askAdd(event, {key, data}) { - const group = new Group(key, data) - store.groups.addEnd(key, group) - store.groups.sort() - } - - render() { - this.clearChildren() - store.groups.forEach((key, item) => { - this.child(item.value()) - }) - } -} -const groups = new Groups() diff --git a/build/static/store/Subscribable.js b/build/static/store/Subscribable.js deleted file mode 100644 index 6c7640e..0000000 --- a/build/static/store/Subscribable.js +++ /dev/null @@ -1,38 +0,0 @@ -class Subscribable { - constructor() { - this.events = { - addSelf: [], - editSelf: [], - removeSelf: [], - changeSelf: [] - } - this.eventDeps = { - addSelf: ["changeSelf"], - editSelf: ["changeSelf"], - removeSelf: ["changeSelf"], - changeSelf: [] - } - } - - subscribe(event, callback) { - if (this.events[event]) { - this.events[event].push(callback) - } else { - throw new Error(`Cannot subscribe to non-existent event ${event}, available events are: ${Object.keys(this.events).join(", ")}`) - } - } - - unsubscribe(event, callback) { - const index = this.events[event].indexOf(callback) - if (index === -1) throw new Error(`Tried to remove a nonexisting subscription from event ${event}`) - this.events[event].splice(index, 1) - } - - broadcast(event, data) { - this.eventDeps[event].concat(event).forEach(eventName => { - this.events[eventName].forEach(f => f(event, data)) - }) - } -} - -export {Subscribable} diff --git a/build/static/store/SubscribeMapList.js b/build/static/store/SubscribeMapList.js deleted file mode 100644 index 189119c..0000000 --- a/build/static/store/SubscribeMapList.js +++ /dev/null @@ -1,86 +0,0 @@ -import {Subscribable} from "../../static/store/Subscribable.js?static=19b7e44aa6" -import {SubscribeValue} from "../../static/store/SubscribeValue.js?static=215b6a5099" - -class SubscribeMapList extends Subscribable { - constructor(inner) { - super() - this.inner = inner - Object.assign(this.events, { - addItem: [], - deleteItem: [], - editItem: [], - changeItem: [], - askAdd: [] - }) - Object.assign(this.eventDeps, { - addItem: ["changeItem"], - deleteItem: ["changeItem"], - editItem: ["changeItem"], - changeItem: [], - askAdd: [] - }) - this.map = new Map() - this.list = [] - } - - 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) { - this.list.forEach(key => f(key, this.get(key))) - } - - askAdd(key, data) { - this.broadcast("askAdd", {key, data}) - } - - addStart(key, value) { - this._add(key, value, true) - } - - addEnd(key, value) { - this._add(key, value, false) - } - - sort() { - this.list.sort((a, b) => { - const orderA = this.map.get(a).value().order - const orderB = this.map.get(b).value().order - return orderA - orderB - }) - this.broadcast("changeItem") - } - - _add(key, value, start) { - 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) - if (start) this.list.unshift(key) - else this.list.push(key) - this.broadcast("addItem", key) - } - return s - } -} - -export {SubscribeMapList} diff --git a/build/static/store/SubscribeSet.js b/build/static/store/SubscribeSet.js deleted file mode 100644 index e43128f..0000000 --- a/build/static/store/SubscribeSet.js +++ /dev/null @@ -1,50 +0,0 @@ -import {Subscribable} from "../../static/store/Subscribable.js?static=19b7e44aa6" - -class SubscribeSet extends Subscribable { - constructor() { - super() - Object.assign(this.events, { - addItem: [], - deleteItem: [], - changeItem: [], - askAdd: [] - }) - Object.assign(this.eventDeps, { - addItem: ["changeItem"], - deleteItem: ["changeItem"], - changeItem: [], - askAdd: [] - }) - this.set = new Set() - } - - has(key) { - return this.set.has(key) - } - - forEach(f) { - for (const key of this.set.keys()) { - f(key) - } - } - - askAdd(key) { - this.broadcast("askAdd", key) - } - - add(key) { - if (!this.set.has(key)) { - this.set.add(key) - this.broadcast("addItem", key) - } - } - - delete(key) { - if (this.set.has(key)) { - this.set.delete(key) - this.broadcast("deleteItem", key) - } - } -} - -export {SubscribeSet} diff --git a/build/static/store/SubscribeValue.js b/build/static/store/SubscribeValue.js deleted file mode 100644 index e8dceb6..0000000 --- a/build/static/store/SubscribeValue.js +++ /dev/null @@ -1,47 +0,0 @@ -import {Subscribable} from "../../static/store/Subscribable.js?static=19b7e44aa6" - -class SubscribeValue extends Subscribable { - constructor() { - super() - this.hasData = false - this.data = null - } - - exists() { - return this.hasData - } - - value() { - if (this.hasData) return this.data - else return null - } - - set(data) { - const exists = this.exists() - this.data = data - this.hasData = true - if (exists) { - this.broadcast("editSelf", this.data) - } else { - this.broadcast("addSelf", this.data) - } - return this - } - - edit(f) { - if (this.exists()) { - f(this.data) - this.set(this.data) - } else { - throw new Error("Tried to edit a SubscribeValue that had no value") - } - } - - delete() { - this.hasData = false - this.broadcast("removeSelf") - return this - } -} - -export {SubscribeValue} diff --git a/build/static/store/store.js b/build/static/store/store.js deleted file mode 100644 index 1450a68..0000000 --- a/build/static/store/store.js +++ /dev/null @@ -1,17 +0,0 @@ -import {Subscribable} from "../../static/store/Subscribable.js?static=19b7e44aa6" -import {SubscribeMapList} from "../../static/store/SubscribeMapList.js?static=b2732c5460" -import {SubscribeSet} from "../../static/store/SubscribeSet.js?static=39a1c0a2a4" -import {SubscribeValue} from "../../static/store/SubscribeValue.js?static=215b6a5099" - -const store = { - groups: new SubscribeMapList(SubscribeValue), - rooms: new SubscribeMapList(SubscribeValue), - directs: new SubscribeSet(), - activeGroup: new SubscribeValue(), - activeRoom: new SubscribeValue(), - newEvents: new Subscribable() -} - -window.store = store - -export {store} diff --git a/build/static/sync/sync.js b/build/static/sync/sync.js deleted file mode 100644 index 3b6fc34..0000000 --- a/build/static/sync/sync.js +++ /dev/null @@ -1,127 +0,0 @@ -import {store} from "../../static/store/store.js?static=ce3066287b" -import * as lsm from "../../static/lsm.js?static=aed2c7ca35" -import {resolveMxc} from "../../static/functions.js?static=e3784c70ce" - -let lastBatch = null - -function sync() { - const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/sync`) - url.searchParams.append("access_token", lsm.get("access_token")) - const filter = { - room: { - // pulling more from the timeline massively increases download size - timeline: { - limit: 5 - }, - // members are not currently needed - state: { - lazy_load_members: true - } - }, - presence: { - // presence is not implemented, ignore it - types: [] - } - } - url.searchParams.append("filter", JSON.stringify(filter)) - url.searchParams.append("timeout", 20000) - if (lastBatch) { - url.searchParams.append("since", lastBatch) - } - return fetch(url.toString()).then(res => res.json()).then(root => { - lastBatch = root.next_batch - return root - }) -} - -function manageSync(root) { - try { - let newEvents = false - - // set up directs - 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, room]) => { - if (!store.rooms.has(id)) { - store.rooms.askAdd(id, room) - } - const storeRoom = store.rooms.get(id).value() - room.state.events.forEach(event => { - if (event.type === "m.room.member") { - storeRoom.members.get(event.state_key).set(event) - } - }) - const timeline = storeRoom.timeline - if (room.timeline.events.length) newEvents = true - timeline.updateEvents(room.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) - } - }) - }) - } - }) - ).then(() => { - store.rooms.sort() - }) - if (newEvents) store.newEvents.broadcast("changeSelf") - } catch (e) { - console.error(root) - throw e - } -} - -function syncLoop() { - return sync().then(manageSync).then(syncLoop) -} - -;[ - { - id: "directs", - name: "Directs", - icon: staticFiles.get("/assets/icons/directs.svg"), - order: -2 - }, - { - id: "channels", - name: "Channels", - icon: staticFiles.get("/assets/icons/channels.svg"), - order: -1 - } -].forEach(data => store.groups.askAdd(data.id, data)) - -store.activeGroup.set(store.groups.get("directs").value()) - -syncLoop() diff --git a/build/static/whitney-400.woff b/build/static/whitney-400.woff deleted file mode 100644 index 2b33081..0000000 Binary files a/build/static/whitney-400.woff and /dev/null differ diff --git a/build/static/whitney-500.woff b/build/static/whitney-500.woff deleted file mode 100644 index fc82138..0000000 Binary files a/build/static/whitney-500.woff and /dev/null differ