Skip to content

Commit

Permalink
Merge pull request #361 from Zekiah-A/main
Browse files Browse the repository at this point in the history
[BIG] Fix chat replies & other recessions, baseline clientside chat reactions implementation & emoji panel component
  • Loading branch information
Zekiah-A authored Nov 30, 2024
2 parents 85cd8e0 + 6b59acc commit 1148323
Show file tree
Hide file tree
Showing 9 changed files with 538 additions and 211 deletions.
340 changes: 193 additions & 147 deletions index.html

Large diffs are not rendered by default.

161 changes: 109 additions & 52 deletions live-chat-elements.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { LitElement, html, css, styleMap } from "./lit.all.min.js"
import { LitElement, html, styleMap, unsafeHTML } from "./lit.all.min.js"
// @ts-expect-error Hack to access window globals from module script
// eslint-disable-next-line @typescript-eslint/no-unused-vars
var { cMessages, currentChannel, chatMessages, x, y, pos, chatMentionUser, onChatContext, chatReply, chatReport, chatModerate, CHAT_COLOURS, hash } = window.moduleExports
var { cMessages, currentChannel, chatMessages, x, y, pos, chatMentionUser, onChatContext, chatReply, chatReport, chatModerate, CHAT_COLOURS, hash, chatReact, EMOJIS, CUSTOM_EMOJIS, intIdNames } = window.moduleExports

class LiveChatMessage extends LitElement {
static properties = {
Expand All @@ -11,7 +11,10 @@ class LiveChatMessage extends LitElement {
sendDate: { type: Number, reflect: true, attribute: "senddate" },
repliesTo: { type: Number, reflect: true, attribute: "repliesto" },
content: { type: String, reflect: true, attribute: "content" },
class: { reflect: true }
reactions: { reflect: true, attribute: false },
class: { reflect: true },
showReactionsPanel: { type: Boolean, attribute: false },
openedReactionDetails: { type: String, attribute: false }
}

constructor() {
Expand All @@ -20,10 +23,13 @@ class LiveChatMessage extends LitElement {
this.senderId = null
this.name = null
this.sendDate = null
this.repliesTo = null
/** @type {LiveChatMessage|null} */ this.repliesTo = null
this.content = null
this.reactions = null
/** @type {Map<string, Set<number>>|null} */ this.reactions = null
this.replyingMessage = null
this.showReactionsPanel = false
this.openedReactionDetails = ""
this.addEventListener("contextmenu", this.#handleContextMenu)
}

connectedCallback() {
Expand All @@ -39,7 +45,7 @@ class LiveChatMessage extends LitElement {
/**
* @param {{ messageId: any; txt: any; senderId: any; name: any; sendDate: any; repliesTo?: null | undefined; }} data
*/
setMessageData(data) {
fromMessage(data) {
const { messageId, txt, senderId, name, sendDate, repliesTo = null } = data
this.messageId = messageId
this.senderId = senderId
Expand All @@ -54,7 +60,7 @@ class LiveChatMessage extends LitElement {
*/
willUpdate(changedProperties) {
if (changedProperties.has("repliesTo") && this.repliesTo !== null) {
this.replyingMessage = this.findReplyingMessage()
this.replyingMessage = this.#findReplyingMessage()
}
if (changedProperties.has("class")) {
this.classList.add("message")
Expand All @@ -65,7 +71,7 @@ class LiveChatMessage extends LitElement {
* @param {string} txt
* @returns {string}
*/
formatMessage(txt) {
#formatMessage(txt) {
if (!txt) return ""

let formatted = sanitise(txt)
Expand All @@ -76,52 +82,43 @@ class LiveChatMessage extends LitElement {
const isLargeEmoji = formatted.match(new RegExp(source)).length === 1 &&
!formatted.replace(full, "").trim()
const size = isLargeEmoji ? "48" : "16"
return `<img src="custom_emojis/${source}.png" alt=":${source}:" title=":${source}:" width="${size}" height="${size}">`
return html`<img src="custom_emojis/${source}.png" alt=":${source}:" title=":${source}:" width="${size}" height="${size}">`
})

// Handle coordinates
formatted = formatted.replaceAll(/([0-9]+),\s*([0-9]+)/g, (match, px, py) => {
px = parseInt(px.trim())
py = parseInt(py.trim())
if (!isNaN(px) && !isNaN(py)) {
return `<a href="#" @click=${(e) => this.handleCoordinateClick(e, px, py)}>${px},${py}</a>`
return html`<a href="#" @click=${(e) => this.#handleCoordinateClick(e, px, py)}>${px},${py}</a>`
}
return match
})

return formatted
}

findReplyingMessage() {
#findReplyingMessage() {
if (!cMessages[currentChannel]) {
return { name: "[ERROR]", originalContent: "Channel not found", fake: true }
return { name: "[ERROR]", content: "Channel not found", fake: true }
}

const message = cMessages[currentChannel].find(msg => msg.messageId === this.repliesTo)
return message || {
name: "[?????]",
originalContent: translate("messageNotFound"),
content: translate("messageNotFound"),
fake: true
}
}

scrollToReply() {
#scrollToReply() {
if (!this.replyingMessage || this.replyingMessage.fake || !chatMessages) {
return
}

const height = Array.from(cMessages[currentChannel])
.slice(0, cMessages[currentChannel].indexOf(this.replyingMessage))
.reduce((sum, msg) => sum + msg.offsetHeight, 0)


this.replyingMessage.setAttribute("highlight", "true")
setTimeout(() => this.replyingMessage.removeAttribute("highlight"), 500)

chatMessages.scroll({
top: height,
left: 0,
behavior: "smooth"
})
this.replyingMessage.scrollIntoView({ behavior: "smooth", block: "nearest" })
}

/**
Expand All @@ -130,90 +127,150 @@ class LiveChatMessage extends LitElement {
* @param {number} newX
* @param {number} newY
*/
handleCoordinateClick(e, newX, newY) {
#handleCoordinateClick(e, newX, newY) {
e.preventDefault()
x = newX
y = newY
pos()
}

handleNameClick() {
#handleNameClick() {
if (this.messageId > 0) {
chatMentionUser(this.senderId)
}
}

handleContextMenu(e) {
#handleContextMenu(e) {
e.preventDefault()
if (this.messageId > 0) {
onChatContext(e, this.senderId, this.messageId)
}
}

handleReply() {
#handleReply() {
chatReply(this.messageId, this.senderId)
}

handleReport() {
#handleReport() {
chatReport(this.messageId, this.senderId)
}

handleModerate() {
#handleModerate() {
chatModerate("delete", this.senderId, this.messageId, this)
}

#handleReact(e) {
const { key } = e.detail
chatReact(this.messageId, key)
}

renderName() {
#renderName() {
const nameStyle = {
color: this.messageId === 0 ? undefined : CHAT_COLOURS[hash("" + this.senderId) & 7]
}

return html`
<span
class="name ${this.messageId === 0 ? "rainbow-glow" : ''}" style=${styleMap(nameStyle)}
class="name ${this.messageId === 0 ? "rainbow-glow" : ""}" style=${styleMap(nameStyle)}
title=${new Date(this.sendDate * 1000).toLocaleString()}
@click=${this.handleNameClick}>[${this.name || ("#" + this.senderId)}]</span>
`
@click=${this.#handleNameClick}>[${this.name || ("#" + this.senderId)}]</span>`
}

renderReply() {
#renderReply() {
if (!this.repliesTo || !this.replyingMessage) {
return null
}

return html`
<p class="reply" @click=${() => this.scrollToReply()}>
โ†ช๏ธ ${this.replyingMessage.name} ${this.replyingMessage.originalContent}
</p>
`
<p class="reply" @click=${this.#scrollToReply}>
โ†ช๏ธ ${this.replyingMessage.name} ${this.replyingMessage.content}
</p>`
}

renderActions() {
#renderActions() {
if (this.messageId <= 0) {
return null
}

return html`
<div class="actions">
<img class="action-button" src="svg/reply-action.svg"
title=${translate("replyTo")} tabindex="0" @click=${this.handleReply}>
<img class="action-button" src="svg/report-action.svg"
title=${translate("report")} tabindex="0" @click=${this.handleReport}>
title=${translate("replyTo")} tabindex="0" @click=${this.#handleReply}>
<img class="action-button" src="svg/react-action.svg"
title=${translate("react")} tabindex="0" @click=${this.handleReact}>
title=${translate("addReaction")} tabindex="0" @click=${() => this.showReactionsPanel = true}>
<img class="action-button" src="svg/report-action.svg"
title=${translate("report")} tabindex="0" @click=${this.#handleReport}>
${localStorage.vip?.startsWith("!") ? html`
<img class="action-button" src="svg/moderate-action.svg"
title=${translate("Moderation options")} tabindex="0" @click=${this.handleModerate}>
title=${translate("Moderation options")} tabindex="0" @click=${this.#handleModerate}>
` : null}
</div>
${this.showReactionsPanel
? html`<r-emoji-panel @selectionchanged=${this.#handleReact} @close=${() => this.showReactionsPanel = false}
@mouseleave=${() => this.showReactionsPanel = false}></r-emoji-panel>`
: null}`
}

#renderReactions() {
if (this.reactions == null) {
return null
}

return html`
<ul class="reactions">
${this.reactions.entries().map(([emojiKey, reactors]) => {
let emojiEl = null
if (EMOJIS.has(emojiKey)) {
emojiEl = html`<span class="emoji ${this.openedReactionDetails === emojiKey ? "expanded" : ""}">${EMOJIS.get(emojiKey)}</span>`
}
else if (CUSTOM_EMOJIS.has(emojiKey)) {
emojiEl = html`<img src="custom_emojis/${emojiKey}.png" class="emoji ${this.openedReactionDetails === emojiKey ? "expanded" : ""}" alt=":${emojiKey}:" title=":${emojiKey}:" width="18" height="18">`
}
if (!emojiEl) {
return null
}
return html`
<li class="reaction ${this.openedReactionDetails == emojiKey ? "expanded" : ""}">
<details class="reaction-details" ?open=${this.openedReactionDetails === emojiKey} @toggle=${(e) => {
if (e.target.open) {
this.openedReactionDetails = emojiKey
}
else if (this.openedReactionDetails === emojiKey) {
this.openedReactionDetails = ""
}
}}>
<summary>
<div class="emoji-container">
${emojiEl}
${this.openedReactionDetails == emojiKey ? html`<p>:${emojiKey}:</p>` : null}
</div>
</summary>
<div class="reaction-body">
<hr>
<h3>Added by:</h3>
<ul class="reactors">
${reactors.entries().map(([reactorId]) => html`
<li class="reactor" title=${"User ID: #" + reactorId}>
${intIdNames.has(reactorId)
? intIdNames.get(reactorId)
: "#" + reactorId}
</li>`)}
</ul>
</div>
</details>
</li>`
})}
</ul>
`
}

render() {
return html`
${this.renderReply()}
${this.renderName()}
<span .innerHTML=${this.formatMessage(this.content)}></span>
${this.renderActions()}
`
${this.#renderReply()}
${this.#renderName()}
<span>${unsafeHTML(this.#formatMessage(this.content))}</span>
${this.#renderReactions()}
${this.#renderActions()}`
}
}
customElements.define("r-live-chat-message", LiveChatMessage)
Expand Down
2 changes: 1 addition & 1 deletion posts-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ class Votes extends LitElement {
}

#notifyVote() {
const event = new CustomEvent("vote-changed", {
const event = new CustomEvent("votechanged", {
detail: { voted: this.voted },
bubbles: true,
composed: true
Expand Down
2 changes: 1 addition & 1 deletion posts.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<link href="style.css?v=354" rel="stylesheet">
<link href="posts.css?v=2" rel="stylesheet">
<script type="application/javascript" src="shared.js?v=6"></script>
<script type="application/javascript" src="shared-elements.js?v=8" defer></script>
<script id="postsManager" type="application/javascript" src="posts-manager.js?v=6" defer></script>
<script type="module" src="shared-elements.js?v=9" defer></script>
<script type="module" src="posts-elements.js?v=8" defer></script>
<script>
WebSocket.prototype.send = function(){this.close()}; document.execCommand = (_) => {window.location.reload(true)};
Expand Down
17 changes: 12 additions & 5 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,18 @@ const DEFAULT_EMOJIS = new Map([
[ "heart", "โค๏ธ" ],
[ "moyai", "๐Ÿ—ฟ" ],
[ "bruh", "๐Ÿ—ฟ" ],
[ "turkey", "๐Ÿ‡น๐Ÿ‡ท" ],
[ "skull", "๐Ÿ’€" ],
[ "sus", "เถž" ],
[ "tr", "๐Ÿ‡น๐Ÿ‡ท" ],
[ "turkey", "๐Ÿ‡น๐Ÿ‡ท" ],
[ "ir", "๐Ÿ‡ฎ๐Ÿ‡ท" ],
[ "iran", "๐Ÿ‡ฎ๐Ÿ‡ท" ],
[ "uk", "๐Ÿ‡ฌ๐Ÿ‡ง" ],
[ "britain", "๐Ÿ‡ฌ๐Ÿ‡ง" ],
[ "usa", "๐Ÿ‡บ๐Ÿ‡ธ" ],
[ "america", "๐Ÿ‡บ๐Ÿ‡ธ" ],
[ "ru", "๐Ÿ‡ท๐Ÿ‡บ" ],
[ "russia", "๐Ÿ‡ท๐Ÿ‡บ" ],
[ "eyes", "๐Ÿ‘€" ],
[ "fire", "๐Ÿ”ฅ" ],
[ "thumbsup", "๐Ÿ‘" ],
Expand Down Expand Up @@ -153,7 +158,9 @@ const DEFAULT_EMOJIS = new Map([
[ "deaf", "๐Ÿง" ],
[ "mew", "๐Ÿคซ๐Ÿง" ],
[ "pray", "๐Ÿ™" ],
[ "thinking", "๐Ÿค”" ]
[ "thinking", "๐Ÿค”" ],
[ "sweat", "๐Ÿ˜…" ],
[ "wave", "๐Ÿ‘‹"]
])
const DEFAULT_CUSTOM_EMOJIS = new Map([
[ "amogus", "custom_emojis/amogus.png" ],
Expand Down Expand Up @@ -1064,8 +1071,8 @@ const serverOptions:TLSWebSocketServeOptions<ClientData> = {
const reactionBuf = Buffer.alloc(9 + encodedReaction.byteLength)
reactionBuf[0] = 18
reactionBuf.writeUint32BE(messageId, 1)
reactionBuf.writeUint32BE(ws.data.intId, 1)
reactionBuf.set(encodedReaction, 5)
reactionBuf.writeUint32BE(ws.data.intId, 5)
reactionBuf.set(encodedReaction, 9)
wss.publish("all", reactionBuf)
break
}
Expand Down Expand Up @@ -1322,7 +1329,7 @@ const serverOptions:TLSWebSocketServeOptions<ClientData> = {
ws.data.shadowBanned = true
const sanitisedDetail = detail.replaceAll("```", "`โ€‹`โ€‹`โ€‹")
modWebhookLog("Client activity reported webdriver usage:\nClient data:\n```\n" +
`Ip: ${ws.data.ip}\nChat name: ${ws.data.chatName}\nUser id: ${ws.data.intId}\nPerms: ${ws.data.perms}\nHeaders: ${JSON.stringify(ws.data.headers, null, 4)}\n` +
`Ip: ${ws.data.ip}\nChat name: ${ws.data.chatName}\nUser ID: ${ws.data.intId}\nPerms: ${ws.data.perms}\nHeaders: ${JSON.stringify(ws.data.headers, null, 4)}\n` +
`Connect date: ${new Date(ws.data.connDate).toLocaleString()}\nLast period captcha: ${new Date(ws.data.lastPeriodCaptcha).toLocaleString()}\n` +
`\`\`\`\nClient details (untrusted):\n\`\`\`\n${sanitisedDetail}\n\`\`\`\nServer has temporarily shadowbanned this connection.`)
break
Expand Down
Loading

0 comments on commit 1148323

Please sign in to comment.