Reactive state for groups and rooms
This commit is contained in:
parent
f42ea1493b
commit
dd0b14720e
17 changed files with 671 additions and 162 deletions
|
@ -2,10 +2,10 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="static/main.css?static=d352e5de1f">
|
||||
<link rel="stylesheet" type="text/css" href="static/main.css?static=79b6afceb8">
|
||||
<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=c7bdd4a2f3"></script>
|
||||
<script type="module" src="static/room-picker.js?static=596e719ff8"></script>
|
||||
<title>Carbon</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -25,19 +25,22 @@
|
|||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="5.6568542"
|
||||
inkscape:cx="30.795644"
|
||||
inkscape:cy="43.802047"
|
||||
inkscape:zoom="22.627417"
|
||||
inkscape:cx="27.665561"
|
||||
inkscape:cy="33.324951"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="879"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
showborder="false">
|
||||
showborder="true"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-grids="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid824" />
|
||||
|
@ -61,7 +64,7 @@
|
|||
transform="translate(36.739286,-225.97828)">
|
||||
<path
|
||||
style="opacity:1;fill:#e2e2e2;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.52916664;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||
d="m -33.035119,229.41786 h 5.067509 c 0.868277,0 1.567287,0.69901 1.567287,1.56728 v 1.88109 c 0,0.86828 -0.69901,1.56729 -1.567287,1.56729 h -2.157093 l -1.322916,1.86351 -1.322917,-1.86351 h -0.264583 c -0.868277,0 -1.567287,-0.69901 -1.567287,-1.56729 v -1.88109 c 0,-0.86827 0.69901,-1.56728 1.567287,-1.56728 z"
|
||||
d="m -33.035119,228.8887 h 5.067509 c 0.868277,0 1.567287,0.69901 1.567287,1.56728 v 2.66473 c 0,0.86828 -0.69901,1.56729 -1.567287,1.56729 h -2.157093 l -1.322916,1.86351 -1.322917,-1.86351 h -0.264583 c -0.868277,0 -1.567287,-0.69901 -1.567287,-1.56729 v -2.66473 c 0,-0.86827 0.69901,-1.56728 1.567287,-1.56728 z"
|
||||
id="rect820"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ssssscccssss" />
|
||||
|
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
|
@ -31,7 +31,7 @@ body {
|
|||
width: 240px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #202224 #2f3135;
|
||||
flex-shrink: 0;
|
||||
|
@ -40,16 +40,20 @@ body {
|
|||
.c-room {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
padding: 6px 8px;
|
||||
margin: 2px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.c-room:hover {
|
||||
background-color: #393c42;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.c-room:not(.c-room--active):hover {
|
||||
background-color: #393c42;
|
||||
}
|
||||
.c-room--active {
|
||||
background-color: #42454a;
|
||||
}
|
||||
.c-room__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #bbb;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
|
@ -121,8 +125,9 @@ body {
|
|||
.c-group-marker {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
transition: transform ease 0.12s;
|
||||
transition: transform ease 0.12s, opacity ease-out 0.12s;
|
||||
height: 46px;
|
||||
width: 6px;
|
||||
background-color: #ccc;
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
import {q, ElemJS, ejs} from "./basic.js"
|
||||
import {store} from "./store/store.js"
|
||||
|
||||
class ActiveGroupMarker extends ElemJS {
|
||||
constructor() {
|
||||
super(q("#c-group-marker"))
|
||||
store.activeGroup.subscribe("changeSelf", this.render.bind(this))
|
||||
}
|
||||
|
||||
follow(group) {
|
||||
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(groups, data) {
|
||||
constructor(key, data) {
|
||||
super("div")
|
||||
|
||||
this.groups = groups
|
||||
this.data = data
|
||||
this.active = false
|
||||
|
||||
this.on("click", this.onClick.bind(this))
|
||||
|
||||
this.class("c-group")
|
||||
this.child(
|
||||
|
@ -29,29 +34,45 @@ class Group extends ElemJS {
|
|||
),
|
||||
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))
|
||||
}
|
||||
|
||||
setActive(active) {
|
||||
this.active = active
|
||||
render() {
|
||||
const active = store.activeGroup.value() === this
|
||||
this.element.classList[active ? "add" : "remove"]("c-group--active")
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.groups.setGroup(this)
|
||||
store.activeGroup.set(this)
|
||||
}
|
||||
}
|
||||
|
||||
class Room extends ElemJS {
|
||||
constructor(name) {
|
||||
constructor(key, data) {
|
||||
super("div")
|
||||
|
||||
this.name = name
|
||||
this.data = data
|
||||
|
||||
this.class("c-room")
|
||||
this.child(
|
||||
ejs("div").class("c-room__icon"),
|
||||
ejs("div").class("c-room__name").text(this.name)
|
||||
ejs("div").class("c-room__name").text(this.data.name)
|
||||
)
|
||||
|
||||
this.on("click", this.onClick.bind(this))
|
||||
store.activeRoom.subscribe("changeSelf", this.render.bind(this))
|
||||
}
|
||||
|
||||
onClick() {
|
||||
store.activeRoom.set(this)
|
||||
}
|
||||
|
||||
render() {
|
||||
const active = store.activeRoom.value() === this
|
||||
this.element.classList[active ? "add" : "remove"]("c-room--active")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,20 +80,38 @@ class Rooms extends ElemJS {
|
|||
constructor() {
|
||||
super(q("#c-rooms"))
|
||||
|
||||
this.roomData = []
|
||||
this.rooms = []
|
||||
|
||||
store.rooms.subscribe("askAdd", this.askAdd.bind(this))
|
||||
store.rooms.subscribe("changeItem", this.render.bind(this))
|
||||
store.activeGroup.subscribe("changeSelf", this.render.bind(this))
|
||||
|
||||
this.render()
|
||||
}
|
||||
|
||||
setRooms(rooms) {
|
||||
this.rooms = rooms
|
||||
this.render()
|
||||
askAdd(event, {key, data}) {
|
||||
const room = new Room(key, data)
|
||||
store.rooms.addEnd(key, room)
|
||||
}
|
||||
|
||||
render() {
|
||||
this.clearChildren()
|
||||
for (const room of this.rooms) {
|
||||
this.child(new Room(room.name))
|
||||
let first = null
|
||||
// set room list
|
||||
store.rooms.forEach((id, room) => {
|
||||
if (room.value().data.group === 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().data.group !== store.activeGroup.value()) {
|
||||
if (first) {
|
||||
store.activeRoom.set(first)
|
||||
} else {
|
||||
store.activeRoom.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,59 +120,73 @@ const rooms = new Rooms()
|
|||
class Groups extends ElemJS {
|
||||
constructor() {
|
||||
super(q("#c-groups-list"))
|
||||
this.groupData = [
|
||||
{name: "Directs", icon: "/static/directs.svg", rooms: [
|
||||
{name: "riley"},
|
||||
{name: "BadAtNames"},
|
||||
{name: "lynxano"},
|
||||
{name: "quarky"},
|
||||
{name: "lepton"},
|
||||
{name: "ash"},
|
||||
{name: "mewmew"},
|
||||
{name: "Toniob"},
|
||||
{name: "cockandball"}
|
||||
]},
|
||||
{name: "Channels", icon: "/static/channels.svg", rooms: [
|
||||
{name: "Carbon brainstorming"},
|
||||
{name: "Bibliogram"},
|
||||
{name: "Monsters Inc Debate Hall"},
|
||||
{name: "DRB clan"},
|
||||
{name: "mettaton simp zone"}
|
||||
]},
|
||||
{name: "Fediverse Drama Museum", rooms: [
|
||||
{name: "witches"},
|
||||
{name: "snouts"},
|
||||
{name: "monads"},
|
||||
{name: "radical"},
|
||||
{name: "blobcat"}
|
||||
]},
|
||||
{name: "Epicord", rooms: [
|
||||
{name: "main"},
|
||||
{name: "gaming"},
|
||||
{name: "inhalers"},
|
||||
{name: "minecraft"},
|
||||
{name: "osu"},
|
||||
{name: "covid"}
|
||||
]},
|
||||
{name: "Invidious", rooms: [
|
||||
]}
|
||||
]
|
||||
this.groups = []
|
||||
this.render()
|
||||
this.setGroup(this.children[0])
|
||||
|
||||
store.groups.subscribe("askAdd", this.askAdd.bind(this))
|
||||
store.groups.subscribe("addItem", this.addItem.bind(this))
|
||||
}
|
||||
|
||||
setGroup(group) {
|
||||
rooms.setRooms(group.data.rooms)
|
||||
this.groups.forEach(g => g.setActive(g === group))
|
||||
activeGroupMarker.follow(group)
|
||||
askAdd(event, {key, data}) {
|
||||
const group = new Group(key, data)
|
||||
store.groups.addEnd(key, group)
|
||||
}
|
||||
|
||||
render() {
|
||||
this.groups = this.groupData.map(data => new Group(this, data))
|
||||
for (const group of this.groups) {
|
||||
this.child(group)
|
||||
}
|
||||
addItem(event, key) {
|
||||
this.child(store.groups.get(key).value())
|
||||
}
|
||||
}
|
||||
const groups = new Groups()
|
||||
|
||||
;[
|
||||
{
|
||||
id: "directs",
|
||||
name: "Directs",
|
||||
icon: "/static/directs.svg"
|
||||
},
|
||||
{
|
||||
id: "channels",
|
||||
name: "Channels",
|
||||
icon: "/static/channels.svg"
|
||||
},
|
||||
{
|
||||
id: "123",
|
||||
name: "Fediverse Drama Museum"
|
||||
},
|
||||
{
|
||||
id: "456",
|
||||
name: "Epicord"
|
||||
},
|
||||
{
|
||||
id: "789",
|
||||
name: "Invidious"
|
||||
}
|
||||
].forEach(data => store.groups.askAdd(data.id, data))
|
||||
|
||||
;[
|
||||
{id: "001", name: "riley", group: store.groups.get("directs").value()},
|
||||
{id: "002", name: "BadAtNames", group: store.groups.get("directs").value()},
|
||||
{id: "003", name: "lynxano", group: store.groups.get("directs").value()},
|
||||
{id: "004", name: "quarky", group: store.groups.get("directs").value()},
|
||||
{id: "005", name: "lepton", group: store.groups.get("directs").value()},
|
||||
{id: "006", name: "ash", group: store.groups.get("directs").value()},
|
||||
{id: "007", name: "mewmew", group: store.groups.get("directs").value()},
|
||||
{id: "008", name: "Toniob", group: store.groups.get("directs").value()},
|
||||
{id: "009", name: "cockandball", group: store.groups.get("directs").value()},
|
||||
{id: "010", name: "Carbon brainstorming", group: store.groups.get("channels").value()},
|
||||
{id: "011", name: "Bibliogram", group: store.groups.get("channels").value()},
|
||||
{id: "012", name: "Monsters Inc Debate Hall", group: store.groups.get("channels").value()},
|
||||
{id: "013", name: "DRB clan", group: store.groups.get("channels").value()},
|
||||
{id: "014", name: "mettaton simp zone", group: store.groups.get("channels").value()},
|
||||
{id: "015", name: "witches", group: store.groups.get("123").value()},
|
||||
{id: "016", name: "snouts", group: store.groups.get("123").value()},
|
||||
{id: "017", name: "monads", group: store.groups.get("123").value()},
|
||||
{id: "018", name: "radical", group: store.groups.get("123").value()},
|
||||
{id: "019", name: "blobcat", group: store.groups.get("123").value()},
|
||||
{id: "020", name: "main", group: store.groups.get("456").value()},
|
||||
{id: "021", name: "gaming", group: store.groups.get("456").value()},
|
||||
{id: "022", name: "inhalers", group: store.groups.get("456").value()},
|
||||
{id: "023", name: "minecraft", group: store.groups.get("456").value()},
|
||||
{id: "024", name: "osu", group: store.groups.get("456").value()},
|
||||
{id: "025", name: "covid", group: store.groups.get("456").value()}
|
||||
].forEach(data => store.rooms.askAdd(data.id, data))
|
||||
|
||||
store.activeGroup.set(store.groups.get("directs").value())
|
||||
|
|
36
build/static/store/Subscribable.js
Normal file
36
build/static/store/Subscribable.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
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) {
|
||||
this.events[event].push(callback)
|
||||
}
|
||||
|
||||
broadcast(event, data) {
|
||||
this.eventDeps[event].concat(event).forEach(eventName => {
|
||||
this.events[eventName].forEach(f => f(event, data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export {Subscribable}
|
77
build/static/store/SubscribeMapList.js
Normal file
77
build/static/store/SubscribeMapList.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
import {Subscribable} from "./Subscribable.js"
|
||||
import {SubscribeValue} from "./SubscribeValue.js"
|
||||
|
||||
class SubscribeMapList extends Subscribable {
|
||||
constructor(inner) {
|
||||
super()
|
||||
this.inner = inner
|
||||
Object.assign(this.events, {
|
||||
addItem: [],
|
||||
removeItem: [],
|
||||
editItem: [],
|
||||
changeItem: [],
|
||||
askAdd: []
|
||||
})
|
||||
Object.assign(this.eventDeps, {
|
||||
addItem: ["changeItem"],
|
||||
removeItem: ["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)
|
||||
}
|
||||
|
||||
_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("changeItem", 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}
|
47
build/static/store/SubscribeValue.js
Normal file
47
build/static/store/SubscribeValue.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import {Subscribable} from "./Subscribable.js"
|
||||
|
||||
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}
|
13
build/static/store/store.js
Normal file
13
build/static/store/store.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import {SubscribeMapList} from "./SubscribeMapList.js"
|
||||
import {SubscribeValue} from "./SubscribeValue.js"
|
||||
|
||||
const store = {
|
||||
groups: new SubscribeMapList(SubscribeValue),
|
||||
rooms: new SubscribeMapList(SubscribeValue),
|
||||
activeGroup: new SubscribeValue(),
|
||||
activeRoom: new SubscribeValue()
|
||||
}
|
||||
|
||||
window.store = store
|
||||
|
||||
export {store}
|
|
@ -25,19 +25,22 @@
|
|||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="5.6568542"
|
||||
inkscape:cx="30.795644"
|
||||
inkscape:cy="43.802047"
|
||||
inkscape:zoom="22.627417"
|
||||
inkscape:cx="27.665561"
|
||||
inkscape:cy="33.324951"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="879"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
showborder="false">
|
||||
showborder="true"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-grids="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid824" />
|
||||
|
@ -61,7 +64,7 @@
|
|||
transform="translate(36.739286,-225.97828)">
|
||||
<path
|
||||
style="opacity:1;fill:#e2e2e2;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.52916664;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
|
||||
d="m -33.035119,229.41786 h 5.067509 c 0.868277,0 1.567287,0.69901 1.567287,1.56728 v 1.88109 c 0,0.86828 -0.69901,1.56729 -1.567287,1.56729 h -2.157093 l -1.322916,1.86351 -1.322917,-1.86351 h -0.264583 c -0.868277,0 -1.567287,-0.69901 -1.567287,-1.56729 v -1.88109 c 0,-0.86827 0.69901,-1.56728 1.567287,-1.56728 z"
|
||||
d="m -33.035119,228.8887 h 5.067509 c 0.868277,0 1.567287,0.69901 1.567287,1.56728 v 2.66473 c 0,0.86828 -0.69901,1.56729 -1.567287,1.56729 h -2.157093 l -1.322916,1.86351 -1.322917,-1.86351 h -0.264583 c -0.868277,0 -1.567287,-0.69901 -1.567287,-1.56729 v -2.66473 c 0,-0.86827 0.69901,-1.56728 1.567287,-1.56728 z"
|
||||
id="rect820"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ssssscccssss" />
|
||||
|
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
|
@ -1,25 +1,30 @@
|
|||
import {q, ElemJS, ejs} from "./basic.js"
|
||||
import {store} from "./store/store.js"
|
||||
|
||||
class ActiveGroupMarker extends ElemJS {
|
||||
constructor() {
|
||||
super(q("#c-group-marker"))
|
||||
store.activeGroup.subscribe("changeSelf", this.render.bind(this))
|
||||
}
|
||||
|
||||
follow(group) {
|
||||
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(groups, data) {
|
||||
constructor(key, data) {
|
||||
super("div")
|
||||
|
||||
this.groups = groups
|
||||
this.data = data
|
||||
this.active = false
|
||||
|
||||
this.on("click", this.onClick.bind(this))
|
||||
|
||||
this.class("c-group")
|
||||
this.child(
|
||||
|
@ -29,29 +34,45 @@ class Group extends ElemJS {
|
|||
),
|
||||
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))
|
||||
}
|
||||
|
||||
setActive(active) {
|
||||
this.active = active
|
||||
render() {
|
||||
const active = store.activeGroup.value() === this
|
||||
this.element.classList[active ? "add" : "remove"]("c-group--active")
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.groups.setGroup(this)
|
||||
store.activeGroup.set(this)
|
||||
}
|
||||
}
|
||||
|
||||
class Room extends ElemJS {
|
||||
constructor(name) {
|
||||
constructor(key, data) {
|
||||
super("div")
|
||||
|
||||
this.name = name
|
||||
this.data = data
|
||||
|
||||
this.class("c-room")
|
||||
this.child(
|
||||
ejs("div").class("c-room__icon"),
|
||||
ejs("div").class("c-room__name").text(this.name)
|
||||
ejs("div").class("c-room__name").text(this.data.name)
|
||||
)
|
||||
|
||||
this.on("click", this.onClick.bind(this))
|
||||
store.activeRoom.subscribe("changeSelf", this.render.bind(this))
|
||||
}
|
||||
|
||||
onClick() {
|
||||
store.activeRoom.set(this)
|
||||
}
|
||||
|
||||
render() {
|
||||
const active = store.activeRoom.value() === this
|
||||
this.element.classList[active ? "add" : "remove"]("c-room--active")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,20 +80,38 @@ class Rooms extends ElemJS {
|
|||
constructor() {
|
||||
super(q("#c-rooms"))
|
||||
|
||||
this.roomData = []
|
||||
this.rooms = []
|
||||
|
||||
store.rooms.subscribe("askAdd", this.askAdd.bind(this))
|
||||
store.rooms.subscribe("changeItem", this.render.bind(this))
|
||||
store.activeGroup.subscribe("changeSelf", this.render.bind(this))
|
||||
|
||||
this.render()
|
||||
}
|
||||
|
||||
setRooms(rooms) {
|
||||
this.rooms = rooms
|
||||
this.render()
|
||||
askAdd(event, {key, data}) {
|
||||
const room = new Room(key, data)
|
||||
store.rooms.addEnd(key, room)
|
||||
}
|
||||
|
||||
render() {
|
||||
this.clearChildren()
|
||||
for (const room of this.rooms) {
|
||||
this.child(new Room(room.name))
|
||||
let first = null
|
||||
// set room list
|
||||
store.rooms.forEach((id, room) => {
|
||||
if (room.value().data.group === 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().data.group !== store.activeGroup.value()) {
|
||||
if (first) {
|
||||
store.activeRoom.set(first)
|
||||
} else {
|
||||
store.activeRoom.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,59 +120,73 @@ const rooms = new Rooms()
|
|||
class Groups extends ElemJS {
|
||||
constructor() {
|
||||
super(q("#c-groups-list"))
|
||||
this.groupData = [
|
||||
{name: "Directs", icon: "/static/directs.svg", rooms: [
|
||||
{name: "riley"},
|
||||
{name: "BadAtNames"},
|
||||
{name: "lynxano"},
|
||||
{name: "quarky"},
|
||||
{name: "lepton"},
|
||||
{name: "ash"},
|
||||
{name: "mewmew"},
|
||||
{name: "Toniob"},
|
||||
{name: "cockandball"}
|
||||
]},
|
||||
{name: "Channels", icon: "/static/channels.svg", rooms: [
|
||||
{name: "Carbon brainstorming"},
|
||||
{name: "Bibliogram"},
|
||||
{name: "Monsters Inc Debate Hall"},
|
||||
{name: "DRB clan"},
|
||||
{name: "mettaton simp zone"}
|
||||
]},
|
||||
{name: "Fediverse Drama Museum", rooms: [
|
||||
{name: "witches"},
|
||||
{name: "snouts"},
|
||||
{name: "monads"},
|
||||
{name: "radical"},
|
||||
{name: "blobcat"}
|
||||
]},
|
||||
{name: "Epicord", rooms: [
|
||||
{name: "main"},
|
||||
{name: "gaming"},
|
||||
{name: "inhalers"},
|
||||
{name: "minecraft"},
|
||||
{name: "osu"},
|
||||
{name: "covid"}
|
||||
]},
|
||||
{name: "Invidious", rooms: [
|
||||
]}
|
||||
]
|
||||
this.groups = []
|
||||
this.render()
|
||||
this.setGroup(this.children[0])
|
||||
|
||||
store.groups.subscribe("askAdd", this.askAdd.bind(this))
|
||||
store.groups.subscribe("addItem", this.addItem.bind(this))
|
||||
}
|
||||
|
||||
setGroup(group) {
|
||||
rooms.setRooms(group.data.rooms)
|
||||
this.groups.forEach(g => g.setActive(g === group))
|
||||
activeGroupMarker.follow(group)
|
||||
askAdd(event, {key, data}) {
|
||||
const group = new Group(key, data)
|
||||
store.groups.addEnd(key, group)
|
||||
}
|
||||
|
||||
render() {
|
||||
this.groups = this.groupData.map(data => new Group(this, data))
|
||||
for (const group of this.groups) {
|
||||
this.child(group)
|
||||
}
|
||||
addItem(event, key) {
|
||||
this.child(store.groups.get(key).value())
|
||||
}
|
||||
}
|
||||
const groups = new Groups()
|
||||
|
||||
;[
|
||||
{
|
||||
id: "directs",
|
||||
name: "Directs",
|
||||
icon: "/static/directs.svg"
|
||||
},
|
||||
{
|
||||
id: "channels",
|
||||
name: "Channels",
|
||||
icon: "/static/channels.svg"
|
||||
},
|
||||
{
|
||||
id: "123",
|
||||
name: "Fediverse Drama Museum"
|
||||
},
|
||||
{
|
||||
id: "456",
|
||||
name: "Epicord"
|
||||
},
|
||||
{
|
||||
id: "789",
|
||||
name: "Invidious"
|
||||
}
|
||||
].forEach(data => store.groups.askAdd(data.id, data))
|
||||
|
||||
;[
|
||||
{id: "001", name: "riley", group: store.groups.get("directs").value()},
|
||||
{id: "002", name: "BadAtNames", group: store.groups.get("directs").value()},
|
||||
{id: "003", name: "lynxano", group: store.groups.get("directs").value()},
|
||||
{id: "004", name: "quarky", group: store.groups.get("directs").value()},
|
||||
{id: "005", name: "lepton", group: store.groups.get("directs").value()},
|
||||
{id: "006", name: "ash", group: store.groups.get("directs").value()},
|
||||
{id: "007", name: "mewmew", group: store.groups.get("directs").value()},
|
||||
{id: "008", name: "Toniob", group: store.groups.get("directs").value()},
|
||||
{id: "009", name: "cockandball", group: store.groups.get("directs").value()},
|
||||
{id: "010", name: "Carbon brainstorming", group: store.groups.get("channels").value()},
|
||||
{id: "011", name: "Bibliogram", group: store.groups.get("channels").value()},
|
||||
{id: "012", name: "Monsters Inc Debate Hall", group: store.groups.get("channels").value()},
|
||||
{id: "013", name: "DRB clan", group: store.groups.get("channels").value()},
|
||||
{id: "014", name: "mettaton simp zone", group: store.groups.get("channels").value()},
|
||||
{id: "015", name: "witches", group: store.groups.get("123").value()},
|
||||
{id: "016", name: "snouts", group: store.groups.get("123").value()},
|
||||
{id: "017", name: "monads", group: store.groups.get("123").value()},
|
||||
{id: "018", name: "radical", group: store.groups.get("123").value()},
|
||||
{id: "019", name: "blobcat", group: store.groups.get("123").value()},
|
||||
{id: "020", name: "main", group: store.groups.get("456").value()},
|
||||
{id: "021", name: "gaming", group: store.groups.get("456").value()},
|
||||
{id: "022", name: "inhalers", group: store.groups.get("456").value()},
|
||||
{id: "023", name: "minecraft", group: store.groups.get("456").value()},
|
||||
{id: "024", name: "osu", group: store.groups.get("456").value()},
|
||||
{id: "025", name: "covid", group: store.groups.get("456").value()}
|
||||
].forEach(data => store.rooms.askAdd(data.id, data))
|
||||
|
||||
store.activeGroup.set(store.groups.get("directs").value())
|
||||
|
|
36
src/js/store/Subscribable.js
Normal file
36
src/js/store/Subscribable.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
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) {
|
||||
this.events[event].push(callback)
|
||||
}
|
||||
|
||||
broadcast(event, data) {
|
||||
this.eventDeps[event].concat(event).forEach(eventName => {
|
||||
this.events[eventName].forEach(f => f(event, data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export {Subscribable}
|
41
src/js/store/SubscribeMap.js
Normal file
41
src/js/store/SubscribeMap.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import {Subscribable} from "./Subscribable.js"
|
||||
import {SubscribeValue} from "./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}
|
77
src/js/store/SubscribeMapList.js
Normal file
77
src/js/store/SubscribeMapList.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
import {Subscribable} from "./Subscribable.js"
|
||||
import {SubscribeValue} from "./SubscribeValue.js"
|
||||
|
||||
class SubscribeMapList extends Subscribable {
|
||||
constructor(inner) {
|
||||
super()
|
||||
this.inner = inner
|
||||
Object.assign(this.events, {
|
||||
addItem: [],
|
||||
removeItem: [],
|
||||
editItem: [],
|
||||
changeItem: [],
|
||||
askAdd: []
|
||||
})
|
||||
Object.assign(this.eventDeps, {
|
||||
addItem: ["changeItem"],
|
||||
removeItem: ["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)
|
||||
}
|
||||
|
||||
_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("changeItem", 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}
|
47
src/js/store/SubscribeValue.js
Normal file
47
src/js/store/SubscribeValue.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import {Subscribable} from "./Subscribable.js"
|
||||
|
||||
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}
|
13
src/js/store/store.js
Normal file
13
src/js/store/store.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import {SubscribeMapList} from "./SubscribeMapList.js"
|
||||
import {SubscribeValue} from "./SubscribeValue.js"
|
||||
|
||||
const store = {
|
||||
groups: new SubscribeMapList(SubscribeValue),
|
||||
rooms: new SubscribeMapList(SubscribeValue),
|
||||
activeGroup: new SubscribeValue(),
|
||||
activeRoom: new SubscribeValue()
|
||||
}
|
||||
|
||||
window.store = store
|
||||
|
||||
export {store}
|
|
@ -64,8 +64,9 @@ $out-width: $base-width + rooms.$list-width
|
|||
.c-group-marker
|
||||
position: absolute
|
||||
top: 5px
|
||||
opacity: 0
|
||||
transform: translateY(8px)
|
||||
transition: transform ease 0.12s
|
||||
transition: transform ease 0.12s, opacity ease-out 0.12s
|
||||
height: $icon-size - 2px
|
||||
width: 6px
|
||||
background-color: #ccc
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@use "../colors" as c
|
||||
|
||||
$list-width: 240px
|
||||
$icon-size: 36px
|
||||
$icon-size: 32px
|
||||
$icon-padding: 8px
|
||||
|
||||
.c-rooms
|
||||
|
@ -10,7 +10,7 @@ $icon-padding: 8px
|
|||
width: $list-width
|
||||
font-size: 20px
|
||||
font-weight: 500
|
||||
overflow-y: scroll
|
||||
overflow-y: auto
|
||||
scrollbar-width: thin
|
||||
scrollbar-color: c.$darkest c.$darker
|
||||
flex-shrink: 0
|
||||
|
@ -18,13 +18,17 @@ $icon-padding: 8px
|
|||
.c-room
|
||||
display: flex
|
||||
align-items: center
|
||||
padding: $icon-padding
|
||||
padding: $icon-padding * 0.75 $icon-padding
|
||||
margin: $icon-padding * 0.25 0
|
||||
cursor: pointer
|
||||
|
||||
&:hover
|
||||
background-color: c.$mild
|
||||
border-radius: 8px
|
||||
|
||||
&:not(&--active):hover
|
||||
background-color: c.$mild
|
||||
|
||||
&--active
|
||||
background-color: c.$milder
|
||||
|
||||
&__icon
|
||||
width: $icon-size
|
||||
height: $icon-size
|
||||
|
|
Loading…
Reference in a new issue