const {q, ElemJS, ejs} = require("./basic.js") const {store} = require("./store/store.js") const {SubscribeMapList} = require("./store/subscribe_map_list.js") const {SubscribeValue} = require("./store/subscribe_value.js") const {Timeline} = require("./timeline.js") const lsm = require("./lsm.js") const {resolveMxc, extractLocalpart, extractDisplayName} = require("./functions.js") class ActiveGroupMarker extends ElemJS { constructor() { super(q("#c-group-marker")) store.activeGroup.subscribe("changeSelf", this.render.bind(this)) } render() { if (store.activeGroup.exists()) { const group = store.activeGroup.value() this.style("opacity", 1) this.style("transform", `translateY(${group.element.offsetTop}px)`) } else { this.style("opacity", 0) } } } const activeGroupMarker = new ActiveGroupMarker() class GroupNotifier extends ElemJS { constructor() { super("div") this.class("c-group__number") this.state = {} this.render() } update(state) { Object.assign(this.state, state) this.render() } clear() { this.state = {} this.render() } render() { let total = Object.values(this.state).reduce((a, c) => a + c, 0) if (total > 0) { this.text(total) this.class("c-group__number--active") } else { this.removeClass("c-group__number--active") } } } class Group extends ElemJS { constructor(key, data) { super("div") this.data = data this.order = this.data.order this.number = new GroupNotifier() this.class("c-group") this.child( (this.data.icon ? ejs("img").class("c-group__icon").attribute("src", this.data.icon) : ejs("div").class("c-group__icon") ), this.number, ejs("div").class("c-group__name").text(this.data.name) ) this.on("click", this.onClick.bind(this)) store.activeGroup.subscribe("changeSelf", this.render.bind(this)) } render() { const active = store.activeGroup.value() === this this.element.classList[active ? "add" : "remove"]("c-group--active") } onClick() { store.activeGroup.set(this) } } class RoomNotifier extends ElemJS { constructor(room) { super("div") this.class("c-room__number") this.room = room this.classes = [ "notifications", "unreads", "none" ] this.state = { notifications: 0, unreads: 0 } this.render() } /** * @param {object} state * @param {number} [state.notifications] * @param {number} [state.unreads] */ update(state) { Object.assign(this.state, state) this.informGroup() this.render() } informGroup() { this.room.getGroup().number.update({[this.room.id]: ( this.state.notifications || (this.state.unreads ? 1 : 0) )}) } render() { const display = { number: this.state.notifications || this.state.unreads, kind: this.state.notifications ? "notifications" : "unreads" } // set number if (display.number) { this.text(display.number) } else { this.text("") display.kind = "none" } // set class this.classes.forEach(c => { const name = "c-room__number--" + c if (c === display.kind) { this.class(name) } else { this.removeClass(name) } }) } } class Room extends ElemJS { constructor(id, data) { super("div") this.id = id this.data = data this.number = new RoomNotifier(this) this.timeline = new Timeline(this) this.group = null this.members = new SubscribeMapList(SubscribeValue) this.class("c-room") this.on("click", this.onClick.bind(this)) store.activeRoom.subscribe("changeSelf", this.render.bind(this)) this.render() } get order() { let string = "" if (this.number.state.notifications) { string += "N" } else if (this.number.state.unreads) { string += "U" } else { string += "_" } if (this.group) { string += this.name } else { string += (4000000000000 - this.timeline.latest) // good until 2065 :) } return string } getMemberName(mxid) { if (this.members.has(mxid)) { const state = this.members.get(mxid).value() return extractDisplayName(state) } else { return extractLocalpart(mxid) } } getHeroes() { if (this.data.summary) { return this.data.summary["m.heroes"] } else { const me = lsm.get("mx_user_id") return this.data.state.events.filter(e => e.type === "m.room.member" && e.content.membership === "join" && e.state_key !== me).map(e => e.state_key) } } getName() { // if the room has a name let name = this.data.state.events.find(e => e.type === "m.room.name") if (name && name.content.name) { return name.content.name } // if the room has no name, use its canonical alias let canonicalAlias = this.data.state.events.find(e => e.type === "m.room.canonical_alias") if (canonicalAlias && canonicalAlias.content.alias) { return canonicalAlias.content.alias } // if the room has no alias, use the names of its members ("heroes") const users = this.getHeroes() if (users && users.length) { const usernames = users.map(mxid => this.getMemberName(mxid)) return usernames.join(", ") } // the room is empty return "Empty room" } getIcon() { // if the room has a normal avatar const avatar = this.data.state.events.find(e => e.type === "m.room.avatar") if (avatar) { const url = avatar.content.url || avatar.content.avatar_url if (url) { return resolveMxc(url, 32, "crop") } } // if the room has no avatar set, use a member's avatar const users = this.getHeroes() if (users && users[0] && this.members.has(users[0])) { // console.log(users[0], this.members.get(users[0])) const userAvatar = this.members.get(users[0]).value().content.avatar_url if (userAvatar) { return resolveMxc(userAvatar, 32, "crop") } } return null } isDirect() { return store.directs.has(this.id) } setGroup(id) { this.group = id } getGroup() { if (this.group) { return store.groups.get(this.group).value() } else { return this.isDirect() ? store.groups.get("directs").value() : store.groups.get("channels").value() } } onClick() { store.activeRoom.set(this) } render() { this.clearChildren() // data const icon = this.getIcon() if (icon) { this.child(ejs("img").class("c-room__icon").attribute("src", icon)) } else { this.child(ejs("div").class("c-room__icon", "c-room__icon--no-icon")) } this.child(ejs("div").class("c-room__name").text(this.getName())) this.child(this.number) // active const active = store.activeRoom.value() === this this.element.classList[active ? "add" : "remove"]("c-room--active") } } class Rooms extends ElemJS { constructor() { super(q("#c-rooms")) this.roomData = [] this.rooms = [] store.rooms.subscribe("askAdd", this.askAdd.bind(this)) store.rooms.subscribe("addItem", this.addItem.bind(this)) // store.rooms.subscribe("changeItem", this.render.bind(this)) store.activeGroup.subscribe("changeSelf", this.render.bind(this)) store.directs.subscribe("changeItem", this.render.bind(this)) store.newEvents.subscribe("changeSelf", this.sort.bind(this)) store.notificationsChange.subscribe("changeSelf", this.sort.bind(this)) this.render() } sort() { store.rooms.sort() this.render() } askAdd(event, {key, data}) { const room = new Room(key, data) store.rooms.addEnd(key, room) } addItem(event, key) { const room = store.rooms.get(key).value() if (room.getGroup() === store.activeGroup.value()) { this.child(room) } } render() { this.clearChildren() let first = null // set room list store.rooms.forEach((id, room) => { if (room.value().getGroup() === store.activeGroup.value()) { if (!first) first = room.value() this.child(room.value()) } }) // if needed, change the active room to be an item in the room list if (!store.activeRoom.exists() || store.activeRoom.value().getGroup() !== store.activeGroup.value()) { if (first) { store.activeRoom.set(first) } else { store.activeRoom.delete() } } } } const rooms = new Rooms() class Groups extends ElemJS { constructor() { super(q("#c-groups-list")) store.groups.subscribe("askAdd", this.askAdd.bind(this)) store.groups.subscribe("changeItem", this.render.bind(this)) } askAdd(event, {key, data}) { const group = new Group(key, data) store.groups.addEnd(key, group) store.groups.sort() } render() { this.clearChildren() store.groups.forEach((key, item) => { item.value().number.clear() this.child(item.value()) }) store.rooms.forEach((id, room) => { room.value().number.informGroup() // update group notification number }) } } const groups = new Groups()