Implement profile modal #52

Merged
twig merged 27 commits from profile-modal into main 2025-08-04 19:06:45 +00:00
9 changed files with 76 additions and 121 deletions
Showing only changes of commit 24038f7767 - Show all commits

View file

@ -3,8 +3,10 @@
class="display-avatar" class="display-avatar"
:src="displayAvatar" :src="displayAvatar"
:alt="displayName" /> :alt="displayName" />
<Icon v-else <DefaultIcon v-else
name="lucide:user" class="display-avatar"
:name="displayName"
:seed="user.uuid"
:alt="displayName" /> :alt="displayName" />
</template> </template>
@ -19,26 +21,17 @@ const props = defineProps<{
}>(); }>();
const displayName = getDisplayName(props.profile) const displayName = getDisplayName(props.profile)
let user: UserResponse
let displayAvatar: string | null let displayAvatar: string | null
twig marked this conversation as resolved Outdated

Typo

Typo

You wrote “UserRespone” instead of “UserResponse” in the comment.

You wrote “UserRespone” instead of “UserResponse” in the comment.
if ("username" in props.profile) { if ("username" in props.profile) {
// assume it's a UserResponse // assume it's a UserResponse
displayAvatar = props.profile.avatar displayAvatar = props.profile.avatar
if (!displayAvatar) { user = props.profile
if (!isCanvasBlocked()) {
displayAvatar = generateDefaultIcon(displayName, props.profile.uuid)
}
}
} else { } else {
// assume it's a GuildMemberResponse // assume it's a GuildMemberResponse
displayAvatar = props.profile.user.avatar displayAvatar = props.profile.user.avatar
if (!displayAvatar) { user = props.profile.user
if (!isCanvasBlocked()) {
displayAvatar = generateDefaultIcon(displayName, props.profile.user_uuid)
}
}
} }
</script> </script>

View file

@ -0,0 +1,48 @@
<template>
<div :style="`background-color: ${generateIrcColor(seed, 50)}`"
class="default-icon">
<span class="default-icon-text">
{{ previewName }}
</span>
</div>
</template>
<script lang="ts" setup>
const props = defineProps<{
seed: string,
name: string
}>();
let previewName = "";
if (props.name.length > 3) {
let guildName: string[] = props.name.split(' ')
for (let i = 0; i < 3; i ++) {
if (guildName.length > i) {
previewName += guildName[i].charAt(0)
} else {
break
}
}
} else {
previewName = props.name
}
</script>
<style scoped>
.default-icon {
display: flex;
align-items: center;
justify-content: center;
}
.default-icon-text {
/* helps centre the icon, yes, this is NOT perfect */
margin-top: -0.15em;
font-weight: bold;
color: var(--secondary-text-color)
}
</style>

View file

@ -32,4 +32,11 @@ function hideModalPopup() {
.member-item { .member-item {
position: relative; position: relative;
} }
.member-avatar {
min-height: 2.3em;
max-height: 2.3em;
min-width: 2.3em;
max-width: 2.3em;
}
</style> </style>

View file

@ -228,7 +228,10 @@ function getDayDifference(date1: Date, date2: Date) {
} }
.message-author-avatar { .message-author-avatar {
width: 100%; min-height: 2em;
max-height: 2em;
min-width: 2em;
max-width: 2em;
} }
.left-column { .left-column {

View file

@ -38,7 +38,7 @@ const props = defineProps<{
.user-avatar { .user-avatar {
min-width: 2.3em; min-width: 2.3em;
max-width: 2.3em; max-width: 2.3em;
min-width: 2.3em; min-height: 2.3em;
max-height: 2.3em; max-height: 2.3em;
} }

View file

@ -17,19 +17,15 @@
</div> </div>
<VerticalSpacer /> <VerticalSpacer />
<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}`" id="guild-icon-container">
<NuxtImg v-if="guild.icon" <NuxtImg v-if="guild.icon"
class="sidebar-icon guild-icon" class="sidebar-icon guild-icon"
:alt="guild.name" :alt="guild.name"
:src="guild.icon" /> :src="guild.icon" />
<NuxtImg v-else-if="!blockedCanvas" <DefaultIcon v-else
class="sidebar-icon guild-icon" class="sidebar-icon guild-icon"
:alt="guild.name" :alt="guild.name"
:src="generateDefaultIcon(guild.name, guild.uuid)" /> :name="guild.name" :seed="guild.uuid"/>
<Icon v-else name="lucide:server"
:style="`color: ${generateIrcColor(guild.uuid, 50)}`"
class="sidebar-icon guild-icon"
:alt="guild.name" />
</NuxtLink> </NuxtLink>
</div> </div>
<VerticalSpacer /> <VerticalSpacer />
@ -52,6 +48,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ModalBase } from '#components'; import { ModalBase } from '#components';
import { render } from 'vue'; import { render } from 'vue';
import DefaultIcon from '~/components/DefaultIcon.vue';
import GuildDropdown from '~/components/Guild/GuildDropdown.vue'; import GuildDropdown from '~/components/Guild/GuildDropdown.vue';
import Loading from '~/components/Popups/Loading.vue'; import Loading from '~/components/Popups/Loading.vue';
import Button from '~/components/UserInterface/Button.vue'; import Button from '~/components/UserInterface/Button.vue';
@ -69,8 +66,6 @@ const createButtonContainer = ref<HTMLButtonElement>();
const { getDisplayName } = useProfile() const { getDisplayName } = useProfile()
const api = useApi(); const api = useApi();
const blockedCanvas = isCanvasBlocked()
const options = [ const options = [
{ name: "Join", value: "join", callback: async () => { { name: "Join", value: "join", callback: async () => {
console.log("join guild!"); console.log("join guild!");
@ -255,6 +250,10 @@ function createDropdown() {
height: var(--sidebar-icon-width); height: var(--sidebar-icon-width);
} }
#guild-icon-container {
text-decoration: none;
}
.guild-icon { .guild-icon {
border-radius: var(--guild-icon-radius); border-radius: var(--guild-icon-radius);
} }

View file

@ -133,13 +133,6 @@ function handleMemberClick(member: GuildMemberResponse) {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.member-avatar {
min-width: 2.3em;
max-width: 2.3em;
min-width: 2.3em;
max-height: 2.3em;
}
.member-display-name { .member-display-name {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View file

@ -1,38 +0,0 @@
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"
}

View file

@ -1,50 +0,0 @@
//
// 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;
}