From be5d65883bf57caabfab64d53d5937e1b5407075 Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Sun, 13 Jul 2025 18:15:48 +0200 Subject: [PATCH 01/10] feat: add xxhash-wasm library --- package.json | 3 ++- pnpm-lock.yaml | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d7d19e..42347b1 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "pinia-plugin-persistedstate": "^4.2.0", "typescript": "^5.8.3", "vue": "^3.5.13", - "vue-router": "^4.5.1" + "vue-router": "^4.5.1", + "xxhash-wasm": "^1.1.0" }, "packageManager": "pnpm@10.11.0", "license": "MIT", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a8cf2c..6b461e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: vue-router: specifier: ^4.5.1 version: 4.5.1(vue@3.5.13(typescript@5.8.3)) + xxhash-wasm: + specifier: ^1.1.0 + version: 1.1.0 devDependencies: '@iconify-json/lucide': specifier: ^1.2.44 @@ -4744,6 +4747,9 @@ packages: engines: {node: '>= 0.10.0'} hasBin: true + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -10146,6 +10152,8 @@ snapshots: cssfilter: 0.0.10 optional: true + xxhash-wasm@1.1.0: {} + y18n@5.0.8: {} yallist@3.1.1: {} From 9b7de48c0276f860892da58d7087be16af3f6b2d Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Sun, 13 Jul 2025 18:16:02 +0200 Subject: [PATCH 02/10] feat: add IRC colours, without a toggle for now --- components/Message.vue | 5 ++++- components/MessageArea.vue | 1 + types/props.ts | 1 + utils/generateIrcColor.ts | 12 ++++++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 utils/generateIrcColor.ts diff --git a/components/Message.vue b/components/Message.vue index 2bcdcfc..e1d3c6c 100644 --- a/components/Message.vue +++ b/components/Message.vue @@ -40,7 +40,7 @@
- + {{ author?.display_name || author?.username }} @@ -71,6 +71,7 @@ import DOMPurify from 'dompurify'; import { parse } from 'marked'; import type { MessageProps } from '~/types/props'; +import generateIrcColor from '~/utils/generateIrcColor'; const props = defineProps(); @@ -117,6 +118,8 @@ onMounted(async () => { // showHover.value = !showHover.value; //} +console.log(props.authorColor) + const menuItems = [ { name: "Reply", callback: () => { if (messageElement.value) replyToMessage(messageElement.value, props) } } ] diff --git a/components/MessageArea.vue b/components/MessageArea.vue index d59b862..cae68f1 100644 --- a/components/MessageArea.vue +++ b/components/MessageArea.vue @@ -7,6 +7,7 @@ :margin-bottom="(messages[i + 1] && messagesType[messages[i + 1].uuid] == 'normal') ?? false" :last="i == messages.length - 1" :message-id="message.uuid" :author="message.user" :me="me" :message="message" :is-reply="message.reply_to" + :author-color="`${generateIrcColor(message.user.uuid)}`" :reply-message="message.reply_to ? getReplyMessage(message.reply_to) : undefined" />
diff --git a/types/props.ts b/types/props.ts index aa6ff0c..fe2b049 100644 --- a/types/props.ts +++ b/types/props.ts @@ -9,6 +9,7 @@ export interface MessageProps { format: "12" | "24", type: "normal" | "grouped", marginBottom: boolean, + authorColor: string, last: boolean, messageId: string, replyingTo?: boolean, diff --git a/utils/generateIrcColor.ts b/utils/generateIrcColor.ts new file mode 100644 index 0000000..d2ed643 --- /dev/null +++ b/utils/generateIrcColor.ts @@ -0,0 +1,12 @@ +import xxhash from "xxhash-wasm" + +const { h64 } = await xxhash() + +export default (seed: string): string => { + const lightness = 50 + + // this should probably be cached eventually + const idHash = h64(seed) + + return `hsl(${idHash % 360n}, 100%, ${lightness}%)` +} \ No newline at end of file From dc786cd42078a89466664f43dda316b9223e175e Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Sun, 13 Jul 2025 18:17:01 +0200 Subject: [PATCH 03/10] fix: remove random console log --- components/Message.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/Message.vue b/components/Message.vue index e1d3c6c..b6afc7b 100644 --- a/components/Message.vue +++ b/components/Message.vue @@ -118,8 +118,6 @@ onMounted(async () => { // showHover.value = !showHover.value; //} -console.log(props.authorColor) - const menuItems = [ { name: "Reply", callback: () => { if (messageElement.value) replyToMessage(messageElement.value, props) } } ] From 9bfe3310ccd4334e45436a79881c417e98c360c1 Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Mon, 14 Jul 2025 19:48:35 +0200 Subject: [PATCH 04/10] fix: comply with es2020 standards --- utils/generateIrcColor.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utils/generateIrcColor.ts b/utils/generateIrcColor.ts index d2ed643..24d4946 100644 --- a/utils/generateIrcColor.ts +++ b/utils/generateIrcColor.ts @@ -1,6 +1,9 @@ import xxhash from "xxhash-wasm" -const { h64 } = await xxhash() +let h64: CallableFunction; +(async () => { + h64 = (await xxhash()).h64; +})(); export default (seed: string): string => { const lightness = 50 From 25cd9a397e5ef5a7cb9396fe5e0ad1d7ec9e3fd7 Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Mon, 14 Jul 2025 20:05:31 +0200 Subject: [PATCH 05/10] feat: implement caching for hash function --- utils/generateIrcColor.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/utils/generateIrcColor.ts b/utils/generateIrcColor.ts index 24d4946..03a30e7 100644 --- a/utils/generateIrcColor.ts +++ b/utils/generateIrcColor.ts @@ -5,11 +5,9 @@ let h64: CallableFunction; h64 = (await xxhash()).h64; })(); -export default (seed: string): string => { - const lightness = 50 - - // this should probably be cached eventually - const idHash = h64(seed) +export default (seed: string, saturation: number = 100, lightness: number = 50): string => { + const idHash = useState(`h64Hash-${seed}`, () => h64(seed)) + const hashValue: bigint = idHash.value - return `hsl(${idHash % 360n}, 100%, ${lightness}%)` + return `hsl(${hashValue % 360n}, ${saturation}%, ${lightness}%)` } \ No newline at end of file From b319a06749199d4baab0e9c085592b33ba0b6bec Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Mon, 14 Jul 2025 21:36:41 +0200 Subject: [PATCH 06/10] feat: import function from JOHN OZBAY --- utils/isCanvasBlocked.ts | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 utils/isCanvasBlocked.ts diff --git a/utils/isCanvasBlocked.ts b/utils/isCanvasBlocked.ts new file mode 100644 index 0000000..3bd191e --- /dev/null +++ b/utils/isCanvasBlocked.ts @@ -0,0 +1,50 @@ +// +// Canvas Blocker & +// Firefox privacy.resistFingerprinting Detector. +// (c) 2018 // JOHN OZBAY // CRYPT.EE +// MIT License +// +export default () => { + // create a 1px image data + var blocked = false; + var canvas = document.createElement("canvas"); + var ctx = canvas.getContext("2d"); + + // some blockers just return an undefined ctx. So let's check that first. + if (ctx) { + var imageData = ctx.createImageData(1,1); + var originalImageData = imageData.data; + + // set pixels to RGB 128 + originalImageData[0]=128; + originalImageData[1]=128; + originalImageData[2]=128; + originalImageData[3]=255; + + // set this to canvas + ctx.putImageData(imageData,1,1); + + try { + // now get the data back from canvas. + var checkData = ctx.getImageData(1, 1, 1, 1).data; + + // If this is firefox, and privacy.resistFingerprinting is enabled, + // OR a browser extension blocking the canvas, + // This will return RGB all white (255,255,255) instead of the (128,128,128) we put. + + // so let's check the R and G to see if they're 255 or 128 (matching what we've initially set) + if (originalImageData[0] !== checkData[0] && originalImageData[1] !== checkData[1]) { + blocked = true; + console.log("Canvas is blocked. Will display warning."); + } + } catch (error) { + // some extensions will return getImageData null. this is to account for that. + blocked = true; + console.log("Canvas is blocked. Will display warning."); + } + } else { + blocked = true; + console.log("Canvas is blocked. Will display warning."); + } + return blocked; +} \ No newline at end of file From f4ddcf9d8db61d4c4704514de43cbe186fde9ac8 Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Mon, 14 Jul 2025 21:37:45 +0200 Subject: [PATCH 07/10] fix: prop --- types/props.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/props.ts b/types/props.ts index fe2b049..9a8e642 100644 --- a/types/props.ts +++ b/types/props.ts @@ -3,7 +3,7 @@ import type { MessageResponse, UserResponse } from "./interfaces"; export interface MessageProps { class?: string, img?: string | null, - author?: UserResponse + author: UserResponse text: string, timestamp: number, format: "12" | "24", From f98e8c611092e747e267afd029cf87eeebf21cca Mon Sep 17 00:00:00 2001 From: JustTemmie <47639983+JustTemmie@users.noreply.github.com> Date: Mon, 14 Jul 2025 21:39:00 +0200 Subject: [PATCH 08/10] feat: implement generic component --- components/Avatar.vue | 37 ++++++++++++++++++++++++++++++++++ components/User/UserEntry.vue | 8 +++++--- components/User/UserPopup.vue | 3 +-- layouts/client.vue | 2 +- utils/generateDefaultIcon.ts | 38 +++++++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 components/Avatar.vue create mode 100644 utils/generateDefaultIcon.ts diff --git a/components/Avatar.vue b/components/Avatar.vue new file mode 100644 index 0000000..368316c --- /dev/null +++ b/components/Avatar.vue @@ -0,0 +1,37 @@ + + + \ No newline at end of file diff --git a/components/User/UserEntry.vue b/components/User/UserEntry.vue index b463759..b539f2c 100644 --- a/components/User/UserEntry.vue +++ b/components/User/UserEntry.vue @@ -1,8 +1,8 @@ @@ -12,6 +12,8 @@ import type { UserResponse } from '~/types/interfaces'; const props = defineProps<{ user: UserResponse }>(); + +const displayName = props.user.display_name || props.user.username