Compare commits
11 commits
06de4777f9
...
cbc010943c
Author | SHA1 | Date | |
---|---|---|---|
cbc010943c | |||
e7558d9a95 | |||
f98e8c6110 | |||
f4ddcf9d8d | |||
b319a06749 | |||
25cd9a397e | |||
bbc822604f | |||
9bfe3310cc | |||
dc786cd420 | |||
9b7de48c02 | |||
be5d65883b |
15 changed files with 177 additions and 30 deletions
37
components/Avatar.vue
Normal file
37
components/Avatar.vue
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<Icon v-if="canvasBlocked"
|
||||||
|
name="lucide:user" />
|
||||||
|
<NuxtImg v-else
|
||||||
|
:src="displayAvatar"
|
||||||
|
:alt="displayName" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { NuxtImg } from '#components';
|
||||||
|
import type { GuildMemberResponse, UserResponse } from '~/types/interfaces';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
user?: UserResponse,
|
||||||
|
member?: GuildMemberResponse,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
|
||||||
|
let displayName: string
|
||||||
|
let displayAvatar: string
|
||||||
|
let canvasBlocked = false
|
||||||
|
|
||||||
|
const user = props.user || props.member?.user
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
displayName = props.member?.nickname
|
||||||
|
|| user.display_name
|
||||||
|
|| user.username
|
||||||
|
|
||||||
|
if (user.avatar) {
|
||||||
|
displayAvatar = user.avatar
|
||||||
|
} else {
|
||||||
|
displayAvatar = generateDefaultIcon(displayName, user.uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="member-item" @click="togglePopup" @blur="hidePopup" tabindex="0">
|
<div class="member-item" @click="togglePopup" @blur="hidePopup" tabindex="0">
|
||||||
<img v-if="props.member.user.avatar" class="member-avatar" :src="props.member.user.avatar" :alt="props.member.user.display_name ?? props.member.user.username" />
|
<Avatar :member="props.member" class="member-avatar"/>
|
||||||
<Icon v-else class="member-avatar" name="lucide:user" />
|
|
||||||
<span class="member-display-name">{{ props.member.user.display_name || props.member.user.username }}</span>
|
<span class="member-display-name">{{ props.member.user.display_name || props.member.user.username }}</span>
|
||||||
<UserPopup v-if="isPopupVisible" :user="props.member.user" id="profile-popup" />
|
<UserPopup v-if="isPopupVisible" :user="props.member.user" id="profile-popup" />
|
||||||
</div>
|
</div>
|
|
@ -13,7 +13,7 @@
|
||||||
<VerticalSpacer />
|
<VerticalSpacer />
|
||||||
|
|
||||||
<div id="direct-message-list">
|
<div id="direct-message-list">
|
||||||
<UserEntry v-for="user of friends" :user="user" :name="user.display_name || user.username"
|
<UserEntry v-for="user of friends" :user="user"
|
||||||
:href="`/me/${user.uuid}`"/>
|
:href="`/me/${user.uuid}`"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,22 +4,16 @@
|
||||||
:editing.sync="props.editing" :replying-to.sync="props.replyingTo">
|
:editing.sync="props.editing" :replying-to.sync="props.replyingTo">
|
||||||
<div v-if="props.replyMessage" class="message-reply-svg">
|
<div v-if="props.replyMessage" class="message-reply-svg">
|
||||||
<svg
|
<svg
|
||||||
width="1.5em"
|
width="1.5em" height="1.5em"
|
||||||
height="1.5em"
|
viewBox="0 0 151.14355 87.562065" version="1.1" id="svg1"
|
||||||
viewBox="0 0 151.14355 87.562065"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
style="overflow: visible;">
|
style="overflow: visible;">
|
||||||
<defs
|
<defs id="defs1" />
|
||||||
id="defs1" />
|
<g id="layer1"
|
||||||
<g
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(40,-35)">
|
transform="translate(40,-35)">
|
||||||
<g
|
<g id="g3"
|
||||||
id="g3"
|
transform="translate(-35,-20)">
|
||||||
transform="translate(-35,-20)">
|
|
||||||
<path
|
<path
|
||||||
style="stroke:var(--reply-text-color);stroke-width:8;stroke-opacity:1"
|
style="stroke:var(--reply-text-color);stroke-width:8;stroke-opacity:1"
|
||||||
d="m 120.02168,87.850978 100.76157,2.4e-5"
|
d="m 120.02168,87.850978 100.76157,2.4e-5"
|
||||||
|
@ -32,16 +26,17 @@
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<MessageReply v-if="props.replyMessage" :author="props.replyMessage.user.display_name || props.replyMessage.user.username" :text="props.replyMessage?.message"
|
<MessageReply v-if="props.replyMessage" :id="props.message.uuid"
|
||||||
:id="props.message.uuid" :reply-id="props.replyMessage.uuid" max-width="reply" />
|
:author="props.replyMessage.user.display_name || props.replyMessage.user.username"
|
||||||
|
:text="props.replyMessage?.message"
|
||||||
|
:reply-id="props.replyMessage.uuid" max-width="reply" />
|
||||||
<div class="left-column">
|
<div class="left-column">
|
||||||
<img v-if="props.img" class="message-author-avatar" :src="props.img" :alt="author?.display_name || author?.username" />
|
<Avatar :user="props.author" class="message-author-avatar"/>
|
||||||
<Icon v-else name="lucide:user" class="message-author-avatar" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="message-data">
|
<div class="message-data">
|
||||||
<div class="message-metadata">
|
<div class="message-metadata">
|
||||||
<span class="message-author-username" tabindex="0">
|
<span class="message-author-username" tabindex="0" :style="`color: ${props.authorColor}`">
|
||||||
{{ author?.display_name || author?.username }}
|
{{ displayName }}
|
||||||
</span>
|
</span>
|
||||||
<span class="message-date" :title="date.toString()">
|
<span class="message-date" :title="date.toString()">
|
||||||
<span v-if="getDayDifference(date, currentDate) === 1">Yesterday at</span>
|
<span v-if="getDayDifference(date, currentDate) === 1">Yesterday at</span>
|
||||||
|
@ -75,6 +70,7 @@ import { parse } from 'marked';
|
||||||
import type { MessageProps } from '~/types/props';
|
import type { MessageProps } from '~/types/props';
|
||||||
import MessageMedia from './MessageMedia.vue';
|
import MessageMedia from './MessageMedia.vue';
|
||||||
import MessageReply from './UserInterface/MessageReply.vue';
|
import MessageReply from './UserInterface/MessageReply.vue';
|
||||||
|
import generateIrcColor from '~/utils/generateIrcColor';
|
||||||
|
|
||||||
const props = defineProps<MessageProps>();
|
const props = defineProps<MessageProps>();
|
||||||
|
|
||||||
|
@ -84,6 +80,7 @@ const dateHidden = ref<boolean>(true);
|
||||||
|
|
||||||
const date = new Date(props.timestamp);
|
const date = new Date(props.timestamp);
|
||||||
const currentDate: Date = new Date()
|
const currentDate: Date = new Date()
|
||||||
|
const displayName = props.author?.display_name || props.author?.username
|
||||||
|
|
||||||
console.log("[MSG] message to render:", props.message);
|
console.log("[MSG] message to render:", props.message);
|
||||||
console.log("author:", props.author);
|
console.log("author:", props.author);
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
:margin-bottom="(messages[i + 1] && messagesType[messages[i + 1].uuid] == 'normal') ?? false"
|
: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"
|
:last="i == messages.length - 1" :message-id="message.uuid" :author="message.user" :me="me"
|
||||||
:message="message" :is-reply="message.reply_to"
|
:message="message" :is-reply="message.reply_to"
|
||||||
|
:author-color="`${generateIrcColor(message.user.uuid)}`"
|
||||||
:reply-message="message.reply_to ? getReplyMessage(message.reply_to) : undefined" />
|
:reply-message="message.reply_to ? getReplyMessage(message.reply_to) : undefined" />
|
||||||
</div>
|
</div>
|
||||||
<div id="message-box" class="rounded-corners">
|
<div id="message-box" class="rounded-corners">
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { MessageResponse, ScrollPosition, UserResponse } from '~/types/interfaces';
|
import type { MessageResponse, ScrollPosition, UserResponse } from '~/types/interfaces';
|
||||||
import scrollToBottom from '~/utils/scrollToBottom';
|
import scrollToBottom from '~/utils/scrollToBottom';
|
||||||
|
import { generateIrcColor } from '#imports';
|
||||||
|
|
||||||
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>();
|
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>();
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink class="user-item" :href="`/me/${user.uuid}`" tabindex="0">
|
<NuxtLink class="user-item" :href="`/me/${user.uuid}`" tabindex="0">
|
||||||
<img v-if="props.user.avatar" class="user-avatar" :src="props.user.avatar" :alt="props.user.display_name ?? props.user.username" />
|
<Avatar :user="props.user" class="user-avatar"/>
|
||||||
<Icon v-else class="user-avatar" name="lucide:user" />
|
|
||||||
<span class="user-display-name">{{ props.user.display_name || props.user.username }}</span>
|
<span class="user-display-name">{{ displayName }}</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ import type { UserResponse } from '~/types/interfaces';
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: UserResponse
|
user: UserResponse
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const displayName = props.user.display_name || props.user.username
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="profile-popup">
|
<div id="profile-popup">
|
||||||
<img v-if="props.user.avatar" id="avatar" :src="props.user.avatar" alt="profile avatar">
|
<Avatar :user="props.user" id="avatar"/>
|
||||||
<Icon v-else id="avatar" name="lucide:user" />
|
|
||||||
|
|
||||||
<div id="cover-color"></div>
|
<div id="cover-color"></div>
|
||||||
<div id="main-body">
|
<div id="main-body">
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<div class="left-column-segment" id="left-column-middle">
|
<div class="left-column-segment" id="left-column-middle">
|
||||||
<NuxtLink v-for="guild of guilds" :href="`/servers/${guild.uuid}`">
|
<NuxtLink v-for="guild of guilds" :href="`/servers/${guild.uuid}`">
|
||||||
<NuxtImg v-if="guild.icon" class="sidebar-icon" :src="guild.icon" :alt="guild.name"/>
|
<NuxtImg v-if="guild.icon" class="sidebar-icon" :src="guild.icon" :alt="guild.name"/>
|
||||||
<Icon v-else name="lucide:server" class="sidebar-icon white" :alt="guild.name" />
|
<NuxtImg v-else class="sidebar-icon" :src="generateDefaultIcon(guild.name, guild.uuid)" :alt="guild.name"/>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<VerticalSpacer />
|
<VerticalSpacer />
|
||||||
|
|
|
@ -22,7 +22,8 @@
|
||||||
"pinia-plugin-persistedstate": "^4.2.0",
|
"pinia-plugin-persistedstate": "^4.2.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1",
|
||||||
|
"xxhash-wasm": "^1.1.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.11.0",
|
"packageManager": "pnpm@10.11.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ChannelEntry from "~/components/Guild/ChannelEntry.vue";
|
import ChannelEntry from "~/components/Guild/ChannelEntry.vue";
|
||||||
import GuildOptionsMenu from "~/components/Guild/GuildOptionsMenu.vue";
|
import GuildOptionsMenu from "~/components/Guild/GuildOptionsMenu.vue";
|
||||||
import MemberEntry from "~/components/Member/MemberEntry.vue";
|
import MemberEntry from "~/components/Guild/MemberEntry.vue";
|
||||||
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse } from "~/types/interfaces";
|
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse } from "~/types/interfaces";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
@ -47,6 +47,9 @@ importers:
|
||||||
vue-router:
|
vue-router:
|
||||||
specifier: ^4.5.1
|
specifier: ^4.5.1
|
||||||
version: 4.5.1(vue@3.5.13(typescript@5.8.3))
|
version: 4.5.1(vue@3.5.13(typescript@5.8.3))
|
||||||
|
xxhash-wasm:
|
||||||
|
specifier: ^1.1.0
|
||||||
|
version: 1.1.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@iconify-json/lucide':
|
'@iconify-json/lucide':
|
||||||
specifier: ^1.2.44
|
specifier: ^1.2.44
|
||||||
|
@ -4744,6 +4747,9 @@ packages:
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
xxhash-wasm@1.1.0:
|
||||||
|
resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
|
||||||
|
|
||||||
y18n@5.0.8:
|
y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -10146,6 +10152,8 @@ snapshots:
|
||||||
cssfilter: 0.0.10
|
cssfilter: 0.0.10
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
xxhash-wasm@1.1.0: {}
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
|
@ -3,12 +3,13 @@ import type { MessageResponse, UserResponse } from "./interfaces";
|
||||||
export interface MessageProps {
|
export interface MessageProps {
|
||||||
class?: string,
|
class?: string,
|
||||||
img?: string | null,
|
img?: string | null,
|
||||||
author?: UserResponse
|
author: UserResponse
|
||||||
text: string,
|
text: string,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
format: "12" | "24",
|
format: "12" | "24",
|
||||||
type: "normal" | "grouped",
|
type: "normal" | "grouped",
|
||||||
marginBottom: boolean,
|
marginBottom: boolean,
|
||||||
|
authorColor: string,
|
||||||
last: boolean,
|
last: boolean,
|
||||||
messageId: string,
|
messageId: string,
|
||||||
replyingTo?: boolean,
|
replyingTo?: boolean,
|
||||||
|
|
38
utils/generateDefaultIcon.ts
Normal file
38
utils/generateDefaultIcon.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
export default (name: string, seed: string): string => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (canvas && ctx) {
|
||||||
|
canvas.width = 256;
|
||||||
|
canvas.height = 256;
|
||||||
|
|
||||||
|
// get the first char from every word in the guild name
|
||||||
|
let previewName = "";
|
||||||
|
if (name.length > 3) {
|
||||||
|
let guildName: string[] = name.split(' ')
|
||||||
|
for (let i = 0; i < 3; i ++) {
|
||||||
|
if (guildName.length > i) {
|
||||||
|
previewName += guildName[i].charAt(0)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
previewName = name
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill background using seeded colour
|
||||||
|
ctx.fillStyle = generateIrcColor(seed, 50)
|
||||||
|
ctx.fillRect(0, 0, 256, 256)
|
||||||
|
|
||||||
|
ctx.fillStyle = 'white'
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.font = 'bold 96px Arial, Helvetica, sans-serif'
|
||||||
|
// 136 isn't actually centered, but it *looks* centered
|
||||||
|
ctx.fillText(previewName, 128, 136)
|
||||||
|
|
||||||
|
return canvas.toDataURL("image/png");
|
||||||
|
}
|
||||||
|
|
||||||
|
return "https://tenor.com/view/dame-da-ne-guy-kiryukazuma-kiryu-yakuza-yakuza-0-gif-14355451116903905918"
|
||||||
|
}
|
13
utils/generateIrcColor.ts
Normal file
13
utils/generateIrcColor.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import xxhash from "xxhash-wasm"
|
||||||
|
|
||||||
|
let h64: CallableFunction;
|
||||||
|
(async () => {
|
||||||
|
h64 = (await xxhash()).h64;
|
||||||
|
})();
|
||||||
|
|
||||||
|
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(${hashValue % 360n}, ${saturation}%, ${lightness}%)`
|
||||||
|
}
|
50
utils/isCanvasBlocked.ts
Normal file
50
utils/isCanvasBlocked.ts
Normal file
|
@ -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;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue