Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/dispatch/static/dispatch/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ declare module '@vue/runtime-core' {
AppDrawer: typeof import('./src/components/AppDrawer.vue')['default']
AppToolbar: typeof import('./src/components/AppToolbar.vue')['default']
AutoComplete: typeof import('./src/components/AutoComplete.vue')['default']
Avatar: typeof import('./src/components/Avatar.vue')['default']
BaseCombobox: typeof import('./src/components/BaseCombobox.vue')['default']
BasicLayout: typeof import('./src/components/layouts/BasicLayout.vue')['default']
ColorPickerInput: typeof import('./src/components/ColorPickerInput.vue')['default']
Expand Down
77 changes: 77 additions & 0 deletions src/dispatch/static/dispatch/src/atomics/CurrentUserAvatar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<script setup>
import { computed } from "vue"
import { useStore } from "vuex"
import UserAvatar from "@/atomics/UserAvatar.vue"

const store = useStore()
const props = defineProps({
size: {
type: [Number, String],
default: 30,
},
showTooltip: {
type: Boolean,
default: false,
},
tooltipText: {
type: String,
default: "",
},
border: {
type: Boolean,
default: false,
},
borderColor: {
type: String,
default: "white",
},
})

// Get current user from store
const currentUser = computed(() => {
try {
return store.state.auth.currentUser || {}
} catch (e) {
console.error("Error getting current user:", e)
return {}
}
})

// Get user name
const userName = computed(() => {
return currentUser.value.name || currentUser.value.email || ""
})

// Get user email
const userEmail = computed(() => {
return currentUser.value.email || ""
})

// Get avatar URL using the userAvatarUrl getter
const userAvatarUrl = computed(() => {
try {
if (store.getters["auth/userAvatarUrl"]) {
return store.getters["auth/userAvatarUrl"](currentUser.value)
}
} catch (e) {
console.error("Error getting avatar URL from store:", e)
}
return ""
})

// Get tooltip text
const displayTooltipText = computed(() => props.tooltipText || userName.value)
</script>

<template>
<UserAvatar
:name="userName"
:email="userEmail"
:imageUrl="userAvatarUrl"
:size="size"
:showTooltip="showTooltip"
:tooltipText="displayTooltipText"
:border="border"
:borderColor="borderColor"
/>
</template>
156 changes: 156 additions & 0 deletions src/dispatch/static/dispatch/src/atomics/UserAvatar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<script setup>
import { defineProps, computed } from "vue"
import DTooltip from "@/components/DTooltip.vue"

const props = defineProps({
name: {
type: String,
required: true,
},
email: {
type: String,
default: "",
},
imageUrl: {
type: String,
default: "",
},
size: {
type: [Number, String],
default: 24,
},
showTooltip: {
type: Boolean,
default: false,
},
tooltipText: {
type: String,
default: "",
},
border: {
type: Boolean,
default: false,
},
borderColor: {
type: String,
default: "white",
},
// New prop to force gradient in specific component instances
forceGradient: {
type: Boolean,
default: false,
},
})

// Check if we should force gradient display from environment variable
const forceGradientFromEnv = import.meta.env.VITE_DISPATCH_FORCE_AVATAR_GRADIENT === "true"

// Generate a color gradient based on the user's name
const getAvatarGradient = (name) => {
let hash = 5381
for (let i = 0; i < name.length; i++) {
hash = ((hash << 5) + hash) ^ name.charCodeAt(i) // Using XOR operator for better distribution
}

const hue = Math.abs(hash) % 360 // Ensure hue is a positive number
const fromColor = `hsl(${hue}, 95%, 50%)`
const toColor = `hsl(${(hue + 120) % 360}, 95%, 50%)` // Getting triadic color by adding 120 to hue

return `linear-gradient(${fromColor}, ${toColor})`
}

// Get avatar URL from email if provided
const getAvatarUrlFromEmail = (email) => {
if (!email) return ""

const userId = email.split("@")[0]
if (userId) {
const avatarTemplate = import.meta.env.VITE_DISPATCH_AVATAR_TEMPLATE || "/avatar/*/128x128.jpg"
// Use a regular expression with the global flag to replace all occurrences of "*"
const stem = avatarTemplate.replace(/\*/g, userId)

// Use the base URL from environment variable if available, otherwise use window.location
const envBaseUrl = import.meta.env.VITE_DISPATCH_AVATAR_BASE_URL
const defaultBaseUrl = `${window.location.protocol}//${window.location.host}`
const baseUrl = envBaseUrl || defaultBaseUrl
return `${baseUrl}${stem}`
}

return ""
}

// Determine if we should show the image or the gradient
const effectiveImageUrl = computed(() => {
// Use provided imageUrl if available
if (props.imageUrl) return props.imageUrl

// Otherwise try to generate one from email
return getAvatarUrlFromEmail(props.email)
})

// Check if we should show the image or the gradient
const hasValidImage = computed(() => {
// If force gradient is enabled (either via prop or env var), always show gradient
if (props.forceGradient || forceGradientFromEnv) {
return false
}

// Otherwise, check if we have a URL
return Boolean(effectiveImageUrl.value)
})

// Get the tooltip text (use provided text or fall back to name)
const displayTooltipText = computed(() => props.tooltipText || props.name)

// Compute avatar style including border if enabled
const avatarStyle = computed(() => {
if (props.border) {
return {
border: `2px solid ${props.borderColor}`,
}
}
return {}
})

// Compute background style for gradient avatar
const gradientStyle = computed(() => {
return {
background: getAvatarGradient(props.name),
}
})
</script>

<template>
<div class="user-avatar-wrapper">
<DTooltip v-if="showTooltip" :text="displayTooltipText">
<template #activator="{ tooltip }">
<v-avatar v-bind="tooltip" :size="size" class="user-avatar" :style="avatarStyle">
<v-img v-if="hasValidImage" :src="effectiveImageUrl" />
<div v-else class="gradient-avatar" :style="gradientStyle" />
</v-avatar>
</template>
</DTooltip>
<v-avatar v-else :size="size" class="user-avatar" :style="avatarStyle">
<v-img v-if="hasValidImage" :src="effectiveImageUrl" />
<div v-else class="gradient-avatar" :style="gradientStyle" />
</v-avatar>
</div>
</template>

<style scoped>
.user-avatar-wrapper {
display: inline-flex;
}

.gradient-avatar {
width: 100%;
height: 100%;
border-radius: 50%;
}

.user-avatar {
display: flex;
align-items: center;
justify-content: center;
}
</style>
6 changes: 5 additions & 1 deletion src/dispatch/static/dispatch/src/case/PageHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@

<DTooltip text="View case participants" :hotkeys="['⌘', '⇧', 'P']">
<template #activator="{ tooltip }">
<ParticipantAvatarGroup :participants="caseParticipants" class="pl-3" v-bind="tooltip" />
<ParticipantAvatarGroup
:participants="caseParticipants"
class="pl-3 d-flex align-center"
v-bind="tooltip"
/>
</template>
</DTooltip>
<DTooltip
Expand Down
16 changes: 4 additions & 12 deletions src/dispatch/static/dispatch/src/components/AppToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,12 @@
</v-menu>
</v-btn>
<v-btn icon size="large" variant="text">
<v-avatar size="30px" v-if="userAvatarUrl(currentUser())">
<v-img :src="userAvatarUrl(currentUser())" />
</v-avatar>
<v-avatar size="30px" v-else>
<v-icon>mdi-account-circle</v-icon>
</v-avatar>
<CurrentUserAvatar :size="30" />
<v-menu activator="parent" width="400">
<v-list class="pb-0">
<v-list-item class="px-2">
<template #prepend>
<v-avatar v-if="userAvatarUrl(currentUser())">
<v-img :src="userAvatarUrl(currentUser())" />
</v-avatar>
<v-avatar v-else>
<v-icon size="30px">mdi-account-circle</v-icon>
</v-avatar>
<CurrentUserAvatar :size="30" />
</template>

<v-list-item-title class="text-h6">
Expand Down Expand Up @@ -148,6 +138,7 @@ import { formatHash } from "@/filters"
import OrganizationApi from "@/organization/api"
import OrganizationCreateEditDialog from "@/organization/CreateEditDialog.vue"
import UserApi from "@/auth/api"
import CurrentUserAvatar from "@/atomics/CurrentUserAvatar.vue"

export default {
name: "AppToolbar",
Expand All @@ -161,6 +152,7 @@ export default {
},
components: {
OrganizationCreateEditDialog,
CurrentUserAvatar,
},
computed: {
queryString: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup>
import { ref, computed, watch } from "vue"
import { useHotKey } from "@/composables/useHotkey"
import UserAvatar from "@/atomics/UserAvatar.vue"

const props = defineProps({
participants: {
Expand Down Expand Up @@ -67,19 +68,6 @@ const filteredParticipants = computed(() => {
return orderedParticipants.value
})

const getAvatarGradient = (participant) => {
let hash = 5381
for (let i = 0; i < participant.length; i++) {
hash = ((hash << 5) + hash) ^ participant.charCodeAt(i) // Using XOR operator for better distribution
}

const hue = Math.abs(hash) % 360 // Ensure hue is a positive number
const fromColor = `hsl(${hue}, 95%, 50%)`
const toColor = `hsl(${(hue + 120) % 360}, 95%, 50%)` // Getting triadic color by adding 120 to hue

return `linear-gradient(${fromColor}, ${toColor})`
}

const toggleMenu = () => {
menu.value = !menu.value
if (!menu.value) {
Expand All @@ -98,7 +86,7 @@ const toggleMenu = () => {
transition="false"
>
<template #activator="{ props: menuProps }">
<v-btn variant="text" v-bind="menuProps">
<v-btn variant="text" v-bind="menuProps" class="d-flex align-center justify-center pa-0">
<!-- Display Visible Participants -->
<div class="avatar-row">
<!-- Display +n Avatar -->
Expand All @@ -112,10 +100,11 @@ const toggleMenu = () => {
:key="index"
class="avatar-container"
>
<v-avatar
size="20px"
:style="{ background: getAvatarGradient(participant.individual.name) }"
v-on="on"
<UserAvatar
:name="participant.individual.name"
:email="participant.individual.email"
:size="20"
:border="false"
/>
</div>
</div>
Expand All @@ -134,10 +123,12 @@ const toggleMenu = () => {
active-class="ma-4"
>
<template #prepend>
<v-avatar
class="mr-n2"
size="12px"
:style="{ background: getAvatarGradient(participant.individual.name) }"
<UserAvatar
:name="participant.individual.name"
:email="participant.individual.email"
:size="14"
:border="false"
class="mr-2"
/>
<!-- <v-icon class="mr-n6 ml-n2" size="x-small" icon="mdi-account"></v-icon> -->
</template>
Expand All @@ -157,7 +148,7 @@ const toggleMenu = () => {
.avatar-row {
display: flex;
align-items: center;
justify-content: start;
justify-content: center;
flex-direction: row-reverse;
position: relative;
}
Expand All @@ -167,6 +158,10 @@ const toggleMenu = () => {
border-radius: 50%; /* Make the border circular */
position: relative;
margin-right: -5px; /* Adjust this value to change the overlapping amount */
display: flex;
align-items: center;
justify-content: center;
overflow: hidden; /* Ensure content doesn't overflow the circular container */
}

.extra-avatar {
Expand All @@ -176,6 +171,8 @@ const toggleMenu = () => {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}

.hotkey {
Expand Down
Loading
Loading