Reactive state for groups and rooms

This commit is contained in:
Cadence Ember 2020-10-15 16:43:37 +13:00
parent f42ea1493b
commit dd0b14720e
Signed by: cadence
GPG key ID: BC1C2C61CF521B17
17 changed files with 671 additions and 162 deletions

View file

@ -2,10 +2,10 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <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/groups.js?static=2cc7f0daf8"></script>
<script type="module" src="static/chat-input.js?static=a90499fdac"></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> <title>Carbon</title>
</head> </head>
<body> <body>

View file

@ -25,19 +25,22 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="5.6568542" inkscape:zoom="22.627417"
inkscape:cx="30.795644" inkscape:cx="27.665561"
inkscape:cy="43.802047" inkscape:cy="33.324951"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1" inkscape:current-layer="layer1"
showgrid="false" showgrid="true"
units="px" units="px"
inkscape:window-width="1440" inkscape:window-width="1440"
inkscape:window-height="879" inkscape:window-height="879"
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="0" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
showborder="false"> showborder="true"
inkscape:snap-bbox="true"
inkscape:bbox-nodes="true"
inkscape:snap-grids="true">
<inkscape:grid <inkscape:grid
type="xygrid" type="xygrid"
id="grid824" /> id="grid824" />
@ -61,7 +64,7 @@
transform="translate(36.739286,-225.97828)"> transform="translate(36.739286,-225.97828)">
<path <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" 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" id="rect820"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
sodipodi:nodetypes="ssssscccssss" /> sodipodi:nodetypes="ssssscccssss" />

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -31,7 +31,7 @@ body {
width: 240px; width: 240px;
font-size: 20px; font-size: 20px;
font-weight: 500; font-weight: 500;
overflow-y: scroll; overflow-y: auto;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #202224 #2f3135; scrollbar-color: #202224 #2f3135;
flex-shrink: 0; flex-shrink: 0;
@ -40,16 +40,20 @@ body {
.c-room { .c-room {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 8px; padding: 6px 8px;
margin: 2px 0;
cursor: pointer; cursor: pointer;
}
.c-room:hover {
background-color: #393c42;
border-radius: 8px; border-radius: 8px;
} }
.c-room:not(.c-room--active):hover {
background-color: #393c42;
}
.c-room--active {
background-color: #42454a;
}
.c-room__icon { .c-room__icon {
width: 36px; width: 32px;
height: 36px; height: 32px;
background-color: #bbb; background-color: #bbb;
margin-right: 8px; margin-right: 8px;
border-radius: 50%; border-radius: 50%;
@ -121,8 +125,9 @@ body {
.c-group-marker { .c-group-marker {
position: absolute; position: absolute;
top: 5px; top: 5px;
opacity: 0;
transform: translateY(8px); transform: translateY(8px);
transition: transform ease 0.12s; transition: transform ease 0.12s, opacity ease-out 0.12s;
height: 46px; height: 46px;
width: 6px; width: 6px;
background-color: #ccc; background-color: #ccc;

View file

@ -1,25 +1,30 @@
import {q, ElemJS, ejs} from "./basic.js" import {q, ElemJS, ejs} from "./basic.js"
import {store} from "./store/store.js"
class ActiveGroupMarker extends ElemJS { class ActiveGroupMarker extends ElemJS {
constructor() { constructor() {
super(q("#c-group-marker")) super(q("#c-group-marker"))
store.activeGroup.subscribe("changeSelf", this.render.bind(this))
} }
follow(group) { render() {
this.style("transform", `translateY(${group.element.offsetTop}px)`) 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() const activeGroupMarker = new ActiveGroupMarker()
class Group extends ElemJS { class Group extends ElemJS {
constructor(groups, data) { constructor(key, data) {
super("div") super("div")
this.groups = groups
this.data = data this.data = data
this.active = false
this.on("click", this.onClick.bind(this))
this.class("c-group") this.class("c-group")
this.child( this.child(
@ -29,29 +34,45 @@ class Group extends ElemJS {
), ),
ejs("div").class("c-group__name").text(this.data.name) 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) { render() {
this.active = active const active = store.activeGroup.value() === this
this.element.classList[active ? "add" : "remove"]("c-group--active") this.element.classList[active ? "add" : "remove"]("c-group--active")
} }
onClick() { onClick() {
this.groups.setGroup(this) store.activeGroup.set(this)
} }
} }
class Room extends ElemJS { class Room extends ElemJS {
constructor(name) { constructor(key, data) {
super("div") super("div")
this.name = name this.data = data
this.class("c-room") this.class("c-room")
this.child( this.child(
ejs("div").class("c-room__icon"), 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() { constructor() {
super(q("#c-rooms")) super(q("#c-rooms"))
this.roomData = []
this.rooms = [] 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() this.render()
} }
setRooms(rooms) { askAdd(event, {key, data}) {
this.rooms = rooms const room = new Room(key, data)
this.render() store.rooms.addEnd(key, room)
} }
render() { render() {
this.clearChildren() this.clearChildren()
for (const room of this.rooms) { let first = null
this.child(new Room(room.name)) // 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 { class Groups extends ElemJS {
constructor() { constructor() {
super(q("#c-groups-list")) super(q("#c-groups-list"))
this.groupData = [
{name: "Directs", icon: "/static/directs.svg", rooms: [ store.groups.subscribe("askAdd", this.askAdd.bind(this))
{name: "riley"}, store.groups.subscribe("addItem", this.addItem.bind(this))
{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])
} }
setGroup(group) { askAdd(event, {key, data}) {
rooms.setRooms(group.data.rooms) const group = new Group(key, data)
this.groups.forEach(g => g.setActive(g === group)) store.groups.addEnd(key, group)
activeGroupMarker.follow(group)
} }
render() { addItem(event, key) {
this.groups = this.groupData.map(data => new Group(this, data)) this.child(store.groups.get(key).value())
for (const group of this.groups) {
this.child(group)
}
} }
} }
const groups = new Groups() 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())

View 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}

View 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}

View 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}

View 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}

View file

@ -25,19 +25,22 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="5.6568542" inkscape:zoom="22.627417"
inkscape:cx="30.795644" inkscape:cx="27.665561"
inkscape:cy="43.802047" inkscape:cy="33.324951"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1" inkscape:current-layer="layer1"
showgrid="false" showgrid="true"
units="px" units="px"
inkscape:window-width="1440" inkscape:window-width="1440"
inkscape:window-height="879" inkscape:window-height="879"
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="0" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
showborder="false"> showborder="true"
inkscape:snap-bbox="true"
inkscape:bbox-nodes="true"
inkscape:snap-grids="true">
<inkscape:grid <inkscape:grid
type="xygrid" type="xygrid"
id="grid824" /> id="grid824" />
@ -61,7 +64,7 @@
transform="translate(36.739286,-225.97828)"> transform="translate(36.739286,-225.97828)">
<path <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" 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" id="rect820"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
sodipodi:nodetypes="ssssscccssss" /> sodipodi:nodetypes="ssssscccssss" />

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1,25 +1,30 @@
import {q, ElemJS, ejs} from "./basic.js" import {q, ElemJS, ejs} from "./basic.js"
import {store} from "./store/store.js"
class ActiveGroupMarker extends ElemJS { class ActiveGroupMarker extends ElemJS {
constructor() { constructor() {
super(q("#c-group-marker")) super(q("#c-group-marker"))
store.activeGroup.subscribe("changeSelf", this.render.bind(this))
} }
follow(group) { render() {
this.style("transform", `translateY(${group.element.offsetTop}px)`) 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() const activeGroupMarker = new ActiveGroupMarker()
class Group extends ElemJS { class Group extends ElemJS {
constructor(groups, data) { constructor(key, data) {
super("div") super("div")
this.groups = groups
this.data = data this.data = data
this.active = false
this.on("click", this.onClick.bind(this))
this.class("c-group") this.class("c-group")
this.child( this.child(
@ -29,29 +34,45 @@ class Group extends ElemJS {
), ),
ejs("div").class("c-group__name").text(this.data.name) 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) { render() {
this.active = active const active = store.activeGroup.value() === this
this.element.classList[active ? "add" : "remove"]("c-group--active") this.element.classList[active ? "add" : "remove"]("c-group--active")
} }
onClick() { onClick() {
this.groups.setGroup(this) store.activeGroup.set(this)
} }
} }
class Room extends ElemJS { class Room extends ElemJS {
constructor(name) { constructor(key, data) {
super("div") super("div")
this.name = name this.data = data
this.class("c-room") this.class("c-room")
this.child( this.child(
ejs("div").class("c-room__icon"), 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() { constructor() {
super(q("#c-rooms")) super(q("#c-rooms"))
this.roomData = []
this.rooms = [] 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() this.render()
} }
setRooms(rooms) { askAdd(event, {key, data}) {
this.rooms = rooms const room = new Room(key, data)
this.render() store.rooms.addEnd(key, room)
} }
render() { render() {
this.clearChildren() this.clearChildren()
for (const room of this.rooms) { let first = null
this.child(new Room(room.name)) // 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 { class Groups extends ElemJS {
constructor() { constructor() {
super(q("#c-groups-list")) super(q("#c-groups-list"))
this.groupData = [
{name: "Directs", icon: "/static/directs.svg", rooms: [ store.groups.subscribe("askAdd", this.askAdd.bind(this))
{name: "riley"}, store.groups.subscribe("addItem", this.addItem.bind(this))
{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])
} }
setGroup(group) { askAdd(event, {key, data}) {
rooms.setRooms(group.data.rooms) const group = new Group(key, data)
this.groups.forEach(g => g.setActive(g === group)) store.groups.addEnd(key, group)
activeGroupMarker.follow(group)
} }
render() { addItem(event, key) {
this.groups = this.groupData.map(data => new Group(this, data)) this.child(store.groups.get(key).value())
for (const group of this.groups) {
this.child(group)
}
} }
} }
const groups = new Groups() 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())

View 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}

View 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}

View 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}

View 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
View 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}

View file

@ -64,8 +64,9 @@ $out-width: $base-width + rooms.$list-width
.c-group-marker .c-group-marker
position: absolute position: absolute
top: 5px top: 5px
opacity: 0
transform: translateY(8px) transform: translateY(8px)
transition: transform ease 0.12s transition: transform ease 0.12s, opacity ease-out 0.12s
height: $icon-size - 2px height: $icon-size - 2px
width: 6px width: 6px
background-color: #ccc background-color: #ccc

View file

@ -1,7 +1,7 @@
@use "../colors" as c @use "../colors" as c
$list-width: 240px $list-width: 240px
$icon-size: 36px $icon-size: 32px
$icon-padding: 8px $icon-padding: 8px
.c-rooms .c-rooms
@ -10,7 +10,7 @@ $icon-padding: 8px
width: $list-width width: $list-width
font-size: 20px font-size: 20px
font-weight: 500 font-weight: 500
overflow-y: scroll overflow-y: auto
scrollbar-width: thin scrollbar-width: thin
scrollbar-color: c.$darkest c.$darker scrollbar-color: c.$darkest c.$darker
flex-shrink: 0 flex-shrink: 0
@ -18,12 +18,16 @@ $icon-padding: 8px
.c-room .c-room
display: flex display: flex
align-items: center align-items: center
padding: $icon-padding padding: $icon-padding * 0.75 $icon-padding
margin: $icon-padding * 0.25 0
cursor: pointer cursor: pointer
border-radius: 8px
&:hover &:not(&--active):hover
background-color: c.$mild background-color: c.$mild
border-radius: 8px
&--active
background-color: c.$milder
&__icon &__icon
width: $icon-size width: $icon-size