Add extremely janky unread messages banner
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Cadence Ember 2020-11-26 16:38:12 +13:00
parent bc861125d8
commit 6e209bafd6
Signed by: cadence
GPG key ID: BC1C2C61CF521B17
12 changed files with 257 additions and 72 deletions

View file

@ -41,7 +41,7 @@ html
| )
link(rel="stylesheet" type="text/css" href=getStatic("/sass/main.sass"))
script(type="module" src=getStatic("/js/main.js"))
body
body.show-focus
main.main
.c-groups
.c-groups__display#c-groups-display
@ -49,6 +49,7 @@ html
.c-groups__container#c-groups-list
.c-rooms#c-rooms
.c-chat
.c-chat-banner#c-chat-banner
.c-chat__messages#c-chat-messages
.c-chat__inner#c-chat
.c-chat-input

View file

@ -12,7 +12,7 @@ purifier.addHook("uponSanitizeAttribute", (node, hookevent, config) => {
const allowedElementAttributes = {
"FONT": ["data-mx-bg-color", "data-mx-color", "color"],
"SPAN": ["data-mx-bg-color", "data-mx-color"],
"SPAN": ["data-mx-bg-color", "data-mx-color", "data-mx-spoiler"],
"A": ["name", "target", "href"],
"IMG": ["width", "height", "alt", "title", "src", "data-mx-emoticon"],
"OL": ["start"],
@ -55,7 +55,7 @@ function cleanHTML(html) {
"color", "name", "target", "href", "width", "height", "alt", "title",
"src", "start", "class", "noreferrer", "noopener",
// matrix attrs
"data-mx-emoticon", "data-mx-bg-color", "data-mx-color"
"data-mx-emoticon", "data-mx-bg-color", "data-mx-color", "data-mx-spoiler"
],
// Return a DOM fragment instead of a string, avoids potential future mutation XSS

11
src/js/focus.js Normal file
View file

@ -0,0 +1,11 @@
document.body.classList.remove("show-focus")
document.addEventListener("mousedown", () => {
document.body.classList.remove("show-focus")
})
document.addEventListener("keydown", event => {
if (event.key === "Tab") {
document.body.classList.add("show-focus")
}
})

View file

@ -1,3 +1,4 @@
require("./focus.js")
const groups = require("./groups.js")
const chat_input = require("./chat-input.js")
const room_picker = require("./room-picker.js")

149
src/js/read-marker.js Normal file
View file

@ -0,0 +1,149 @@
const {ElemJS, ejs, q} = require("./basic.js")
const {store} = require("./store/store.js")
const lsm = require("./lsm.js")
function markFullyRead(roomID, eventID) {
return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${roomID}/read_markers?access_token=${lsm.get("access_token")}`, {
method: "POST",
body: JSON.stringify({
"m.fully_read": eventID,
"m.read": eventID
})
})
}
class ReadBanner extends ElemJS {
constructor() {
super(q("#c-chat-banner"))
this.newMessages = ejs("span")
this.child(
ejs("div").class("c-chat-banner__inner").child(
ejs("button").class("c-chat-banner__part").on("click", this.jumpTo.bind(this)).child(
ejs("div").class("c-chat-banner__part-inner")
.child(this.newMessages)
.addText(" new messages")
),
ejs("button").class("c-chat-banner__part", "c-chat-banner__last").on("click", this.markRead.bind(this)).child(
ejs("div").class("c-chat-banner__part-inner").text("Mark as read")
)
)
)
store.activeRoom.subscribe("changeSelf", this.render.bind(this))
store.notificationsChange.subscribe("changeSelf", this.render.bind(this))
this.render()
}
async jumpTo() {
if (!store.activeRoom.exists()) return
const timeline = store.activeRoom.value().timeline
const readMarker = timeline.readMarker
while (true) {
if (readMarker.attached) {
readMarker.element.scrollIntoView({behavior: "smooth", block: "center"})
return
} else {
q("#c-chat-messages").scrollTo({
top: 0,
left: 0,
behavior: "smooth"
})
await new Promise(resolve => {
const unsubscribe = timeline.subscribe("afterScrollbackLoad", () => {
unsubscribe()
resolve()
})
})
}
}
}
markRead() {
if (!store.activeRoom.exists()) return
const timeline = store.activeRoom.value().timeline
markFullyRead(timeline.id, timeline.latestEventID)
}
render() {
let count = 0
if (store.activeRoom.exists()) {
count = store.activeRoom.value().number.state.unreads
}
if (count !== 0) {
this.newMessages.text(count)
this.class("c-chat-banner--active")
} else {
this.removeClass("c-chat-banner--active")
}
}
}
const readBanner = new ReadBanner()
class ReadMarker extends ElemJS {
constructor(timeline) {
super("div")
this.class("c-read-marker")
this.loadingIcon = ejs("div")
.class("c-read-marker__loading", "loading-icon")
.style("display", "none")
this.child(
ejs("div").class("c-read-marker__inner").child(
ejs("div").class("c-read-marker__text").child(this.loadingIcon).addText("New")
)
)
let processing = false
const observer = new IntersectionObserver(entries => {
const entry = entries[0]
if (!entry.isIntersecting) return
if (processing) return
processing = true
this.loadingIcon.style("display", "")
markFullyRead(this.timeline.id, this.timeline.latestEventID).then(() => {
this.loadingIcon.style("display", "none")
processing = false
})
}, {
root: document.getElementById("c-chat-messages"),
rootMargin: "-80px 0px 0px 0px", // marker must be this distance inside the top of the screen to be counted as read
threshold: 0.01
})
observer.observe(this.element)
this.attached = false
this.timeline = timeline
this.timeline.userReads.get(lsm.get("mx_user_id")).subscribe("changeSelf", (_, eventID) => {
// read marker updated, attach to it
const event = this.timeline.map.get(eventID)
this.attach(event)
})
this.timeline.subscribe("afterChange", () => {
// timeline has new events, attach to last read one
const eventID = this.timeline.userReads.get(lsm.get("mx_user_id")).value()
const event = this.timeline.map.get(eventID)
this.attach(event)
})
}
attach(event) {
if (event && event.data.origin_server_ts !== this.timeline.latest) {
this.class("c-read-marker--attached")
event.element.insertAdjacentElement("beforeend", this.element)
this.attached = true
} else {
this.removeClass("c-read-marker--attached")
this.attached = false
}
if (store.activeRoom.value() === this.timeline.room) {
readBanner.render()
}
}
}
module.exports = {
ReadMarker,
readBanner,
markFullyRead
}

View file

@ -5,6 +5,7 @@ const {SubscribeMap} = require("./store/subscribe_map.js")
const {store} = require("./store/store.js")
const {Anchor} = require("./anchor.js")
const {Sender} = require("./sender.js")
const {ReadMarker, markFullyRead} = require("./read-marker.js")
const lsm = require("./lsm.js")
const {resolveMxc} = require("./functions.js")
const {renderEvent} = require("./events/render-event")
@ -20,16 +21,6 @@ function getTxnId() {
return Date.now() + (sentIndex++)
}
function markFullyRead(roomID, eventID) {
return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${roomID}/read_markers?access_token=${lsm.get("access_token")}`, {
method: "POST",
body: JSON.stringify({
"m.fully_read": eventID,
"m.read": eventID
})
})
}
function eventSearch(list, event, min = 0, max = NO_MAX) {
if (list.length === 0) return {success: false, i: 0}
@ -184,62 +175,6 @@ class ReactiveTimeline extends ElemJS {
}
}
class ReadMarker extends ElemJS {
constructor(timeline) {
super("div")
this.class("c-read-marker")
this.loadingIcon = ejs("div")
.class("c-read-marker__loading", "loading-icon")
.style("display", "none")
this.child(
ejs("div").class("c-read-marker__inner").child(
ejs("div").class("c-read-marker__text").child(this.loadingIcon).addText("New")
)
)
let processing = false
const observer = new IntersectionObserver(entries => {
const entry = entries[0]
if (!entry.isIntersecting) return
if (processing) return
processing = true
this.loadingIcon.style("display", "")
markFullyRead(this.timeline.id, this.timeline.latestEventID).then(() => {
this.loadingIcon.style("display", "none")
processing = false
})
}, {
root: document.getElementById("c-chat-messages"),
rootMargin: "-80px 0px 0px 0px", // marker must be this distance inside the top of the screen to be counted as read
threshold: 0.01
})
observer.observe(this.element)
this.timeline = timeline
this.timeline.userReads.get(lsm.get("mx_user_id")).subscribe("changeSelf", (_, eventID) => {
// read marker updated, attach to it
const event = this.timeline.map.get(eventID)
this.attach(event)
})
this.timeline.subscribe("afterChange", () => {
// timeline has new events, attach to last read one
const eventID = this.timeline.userReads.get(lsm.get("mx_user_id")).value()
const event = this.timeline.map.get(eventID)
this.attach(event)
})
}
attach(event) {
if (event && event.data.origin_server_ts !== this.timeline.latest) {
this.class("c-read-marker--attached")
event.element.insertAdjacentElement("beforeend", this.element)
} else {
this.removeClass("c-read-marker--attached")
}
}
}
class Timeline extends Subscribable {
constructor(room) {
super()

View file

@ -21,3 +21,39 @@ body
.main
height: 100vh
display: flex
button
appearance: none
border: none
background: none
color: inherit
font-family: inherit
font-size: inherit
font-style: inherit
font-weight: inherit
padding: 0
margin: 0
line-height: inherit
cursor: inherit
// focus resets
:focus
outline: none
:-moz-focusring
outline: none
::-moz-focus-inner
border: 0
select:-moz-focusring
color: transparent
text-shadow: 0 0 0 #ddd
body.show-focus
a, select, button, input, video
outline-color: #fff
&:focus
outline: 2px dotted

View file

@ -6,3 +6,4 @@ $milder: #42454a
$divider: #4b4e54
$muted: #999
$link: #57bffd
$notify-highlight: #ffac4b

View file

@ -0,0 +1,47 @@
@use "../colors" as c
.c-chat-banner
position: sticky
z-index: 1
top: 0
left: 0
right: 0
margin-right: 12px
outline-color: #000
opacity: 0
&--active
opacity: 1
&__inner
display: grid
grid-template-columns: 1fr auto
background: c.$notify-highlight
color: #000
margin: 0px 12px
padding: 0px 12px
border-radius: 0px 0px 10px 10px
line-height: 1
box-shadow: 0px 5px 5px -2px rgba(0, 0, 0, 0.1)
cursor: pointer
&:hover
box-shadow: 0px 5px 5px -2px rgba(0, 0, 0, 0.6)
&__part
padding: 6px 0px 8px
&:hover
text-decoration: underline
&__part-inner
display: block
width: 100% // yes, really.
text-align: left
&__last
margin-left: 8px
&__last &__part-inner
border-left: 1px solid #222
padding-left: 8px

View file

@ -2,11 +2,12 @@
.c-chat
display: grid
grid-template-rows: 1fr 82px // fixed so that input box height adjustment doesn't mess up scroll
grid-template-rows: 0 1fr 82px // fixed so that input box height adjustment doesn't mess up scroll
align-items: end
flex: 1
&__messages
position: relative
height: 100%
overflow-y: scroll
scrollbar-color: c.$darkest c.$darker

View file

@ -1,3 +1,5 @@
@use "../colors" as c
.c-read-marker
display: none
position: relative
@ -11,7 +13,7 @@
right: 0px
height: 2px
top: 0px
background-color: #ffac4b // TODO
background-color: c.$notify-highlight
@at-root .c-message:last-child &
top: 11px
@ -22,7 +24,7 @@
top: -9px
display: flex
align-items: center
background-color: #ffac4b // TODO
background-color: c.$notify-highlight
color: #000
font-size: 12px
font-weight: 600

View file

@ -9,3 +9,4 @@
@use "./components/anchor"
@use "./components/highlighted-code"
@use "./components/read-marker"
@use "./components/chat-banner"