diff --git a/build/index.html b/build/index.html
index 3997893..fe38608 100644
--- a/build/index.html
+++ b/build/index.html
@@ -2,10 +2,10 @@
-
+
-
+
Carbon
diff --git a/build/static/directs.svg b/build/static/directs.svg
index cb3558a..b1ed08b 100644
--- a/build/static/directs.svg
+++ b/build/static/directs.svg
@@ -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">
@@ -61,7 +64,7 @@
transform="translate(36.739286,-225.97828)">
diff --git a/build/static/main.css b/build/static/main.css
index 177caf9..fa3314b 100644
--- a/build/static/main.css
+++ b/build/static/main.css
@@ -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;
diff --git a/build/static/room-picker.js b/build/static/room-picker.js
index aa35136..9099473 100644
--- a/build/static/room-picker.js
+++ b/build/static/room-picker.js
@@ -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) {
- this.style("transform", `translateY(${group.element.offsetTop}px)`)
+ 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())
diff --git a/build/static/store/Subscribable.js b/build/static/store/Subscribable.js
new file mode 100644
index 0000000..d205b79
--- /dev/null
+++ b/build/static/store/Subscribable.js
@@ -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}
diff --git a/build/static/store/SubscribeMapList.js b/build/static/store/SubscribeMapList.js
new file mode 100644
index 0000000..fb93574
--- /dev/null
+++ b/build/static/store/SubscribeMapList.js
@@ -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}
diff --git a/build/static/store/SubscribeValue.js b/build/static/store/SubscribeValue.js
new file mode 100644
index 0000000..16e5b88
--- /dev/null
+++ b/build/static/store/SubscribeValue.js
@@ -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}
diff --git a/build/static/store/store.js b/build/static/store/store.js
new file mode 100644
index 0000000..a4479ca
--- /dev/null
+++ b/build/static/store/store.js
@@ -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}
diff --git a/src/assets/icons/directs.svg b/src/assets/icons/directs.svg
index cb3558a..b1ed08b 100644
--- a/src/assets/icons/directs.svg
+++ b/src/assets/icons/directs.svg
@@ -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">
@@ -61,7 +64,7 @@
transform="translate(36.739286,-225.97828)">
diff --git a/src/js/room-picker.js b/src/js/room-picker.js
index aa35136..9099473 100644
--- a/src/js/room-picker.js
+++ b/src/js/room-picker.js
@@ -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) {
- this.style("transform", `translateY(${group.element.offsetTop}px)`)
+ 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())
diff --git a/src/js/store/Subscribable.js b/src/js/store/Subscribable.js
new file mode 100644
index 0000000..d205b79
--- /dev/null
+++ b/src/js/store/Subscribable.js
@@ -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}
diff --git a/src/js/store/SubscribeMap.js b/src/js/store/SubscribeMap.js
new file mode 100644
index 0000000..562d70f
--- /dev/null
+++ b/src/js/store/SubscribeMap.js
@@ -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}
diff --git a/src/js/store/SubscribeMapList.js b/src/js/store/SubscribeMapList.js
new file mode 100644
index 0000000..fb93574
--- /dev/null
+++ b/src/js/store/SubscribeMapList.js
@@ -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}
diff --git a/src/js/store/SubscribeValue.js b/src/js/store/SubscribeValue.js
new file mode 100644
index 0000000..16e5b88
--- /dev/null
+++ b/src/js/store/SubscribeValue.js
@@ -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}
diff --git a/src/js/store/store.js b/src/js/store/store.js
new file mode 100644
index 0000000..a4479ca
--- /dev/null
+++ b/src/js/store/store.js
@@ -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}
diff --git a/src/sass/components/groups.sass b/src/sass/components/groups.sass
index ee4c27a..10571dc 100644
--- a/src/sass/components/groups.sass
+++ b/src/sass/components/groups.sass
@@ -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
diff --git a/src/sass/components/rooms.sass b/src/sass/components/rooms.sass
index f176d1f..dc0f0e2 100644
--- a/src/sass/components/rooms.sass
+++ b/src/sass/components/rooms.sass
@@ -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,12 +18,16 @@ $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
+ border-radius: 8px
- &:hover
+ &:not(&--active):hover
background-color: c.$mild
- border-radius: 8px
+
+ &--active
+ background-color: c.$milder
&__icon
width: $icon-size