Add extremely janky unread messages banner
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
bc861125d8
commit
6e209bafd6
12 changed files with 257 additions and 72 deletions
|
@ -41,7 +41,7 @@ html
|
||||||
| )
|
| )
|
||||||
link(rel="stylesheet" type="text/css" href=getStatic("/sass/main.sass"))
|
link(rel="stylesheet" type="text/css" href=getStatic("/sass/main.sass"))
|
||||||
script(type="module" src=getStatic("/js/main.js"))
|
script(type="module" src=getStatic("/js/main.js"))
|
||||||
body
|
body.show-focus
|
||||||
main.main
|
main.main
|
||||||
.c-groups
|
.c-groups
|
||||||
.c-groups__display#c-groups-display
|
.c-groups__display#c-groups-display
|
||||||
|
@ -49,6 +49,7 @@ html
|
||||||
.c-groups__container#c-groups-list
|
.c-groups__container#c-groups-list
|
||||||
.c-rooms#c-rooms
|
.c-rooms#c-rooms
|
||||||
.c-chat
|
.c-chat
|
||||||
|
.c-chat-banner#c-chat-banner
|
||||||
.c-chat__messages#c-chat-messages
|
.c-chat__messages#c-chat-messages
|
||||||
.c-chat__inner#c-chat
|
.c-chat__inner#c-chat
|
||||||
.c-chat-input
|
.c-chat-input
|
||||||
|
|
|
@ -12,7 +12,7 @@ purifier.addHook("uponSanitizeAttribute", (node, hookevent, config) => {
|
||||||
|
|
||||||
const allowedElementAttributes = {
|
const allowedElementAttributes = {
|
||||||
"FONT": ["data-mx-bg-color", "data-mx-color", "color"],
|
"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"],
|
"A": ["name", "target", "href"],
|
||||||
"IMG": ["width", "height", "alt", "title", "src", "data-mx-emoticon"],
|
"IMG": ["width", "height", "alt", "title", "src", "data-mx-emoticon"],
|
||||||
"OL": ["start"],
|
"OL": ["start"],
|
||||||
|
@ -55,7 +55,7 @@ function cleanHTML(html) {
|
||||||
"color", "name", "target", "href", "width", "height", "alt", "title",
|
"color", "name", "target", "href", "width", "height", "alt", "title",
|
||||||
"src", "start", "class", "noreferrer", "noopener",
|
"src", "start", "class", "noreferrer", "noopener",
|
||||||
// matrix attrs
|
// 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
|
// Return a DOM fragment instead of a string, avoids potential future mutation XSS
|
||||||
|
|
11
src/js/focus.js
Normal file
11
src/js/focus.js
Normal 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")
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,3 +1,4 @@
|
||||||
|
require("./focus.js")
|
||||||
const groups = require("./groups.js")
|
const groups = require("./groups.js")
|
||||||
const chat_input = require("./chat-input.js")
|
const chat_input = require("./chat-input.js")
|
||||||
const room_picker = require("./room-picker.js")
|
const room_picker = require("./room-picker.js")
|
||||||
|
|
149
src/js/read-marker.js
Normal file
149
src/js/read-marker.js
Normal 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
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ const {SubscribeMap} = require("./store/subscribe_map.js")
|
||||||
const {store} = require("./store/store.js")
|
const {store} = require("./store/store.js")
|
||||||
const {Anchor} = require("./anchor.js")
|
const {Anchor} = require("./anchor.js")
|
||||||
const {Sender} = require("./sender.js")
|
const {Sender} = require("./sender.js")
|
||||||
|
const {ReadMarker, markFullyRead} = require("./read-marker.js")
|
||||||
const lsm = require("./lsm.js")
|
const lsm = require("./lsm.js")
|
||||||
const {resolveMxc} = require("./functions.js")
|
const {resolveMxc} = require("./functions.js")
|
||||||
const {renderEvent} = require("./events/render-event")
|
const {renderEvent} = require("./events/render-event")
|
||||||
|
@ -20,16 +21,6 @@ function getTxnId() {
|
||||||
return Date.now() + (sentIndex++)
|
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) {
|
function eventSearch(list, event, min = 0, max = NO_MAX) {
|
||||||
if (list.length === 0) return {success: false, i: 0}
|
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 {
|
class Timeline extends Subscribable {
|
||||||
constructor(room) {
|
constructor(room) {
|
||||||
super()
|
super()
|
||||||
|
|
|
@ -21,3 +21,39 @@ body
|
||||||
.main
|
.main
|
||||||
height: 100vh
|
height: 100vh
|
||||||
display: flex
|
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
|
||||||
|
|
|
@ -6,3 +6,4 @@ $milder: #42454a
|
||||||
$divider: #4b4e54
|
$divider: #4b4e54
|
||||||
$muted: #999
|
$muted: #999
|
||||||
$link: #57bffd
|
$link: #57bffd
|
||||||
|
$notify-highlight: #ffac4b
|
||||||
|
|
47
src/sass/components/chat-banner.sass
Normal file
47
src/sass/components/chat-banner.sass
Normal 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
|
|
@ -2,11 +2,12 @@
|
||||||
|
|
||||||
.c-chat
|
.c-chat
|
||||||
display: grid
|
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
|
align-items: end
|
||||||
flex: 1
|
flex: 1
|
||||||
|
|
||||||
&__messages
|
&__messages
|
||||||
|
position: relative
|
||||||
height: 100%
|
height: 100%
|
||||||
overflow-y: scroll
|
overflow-y: scroll
|
||||||
scrollbar-color: c.$darkest c.$darker
|
scrollbar-color: c.$darkest c.$darker
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@use "../colors" as c
|
||||||
|
|
||||||
.c-read-marker
|
.c-read-marker
|
||||||
display: none
|
display: none
|
||||||
position: relative
|
position: relative
|
||||||
|
@ -11,7 +13,7 @@
|
||||||
right: 0px
|
right: 0px
|
||||||
height: 2px
|
height: 2px
|
||||||
top: 0px
|
top: 0px
|
||||||
background-color: #ffac4b // TODO
|
background-color: c.$notify-highlight
|
||||||
|
|
||||||
@at-root .c-message:last-child &
|
@at-root .c-message:last-child &
|
||||||
top: 11px
|
top: 11px
|
||||||
|
@ -22,7 +24,7 @@
|
||||||
top: -9px
|
top: -9px
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
background-color: #ffac4b // TODO
|
background-color: c.$notify-highlight
|
||||||
color: #000
|
color: #000
|
||||||
font-size: 12px
|
font-size: 12px
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
|
|
|
@ -9,3 +9,4 @@
|
||||||
@use "./components/anchor"
|
@use "./components/anchor"
|
||||||
@use "./components/highlighted-code"
|
@use "./components/highlighted-code"
|
||||||
@use "./components/read-marker"
|
@use "./components/read-marker"
|
||||||
|
@use "./components/chat-banner"
|
||||||
|
|
Loading…
Reference in a new issue