forked from cadence/Carbon
		
	Now technically a chat app
This commit is contained in:
		
							parent
							
								
									ac6320c12c
								
							
						
					
					
						commit
						33e4a7d7cb
					
				
					 19 changed files with 434 additions and 13 deletions
				
			
		| 
						 | 
				
			
			@ -2,11 +2,12 @@
 | 
			
		|||
<html>
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8">
 | 
			
		||||
    <link rel="stylesheet" type="text/css" href="static/main.css?static=b0aba41b0b">
 | 
			
		||||
    <link rel="stylesheet" type="text/css" href="static/main.css?static=0c50689ce5">
 | 
			
		||||
    <script type="module" src="static/groups.js?static=2cc7f0daf8"></script>
 | 
			
		||||
    <script type="module" src="static/chat-input.js?static=a90499fdac"></script>
 | 
			
		||||
    <script type="module" src="static/room-picker.js?static=d92adfaaca"></script>
 | 
			
		||||
    <script type="module" src="static/sync/sync.js?static=232a64285c"></script>
 | 
			
		||||
    <script type="module" src="static/chat-input.js?static=e8b21037fa"></script>
 | 
			
		||||
    <script type="module" src="static/room-picker.js?static=1d38378110"></script>
 | 
			
		||||
    <script type="module" src="static/sync/sync.js?static=9e31c8a727"></script>
 | 
			
		||||
    <script type="module" src="static/chat.js?static=d4a415f1ae"></script>
 | 
			
		||||
    <title>Carbon</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +21,7 @@
 | 
			
		|||
      <div class="c-rooms" id="c-rooms"></div>
 | 
			
		||||
      <div class="c-chat">
 | 
			
		||||
        <div class="c-chat__messages">
 | 
			
		||||
          <div class="c-chat__inner">
 | 
			
		||||
          <div class="c-chat__inner" id="c-chat">
 | 
			
		||||
            <div class="c-message-notice">
 | 
			
		||||
              <div class="c-message-notice__inner">You've reached the start of the conversation.</div>
 | 
			
		||||
            </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										95
									
								
								build/static/Timeline.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								build/static/Timeline.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,95 @@
 | 
			
		|||
import {ElemJS} from "./basic.js"
 | 
			
		||||
import {Subscribable} from "./store/Subscribable.js"
 | 
			
		||||
 | 
			
		||||
class Event extends ElemJS {
 | 
			
		||||
	constructor(data) {
 | 
			
		||||
		super("div")
 | 
			
		||||
		this.class("c-message")
 | 
			
		||||
		this.data = null
 | 
			
		||||
		this.update(data)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	update(data) {
 | 
			
		||||
		this.data = data
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.child(this.data.content.body)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Timeline extends Subscribable {
 | 
			
		||||
	constructor() {
 | 
			
		||||
		super()
 | 
			
		||||
		Object.assign(this.events, {
 | 
			
		||||
			addItem: [],
 | 
			
		||||
			removeItem: []
 | 
			
		||||
		})
 | 
			
		||||
		Object.assign(this.eventDeps, {
 | 
			
		||||
			addItem: [],
 | 
			
		||||
			removeItem: []
 | 
			
		||||
		})
 | 
			
		||||
		this.list = []
 | 
			
		||||
		this.map = new Map()
 | 
			
		||||
		this.elementsMap = new Map()
 | 
			
		||||
		this.elementsList = []
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_binarySearch(event, min = 0, max = -1) {
 | 
			
		||||
		if (this.list.length === 0) return {success: false, i: 0}
 | 
			
		||||
 | 
			
		||||
		if (max === -1) max = this.list.length - 1
 | 
			
		||||
		let mid = Math.floor((max + min) / 2)
 | 
			
		||||
		// success condition
 | 
			
		||||
		if (this.list[mid] && this.list[mid].event_id === event.event_id) return {success: true, i: mid}
 | 
			
		||||
		// failed condition
 | 
			
		||||
		if (min >= max) {
 | 
			
		||||
			while (mid !== -1 && (!this.list[mid] || this.list[mid].origin_server_ts > event.origin_server_ts)) mid--
 | 
			
		||||
			return {
 | 
			
		||||
				success: false,
 | 
			
		||||
				i: mid + 1
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// recurse (below)
 | 
			
		||||
		if (this.list[mid].origin_server_ts > event.origin_server_ts) return this._binarySearch(event, min, mid-1)
 | 
			
		||||
		// recurse (above)
 | 
			
		||||
		else return this._binarySearch(event, mid+1, max)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updateEvents(events) {
 | 
			
		||||
		for (const event of events) {
 | 
			
		||||
			if (this.map.has(event.event_id)) {
 | 
			
		||||
				this.map.set(event.event_id, event)
 | 
			
		||||
				this.elementsMap.get(event.event_id).update(this.map.get(event.event_id))
 | 
			
		||||
			} else {
 | 
			
		||||
				const index = this._binarySearch(event).i
 | 
			
		||||
				this.list.splice(index, 0, event)
 | 
			
		||||
				this.map.set(event.event_id, event)
 | 
			
		||||
				const e = new Event(event)
 | 
			
		||||
				this.elementsList.splice(index, 0, e)
 | 
			
		||||
				this.elementsMap.set(event.event_id, e)
 | 
			
		||||
				this.broadcast("addItem", {index, element: e})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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}
 | 
			
		||||
| 
						 | 
				
			
			@ -119,6 +119,18 @@ class ElemJS {
 | 
			
		|||
		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.
 | 
			
		||||
	 */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,46 @@
 | 
			
		|||
import {q} from "./basic.js"
 | 
			
		||||
import {store} from "./store/store.js"
 | 
			
		||||
import * as lsm from "./lsm.js"
 | 
			
		||||
 | 
			
		||||
let sentIndex = 0
 | 
			
		||||
 | 
			
		||||
const chat = q("#c-chat-textarea")
 | 
			
		||||
 | 
			
		||||
chat.addEventListener("keydown", event => {
 | 
			
		||||
	if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey) {
 | 
			
		||||
		chat.value = ""
 | 
			
		||||
		event.preventDefault()
 | 
			
		||||
		const body = chat.value
 | 
			
		||||
		send(chat.value)
 | 
			
		||||
		chat.value = ""
 | 
			
		||||
		fixHeight()
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
chat.addEventListener("input", () => {
 | 
			
		||||
	fixHeight()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function fixHeight() {
 | 
			
		||||
	chat.style.height = "0px"
 | 
			
		||||
	console.log(chat.clientHeight, chat.scrollHeight)
 | 
			
		||||
	chat.style.height = (chat.scrollHeight + 1) + "px"
 | 
			
		||||
})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getTxnId() {
 | 
			
		||||
	return Date.now() + (sentIndex++)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function send(body) {
 | 
			
		||||
	if (!store.activeRoom.exists()) return
 | 
			
		||||
	const id = store.activeRoom.value().id
 | 
			
		||||
	return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${id}/send/m.room.message/${getTxnId()}?access_token=${lsm.get("access_token")}`, {
 | 
			
		||||
		method: "PUT",
 | 
			
		||||
		body: JSON.stringify({
 | 
			
		||||
			msgtype: "m.text",
 | 
			
		||||
			body
 | 
			
		||||
		}),
 | 
			
		||||
		headers: {
 | 
			
		||||
			"Content-Type": "application/json"
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										62
									
								
								build/static/chat.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								build/static/chat.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
import {ElemJS, q, ejs} from "./basic.js"
 | 
			
		||||
import {store} from "./store/store.js"
 | 
			
		||||
 | 
			
		||||
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 = (_, {element, index}) => {
 | 
			
		||||
				this.childAt(index, element)
 | 
			
		||||
			}
 | 
			
		||||
			const name = "addItem"
 | 
			
		||||
			this.removableSubscriptions.push({name, target: timeline, subscription})
 | 
			
		||||
			timeline.subscribe(name, subscription)
 | 
			
		||||
		}
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
		if (store.activeRoom.exists()) {
 | 
			
		||||
			const timeline = store.activeRoom.value().timeline
 | 
			
		||||
			for (const group of timeline.getGroupedEvents()) {
 | 
			
		||||
				const first = group[0]
 | 
			
		||||
				this.child(
 | 
			
		||||
					ejs("div").class("c-message-group").child(
 | 
			
		||||
						ejs("div").class("c-message-group__avatar").child(
 | 
			
		||||
							ejs("div").class("c-message-group__icon")
 | 
			
		||||
						),
 | 
			
		||||
						ejs("div").class("c-message-group__messages").child(
 | 
			
		||||
							ejs("div").class("c-message-group__intro").child(
 | 
			
		||||
								ejs("div").class("c-message-group__name").text(first.sender)
 | 
			
		||||
							),
 | 
			
		||||
							...group.map(event => timeline.elementsMap.get(event.event_id))
 | 
			
		||||
						)
 | 
			
		||||
					)
 | 
			
		||||
				)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
new Chat()
 | 
			
		||||
| 
						 | 
				
			
			@ -208,6 +208,7 @@ body {
 | 
			
		|||
	display: grid;
 | 
			
		||||
	grid-template-rows: 1fr auto;
 | 
			
		||||
	align-items: end;
 | 
			
		||||
	flex: 1;
 | 
			
		||||
}
 | 
			
		||||
.c-chat__messages {
 | 
			
		||||
	height: 100%;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
import {q, ElemJS, ejs} from "./basic.js"
 | 
			
		||||
import {store} from "./store/store.js"
 | 
			
		||||
import {Timeline} from "./Timeline.js"
 | 
			
		||||
import * as lsm from "./lsm.js"
 | 
			
		||||
 | 
			
		||||
function resolveMxc(url, size, method) {
 | 
			
		||||
| 
						 | 
				
			
			@ -66,6 +67,7 @@ class Room extends ElemJS {
 | 
			
		|||
 | 
			
		||||
		this.id = id
 | 
			
		||||
		this.data = data
 | 
			
		||||
		this.timeline = new Timeline()
 | 
			
		||||
 | 
			
		||||
		this.class("c-room")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,9 @@ class Subscribable {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	unsubscribe(event, callback) {
 | 
			
		||||
		this.events[event].push(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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,6 +48,8 @@ function manageSync(root) {
 | 
			
		|||
			if (!store.rooms.has(id)) {
 | 
			
		||||
				store.rooms.askAdd(id, room)
 | 
			
		||||
			}
 | 
			
		||||
			const timeline = store.rooms.get(id).value().timeline
 | 
			
		||||
			timeline.updateEvents(room.timeline.events)
 | 
			
		||||
		})
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		console.error(root)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										10
									
								
								spec.js
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								spec.js
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -64,6 +64,16 @@ module.exports = [
 | 
			
		|||
		source: "/js/lsm.js",
 | 
			
		||||
		target: "/static/lsm.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/Timeline.js",
 | 
			
		||||
		target: "/static/Timeline.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "js",
 | 
			
		||||
		source: "/js/chat.js",
 | 
			
		||||
		target: "/static/chat.js"
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: "file",
 | 
			
		||||
		source: "/assets/fonts/whitney-500.woff",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,6 +38,7 @@ html
 | 
			
		|||
    script(type="module" src=getStatic("/js/chat-input.js"))
 | 
			
		||||
    script(type="module" src=getStatic("/js/room-picker.js"))
 | 
			
		||||
    script(type="module" src=getStatic("/js/sync/sync.js"))
 | 
			
		||||
    script(type="module" src=getStatic("/js/chat.js"))
 | 
			
		||||
    title Carbon
 | 
			
		||||
  body
 | 
			
		||||
    main.main
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +49,7 @@ html
 | 
			
		|||
      .c-rooms#c-rooms
 | 
			
		||||
      .c-chat
 | 
			
		||||
        .c-chat__messages
 | 
			
		||||
          .c-chat__inner
 | 
			
		||||
          .c-chat__inner#c-chat
 | 
			
		||||
            +message-notice("You've reached the start of the conversation.")
 | 
			
		||||
            +message("Cadence", [
 | 
			
		||||
              `the second button is for rooms (gonna call them "channels" to make discord users happy) that are not in a group (which will be most rooms - few people set up groups because they're so annoying, and many communities of people only need a single chatroom)`,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										95
									
								
								src/js/Timeline.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/js/Timeline.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,95 @@
 | 
			
		|||
import {ElemJS} from "./basic.js"
 | 
			
		||||
import {Subscribable} from "./store/Subscribable.js"
 | 
			
		||||
 | 
			
		||||
class Event extends ElemJS {
 | 
			
		||||
	constructor(data) {
 | 
			
		||||
		super("div")
 | 
			
		||||
		this.class("c-message")
 | 
			
		||||
		this.data = null
 | 
			
		||||
		this.update(data)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	update(data) {
 | 
			
		||||
		this.data = data
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.child(this.data.content.body)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Timeline extends Subscribable {
 | 
			
		||||
	constructor() {
 | 
			
		||||
		super()
 | 
			
		||||
		Object.assign(this.events, {
 | 
			
		||||
			addItem: [],
 | 
			
		||||
			removeItem: []
 | 
			
		||||
		})
 | 
			
		||||
		Object.assign(this.eventDeps, {
 | 
			
		||||
			addItem: [],
 | 
			
		||||
			removeItem: []
 | 
			
		||||
		})
 | 
			
		||||
		this.list = []
 | 
			
		||||
		this.map = new Map()
 | 
			
		||||
		this.elementsMap = new Map()
 | 
			
		||||
		this.elementsList = []
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_binarySearch(event, min = 0, max = -1) {
 | 
			
		||||
		if (this.list.length === 0) return {success: false, i: 0}
 | 
			
		||||
 | 
			
		||||
		if (max === -1) max = this.list.length - 1
 | 
			
		||||
		let mid = Math.floor((max + min) / 2)
 | 
			
		||||
		// success condition
 | 
			
		||||
		if (this.list[mid] && this.list[mid].event_id === event.event_id) return {success: true, i: mid}
 | 
			
		||||
		// failed condition
 | 
			
		||||
		if (min >= max) {
 | 
			
		||||
			while (mid !== -1 && (!this.list[mid] || this.list[mid].origin_server_ts > event.origin_server_ts)) mid--
 | 
			
		||||
			return {
 | 
			
		||||
				success: false,
 | 
			
		||||
				i: mid + 1
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// recurse (below)
 | 
			
		||||
		if (this.list[mid].origin_server_ts > event.origin_server_ts) return this._binarySearch(event, min, mid-1)
 | 
			
		||||
		// recurse (above)
 | 
			
		||||
		else return this._binarySearch(event, mid+1, max)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updateEvents(events) {
 | 
			
		||||
		for (const event of events) {
 | 
			
		||||
			if (this.map.has(event.event_id)) {
 | 
			
		||||
				this.map.set(event.event_id, event)
 | 
			
		||||
				this.elementsMap.get(event.event_id).update(this.map.get(event.event_id))
 | 
			
		||||
			} else {
 | 
			
		||||
				const index = this._binarySearch(event).i
 | 
			
		||||
				this.list.splice(index, 0, event)
 | 
			
		||||
				this.map.set(event.event_id, event)
 | 
			
		||||
				const e = new Event(event)
 | 
			
		||||
				this.elementsList.splice(index, 0, e)
 | 
			
		||||
				this.elementsMap.set(event.event_id, e)
 | 
			
		||||
				this.broadcast("addItem", {index, element: e})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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}
 | 
			
		||||
| 
						 | 
				
			
			@ -119,6 +119,18 @@ class ElemJS {
 | 
			
		|||
		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.
 | 
			
		||||
	 */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,46 @@
 | 
			
		|||
import {q} from "./basic.js"
 | 
			
		||||
import {store} from "./store/store.js"
 | 
			
		||||
import * as lsm from "./lsm.js"
 | 
			
		||||
 | 
			
		||||
let sentIndex = 0
 | 
			
		||||
 | 
			
		||||
const chat = q("#c-chat-textarea")
 | 
			
		||||
 | 
			
		||||
chat.addEventListener("keydown", event => {
 | 
			
		||||
	if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey) {
 | 
			
		||||
		chat.value = ""
 | 
			
		||||
		event.preventDefault()
 | 
			
		||||
		const body = chat.value
 | 
			
		||||
		send(chat.value)
 | 
			
		||||
		chat.value = ""
 | 
			
		||||
		fixHeight()
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
chat.addEventListener("input", () => {
 | 
			
		||||
	fixHeight()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function fixHeight() {
 | 
			
		||||
	chat.style.height = "0px"
 | 
			
		||||
	console.log(chat.clientHeight, chat.scrollHeight)
 | 
			
		||||
	chat.style.height = (chat.scrollHeight + 1) + "px"
 | 
			
		||||
})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getTxnId() {
 | 
			
		||||
	return Date.now() + (sentIndex++)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function send(body) {
 | 
			
		||||
	if (!store.activeRoom.exists()) return
 | 
			
		||||
	const id = store.activeRoom.value().id
 | 
			
		||||
	return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${id}/send/m.room.message/${getTxnId()}?access_token=${lsm.get("access_token")}`, {
 | 
			
		||||
		method: "PUT",
 | 
			
		||||
		body: JSON.stringify({
 | 
			
		||||
			msgtype: "m.text",
 | 
			
		||||
			body
 | 
			
		||||
		}),
 | 
			
		||||
		headers: {
 | 
			
		||||
			"Content-Type": "application/json"
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										62
									
								
								src/js/chat.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/js/chat.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
import {ElemJS, q, ejs} from "./basic.js"
 | 
			
		||||
import {store} from "./store/store.js"
 | 
			
		||||
 | 
			
		||||
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 = (_, {element, index}) => {
 | 
			
		||||
				this.childAt(index, element)
 | 
			
		||||
			}
 | 
			
		||||
			const name = "addItem"
 | 
			
		||||
			this.removableSubscriptions.push({name, target: timeline, subscription})
 | 
			
		||||
			timeline.subscribe(name, subscription)
 | 
			
		||||
		}
 | 
			
		||||
		this.render()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.clearChildren()
 | 
			
		||||
		if (store.activeRoom.exists()) {
 | 
			
		||||
			const timeline = store.activeRoom.value().timeline
 | 
			
		||||
			for (const group of timeline.getGroupedEvents()) {
 | 
			
		||||
				const first = group[0]
 | 
			
		||||
				this.child(
 | 
			
		||||
					ejs("div").class("c-message-group").child(
 | 
			
		||||
						ejs("div").class("c-message-group__avatar").child(
 | 
			
		||||
							ejs("div").class("c-message-group__icon")
 | 
			
		||||
						),
 | 
			
		||||
						ejs("div").class("c-message-group__messages").child(
 | 
			
		||||
							ejs("div").class("c-message-group__intro").child(
 | 
			
		||||
								ejs("div").class("c-message-group__name").text(first.sender)
 | 
			
		||||
							),
 | 
			
		||||
							...group.map(event => timeline.elementsMap.get(event.event_id))
 | 
			
		||||
						)
 | 
			
		||||
					)
 | 
			
		||||
				)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
new Chat()
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
import {q, ElemJS, ejs} from "./basic.js"
 | 
			
		||||
import {store} from "./store/store.js"
 | 
			
		||||
import {Timeline} from "./Timeline.js"
 | 
			
		||||
import * as lsm from "./lsm.js"
 | 
			
		||||
 | 
			
		||||
function resolveMxc(url, size, method) {
 | 
			
		||||
| 
						 | 
				
			
			@ -66,6 +67,7 @@ class Room extends ElemJS {
 | 
			
		|||
 | 
			
		||||
		this.id = id
 | 
			
		||||
		this.data = data
 | 
			
		||||
		this.timeline = new Timeline()
 | 
			
		||||
 | 
			
		||||
		this.class("c-room")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,9 @@ class Subscribable {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	unsubscribe(event, callback) {
 | 
			
		||||
		this.events[event].push(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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,6 +48,8 @@ function manageSync(root) {
 | 
			
		|||
			if (!store.rooms.has(id)) {
 | 
			
		||||
				store.rooms.askAdd(id, room)
 | 
			
		||||
			}
 | 
			
		||||
			const timeline = store.rooms.get(id).value().timeline
 | 
			
		||||
			timeline.updateEvents(room.timeline.events)
 | 
			
		||||
		})
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		console.error(root)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
  display: grid
 | 
			
		||||
  grid-template-rows: 1fr auto
 | 
			
		||||
  align-items: end
 | 
			
		||||
  // height: 100%
 | 
			
		||||
  flex: 1
 | 
			
		||||
 | 
			
		||||
  &__messages
 | 
			
		||||
    height: 100%
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue