Skip to content

perf(utils): ensure only 64px or 512px avatars are loaded #6749

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
49 changes: 33 additions & 16 deletions src/components/NcAvatar/NcAvatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,24 @@ export default {
</template>

<script>
import { getCurrentUser } from '@nextcloud/auth'
import { getBuilder } from '@nextcloud/browser-storage'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { generateUrl } from '@nextcloud/router'
import { vOnClickOutside as ClickOutside } from '@vueuse/components'
import { useTemplateRef } from 'vue'

import { getRoute } from '../../components/NcRichText/autolink.ts'
import { useIsDarkThemeElement } from '../../composables/index.ts'
import { userStatus } from '../../mixins/index.js'
import { getAvatarUrl } from '../../utils/getAvatarUrl.ts'
import { getUserStatusText } from '../../utils/UserStatus.ts'
import { t } from '../../l10n.js'

import axios from '@nextcloud/axios'
import usernameToColor from '../../functions/usernameToColor/index.js'

import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
import NcActions from '../NcActions/index.js'
import NcActionLink from '../NcActionLink/index.js'
import NcActionRouter from '../NcActionRouter/index.js'
Expand All @@ -242,21 +260,6 @@ import NcButton from '../NcButton/index.ts'
import NcIconSvgWrapper from '../NcIconSvgWrapper/index.js'
import NcLoadingIcon from '../NcLoadingIcon/index.js'
import NcUserStatusIcon from '../NcUserStatusIcon/index.js'
import usernameToColor from '../../functions/usernameToColor/index.js'
import { getAvatarUrl } from '../../utils/getAvatarUrl.ts'
import { getUserStatusText } from '../../utils/UserStatus.ts'
import { userStatus } from '../../mixins/index.js'
import { t } from '../../l10n.js'
import { getRoute } from '../../components/NcRichText/autolink.ts'

import axios from '@nextcloud/axios'
import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'

import { getCurrentUser } from '@nextcloud/auth'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { getBuilder } from '@nextcloud/browser-storage'
import { generateUrl } from '@nextcloud/router'
import { vOnClickOutside as ClickOutside } from '@vueuse/components'

const browserStorage = getBuilder('nextcloud').persist().build()

Expand Down Expand Up @@ -417,6 +420,16 @@ export default {
default: 'body',
},
},

setup() {
const root = useTemplateRef('main')
const isDarkTheme = useIsDarkThemeElement(root)
Comment on lines +425 to +426
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need to check specifically main and not body and thus use shared composable?

If so, I would at least go with props.menuContainer, so it inherits it from container

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes otherwise it does not work if you use the avatar within a forced theme.
This can not be influenced by the app developer so we need to do this here.

(how does talk handle this currently? IIRC you use force dark theme during calls?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need to check specifically main and not body and thus use shared composable?

To avoid misunderstanding, main here is template ref key, not <main> query selector

Copy link
Contributor

@Antreesy Antreesy Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does talk handle this currently?

<div class="top-bar__wrapper" :data-theme-dark="isInCall"> 🙈

But we do not rewrite avatars in call, if I'm not mistaken, only rely on useIsDarkTheme() composable from the lib

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We remount the conversation avatar in the top bar


return {
isDarkTheme,
}
},

data() {
return {
avatarUrlLoaded: null,
Expand Down Expand Up @@ -717,7 +730,11 @@ export default {
* @return {string}
*/
avatarUrlGenerator(user, size) {
let avatarUrl = getAvatarUrl(user, size, this.isGuest)
let avatarUrl = getAvatarUrl(user, {
size,
isDarkTheme: this.isDarkTheme,
isGuest: this.isGuest,
})

// eslint-disable-next-line camelcase
if (user === getCurrentUser()?.uid && typeof oc_userconfig !== 'undefined') {
Expand Down
19 changes: 13 additions & 6 deletions src/components/NcRichContenteditable/NcAutoCompleteResult.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
-->

<template>
<div class="autocomplete-result">
<div ref="root" class="autocomplete-result">
<!-- Avatar or icon -->
<div :class="[icon, `autocomplete-result__icon--${avatarUrl ? 'with-avatar' : ''}`]"
:style="avatarUrl ? { backgroundImage: `url(${avatarUrl})` } : null "
Expand All @@ -31,6 +31,8 @@
</template>

<script>
import { useTemplateRef } from 'vue'
import { useIsDarkThemeElement } from '../../composables/useIsDarkTheme/index.ts'
import { getAvatarUrl } from '../../utils/getAvatarUrl.ts'

import NcUserStatusIcon from '../NcUserStatusIcon/index.js'
Expand Down Expand Up @@ -81,25 +83,30 @@ export default {
default: () => ({}),
},
},

setup() {
const root = useTemplateRef('root')
const isDarkTheme = useIsDarkThemeElement(root)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 99% of cases we need only the entire document dark theme. And in rare cases we need to check the avatar itself (for example, in the top bar in Talk during a call, because the call view is always aka dark).

What about using useIsDarkTheme by default and useIsDarkThemeElement only if specified explicitly?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for other components

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of [data-theme-dark] for override theme we provide <NcThemeProvider /> or useDarkTheme that provides the overridden dark theme?

In this case we don't need a mutation observer and can rely on JS only.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

provide inject in the composable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or in the component provider, both are fine for me.

<!-- before -->
<div data-theme-dark>
  ...content...
</div>

<!-- after -->
<NcThemeProvider dark>
  ...content...
</NcThemeProvider>

Only for the cases when we override the theme.

I worry about it because overriding the theme is quite a rare case, but to support it this PR adds observer for every avatar instance, and we may have a lot.

return {
isDarkTheme,
}
},

computed: {
avatarUrl() {
if (this.iconUrl) {
return this.iconUrl
}

return this.id && this.source === 'users'
? this.getAvatarUrl(this.id, 44)
? getAvatarUrl(this.id, { isDarkTheme: this.isDarkTheme })
: null
},
// For backwards compatibility
labelWithFallback() {
return this.label || this.title
},
},

methods: {
getAvatarUrl,
},
}
</script>

Expand Down
22 changes: 15 additions & 7 deletions src/components/NcRichContenteditable/NcMentionBubble.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
-->

<template>
<span :class="{'mention-bubble--primary': primary}"
<span ref="root"
:class="{'mention-bubble--primary': primary}"
class="mention-bubble"
contenteditable="false">
<span class="mention-bubble__wrapper">
Expand All @@ -25,7 +26,8 @@
</template>

<script>
import { getAvatarUrl } from '../../utils/getAvatarUrl.ts'
import { useTemplateRef } from 'vue'
import { useIsDarkThemeElement } from '../../composables/index.ts'

export default {
name: 'NcMentionBubble',
Expand Down Expand Up @@ -65,14 +67,24 @@ export default {
default: false,
},
},

setup() {
const root = useTemplateRef('root')
const isDarkTheme = useIsDarkThemeElement(root)

return {
isDarkTheme,
}
},

computed: {
avatarUrl() {
if (this.iconUrl) {
return this.iconUrl
}

return this.id && this.source === 'users'
? this.getAvatarUrl(this.id, 44)
? this.getAvatarUrl(this.id, { isDarkTheme: this.isDarkTheme })
: null
},
mentionText() {
Expand All @@ -85,10 +97,6 @@ export default {
return this.label || this.title
},
},

methods: {
getAvatarUrl,
},
}
</script>

Expand Down
15 changes: 9 additions & 6 deletions src/composables/useIsDarkTheme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { DeepReadonly, Ref } from 'vue'
import { ref, readonly, watch } from 'vue'
import type { DeepReadonly, MaybeRef, Ref } from 'vue'
import { ref, readonly, watch, toValue, computed } from 'vue'

Check failure on line 7 in src/composables/useIsDarkTheme/index.ts

View workflow job for this annotation

GitHub Actions / NPM lint

'computed' is defined but never used
import { createSharedComposable, usePreferredDark, useMutationObserver } from '@vueuse/core'
import { checkIfDarkTheme } from '../../functions/isDarkTheme/index.ts'

Expand All @@ -15,19 +15,22 @@
* @param el - The element to check for the dark theme enabled on (default is `document.body`)
* @return {DeepReadonly<Ref<boolean>>} - computed boolean whether the dark theme is enabled
*/
export function useIsDarkThemeElement(el: HTMLElement = document.body): DeepReadonly<Ref<boolean>> {
const isDarkTheme = ref(checkIfDarkTheme(el))
export function useIsDarkThemeElement(el: MaybeRef<HTMLElement> = document.body): DeepReadonly<Ref<boolean>> {
const element = toRef(el)

Check failure on line 19 in src/composables/useIsDarkTheme/index.ts

View workflow job for this annotation

GitHub Actions / test

tests/unit/components/NcAvatar/NcAvatar.spec.ts > NcAvatar.vue > Fallback initials > should display initials for 'special characters in name' ("'Jane (Doe)'" -> "'JD'")

ReferenceError: toRef is not defined ❯ useIsDarkThemeElement src/composables/useIsDarkTheme/index.ts:19:18 ❯ setup src/components/NcAvatar/NcAvatar.vue:426:44 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7848:25 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7809:36 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5159:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5125:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4654:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5269:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:229:19

Check failure on line 19 in src/composables/useIsDarkTheme/index.ts

View workflow job for this annotation

GitHub Actions / test

tests/unit/components/NcAvatar/NcAvatar.spec.ts > NcAvatar.vue > Fallback initials > should display initials for 'display name property' ("'Jane Doe'" -> "'JD'")

ReferenceError: toRef is not defined ❯ useIsDarkThemeElement src/composables/useIsDarkTheme/index.ts:19:18 ❯ setup src/components/NcAvatar/NcAvatar.vue:426:44 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7848:25 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7809:36 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5159:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5125:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4654:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5269:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:229:19

Check failure on line 19 in src/composables/useIsDarkTheme/index.ts

View workflow job for this annotation

GitHub Actions / test

tests/unit/components/NcAvatar/NcAvatar.spec.ts > NcAvatar.vue > Fallback initials > should display initials for 'empty user' ("''" -> "'?'")

ReferenceError: toRef is not defined ❯ useIsDarkThemeElement src/composables/useIsDarkTheme/index.ts:19:18 ❯ setup src/components/NcAvatar/NcAvatar.vue:426:44 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7848:25 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7809:36 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5159:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5125:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4654:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5269:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:229:19

Check failure on line 19 in src/composables/useIsDarkTheme/index.ts

View workflow job for this annotation

GitHub Actions / test

tests/unit/components/NcAvatar/NcAvatar.spec.ts > NcAvatar.vue > should display initials for display name property over user id

ReferenceError: toRef is not defined ❯ useIsDarkThemeElement src/composables/useIsDarkTheme/index.ts:19:18 ❯ setup src/components/NcAvatar/NcAvatar.vue:426:44 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7848:25 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7809:36 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5159:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5125:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4654:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5269:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:229:19

Check failure on line 19 in src/composables/useIsDarkTheme/index.ts

View workflow job for this annotation

GitHub Actions / test

tests/unit/components/NcAvatar/NcAvatar.spec.ts > NcAvatar.vue > should display initials for user id

ReferenceError: toRef is not defined ❯ useIsDarkThemeElement src/composables/useIsDarkTheme/index.ts:19:18 ❯ setup src/components/NcAvatar/NcAvatar.vue:426:44 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7848:25 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7809:36 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5159:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5125:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4654:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5269:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:229:19

Check failure on line 19 in src/composables/useIsDarkTheme/index.ts

View workflow job for this annotation

GitHub Actions / test

tests/unit/components/NcAvatar/NcAvatar.spec.ts > NcAvatar.vue > aria label is does not include status if status not shown

ReferenceError: toRef is not defined ❯ useIsDarkThemeElement src/composables/useIsDarkTheme/index.ts:19:18 ❯ setup src/components/NcAvatar/NcAvatar.vue:426:44 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7848:25 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7809:36 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5159:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5125:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4654:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5269:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:229:19

Check failure on line 19 in src/composables/useIsDarkTheme/index.ts

View workflow job for this annotation

GitHub Actions / test

tests/unit/components/NcAvatar/NcAvatar.spec.ts > NcAvatar.vue > aria label is set to include status even if status is do-not-disturb

ReferenceError: toRef is not defined ❯ useIsDarkThemeElement src/composables/useIsDarkTheme/index.ts:19:18 ❯ setup src/components/NcAvatar/NcAvatar.vue:426:44 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7848:25 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7809:36 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5159:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5125:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4654:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5269:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:229:19

Check failure on line 19 in src/composables/useIsDarkTheme/index.ts

View workflow job for this annotation

GitHub Actions / test

tests/unit/components/NcAvatar/NcAvatar.spec.ts > NcAvatar.vue > aria label is set to include status if status is shown visually

ReferenceError: toRef is not defined ❯ useIsDarkThemeElement src/composables/useIsDarkTheme/index.ts:19:18 ❯ setup src/components/NcAvatar/NcAvatar.vue:426:44 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7848:25 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7809:36 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5159:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5125:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4654:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5269:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:229:19

Check failure on line 19 in src/composables/useIsDarkTheme/index.ts

View workflow job for this annotation

GitHub Actions / test

tests/unit/mixins/richEditor.spec.js > richEditor.js > renderContent > keeps adjacent mentions with user data

ReferenceError: toRef is not defined ❯ useIsDarkThemeElement src/composables/useIsDarkTheme/index.ts:19:18 ❯ setup src/components/NcRichContenteditable/NcMentionBubble.vue:73:44 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7848:25 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7809:36 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5159:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5125:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4654:11 ❯ render node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5922:7 ❯ mount node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:3922:13

Check failure on line 19 in src/composables/useIsDarkTheme/index.ts

View workflow job for this annotation

GitHub Actions / test

tests/unit/mixins/richEditor.spec.js > richEditor.js > renderContent > keeps mentions with user data

ReferenceError: toRef is not defined ❯ useIsDarkThemeElement src/composables/useIsDarkTheme/index.ts:19:18 ❯ setup src/components/NcRichContenteditable/NcMentionBubble.vue:73:44 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7848:25 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7809:36 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5159:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5125:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4654:11 ❯ render node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5922:7 ❯ mount node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:3922:13
const isDarkTheme = ref(checkIfDarkTheme(toValue(el)))
const isDarkSystemTheme = usePreferredDark()

/** Update the isDarkTheme */
function updateIsDarkTheme() {
isDarkTheme.value = checkIfDarkTheme(el)
isDarkTheme.value = checkIfDarkTheme(element.value)
}

// Watch for element change to handle data-theme* attributes change
useMutationObserver(el, updateIsDarkTheme, { attributes: true })
useMutationObserver(element, updateIsDarkTheme, { attributes: true })
// Watch for system theme change for the default theme
watch(isDarkSystemTheme, updateIsDarkTheme, { immediate: true })
// Watch for element changes
watch(element, updateIsDarkTheme)
Comment on lines +32 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a use case for this watch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Element updated?


return readonly(isDarkTheme)
}
Expand Down
43 changes: 39 additions & 4 deletions src/utils/getAvatarUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,47 @@
*/

import { generateUrl } from '@nextcloud/router'
import { checkIfDarkTheme } from '../functions/isDarkTheme/index.ts'

export const getAvatarUrl = (user: string, size: number | string, isGuest?: boolean): string => {
const darkTheme = window.getComputedStyle(document.body)
.getPropertyValue('--background-invert-if-dark') === 'invert(100%)'
interface AvatarUrlOptions {
/**
* Should the dark theme variant be used.
*/
isDarkTheme?: boolean

return generateUrl('/avatar' + (isGuest ? '/guest' : '') + '/{user}/{size}' + (darkTheme ? '/dark' : ''), {
/**
* Is the user a guest user.
*/
isGuest?: boolean

/**
* Size of the avatar.
* @default 64
*/
size?: 64 | 512
}

/**
* Get the avatar URL for a given user.
*
* @param user - The user id
* @param options - Adjustments for the avatar format
*/
export function getAvatarUrl(user: string, options?: AvatarUrlOptions): string {
// backend only supports 64 and 512px
// so we only requrest the needed size for better caching of the request.
const size = (options?.size || 64) <= 64
? 64
: 512

const guestUrl = options?.isGuest
? '/guest'
: ''
const themeUrl = options?.isDarkTheme ?? checkIfDarkTheme(document.body)
? '/dark'
: ''

return generateUrl(`/avatar${guestUrl}/{user}/{size}${themeUrl}`, {
Comment on lines +33 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be in nextcloud/router?

user,
size,
})
Expand Down
46 changes: 38 additions & 8 deletions tests/unit/utils/getAvatarUrl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,56 @@ describe('getAvatarUrl', () => {
})

it('should return correct relative URL for user avatar', () => {
expect(getAvatarUrl('john', 44)).toBe('//index.php/avatar/john/44')
expect(getAvatarUrl('alice', '64', false)).toBe('//index.php/avatar/alice/64')
expect(getAvatarUrl('alice')).toBe('//index.php/avatar/alice/64')
expect(getAvatarUrl('john', { size: 64 })).toBe('//index.php/avatar/john/64')
})

it('should return correct relative URL with fixed sizes', () => {
/// @ts-expect-error testing invalid value
expect(getAvatarUrl('alice', { size: 0 })).toBe('//index.php/avatar/alice/64')
/// @ts-expect-error testing invalid value
expect(getAvatarUrl('alice', { size: -1 })).toBe('//index.php/avatar/alice/64')
expect(getAvatarUrl('john', { size: 64 })).toBe('//index.php/avatar/john/64')
/// @ts-expect-error testing invalid value
expect(getAvatarUrl('john', { size: 65 })).toBe('//index.php/avatar/john/512')
expect(getAvatarUrl('john', { size: 512 })).toBe('//index.php/avatar/john/512')
})

it('should return correct relative URL for user avatar in dark mode', () => {
document.body.style.setProperty('--background-invert-if-dark', 'invert(100%)')

expect(getAvatarUrl('alice')).toBe('//index.php/avatar/alice/64/dark')
expect(getAvatarUrl('john', { size: 512 })).toBe('//index.php/avatar/john/512/dark')
})

it('should return correct relative URL for user avatar in dark mode if enforced', () => {
expect(getAvatarUrl('alice', { isDarkTheme: true })).toBe('//index.php/avatar/alice/64/dark')
expect(getAvatarUrl('john', { isDarkTheme: true, size: 512 })).toBe('//index.php/avatar/john/512/dark')
})

it('should return correct relative URL for user avatar in bright mode if enforced but body is darkmode', () => {
document.body.style.setProperty('--background-invert-if-dark', 'invert(100%)')

expect(getAvatarUrl('alice', { isDarkTheme: false })).toBe('//index.php/avatar/alice/64')
expect(getAvatarUrl('john', { isDarkTheme: false, size: 512 })).toBe('//index.php/avatar/john/512')
})

it('should return correct relative URL for user avatar in dark mode', () => {
document.body.style.setProperty('--background-invert-if-dark', 'invert(100%)')

expect(getAvatarUrl('john', 44)).toBe('//index.php/avatar/john/44/dark')
expect(getAvatarUrl('alice', '64', false)).toBe('//index.php/avatar/alice/64/dark')
expect(getAvatarUrl('alice')).toBe('//index.php/avatar/alice/64/dark')
expect(getAvatarUrl('john', { size: 64 })).toBe('//index.php/avatar/john/64/dark')
})

it('should return correct relative URL for guest avatar', () => {
expect(getAvatarUrl('john', 44, true)).toBe('//index.php/avatar/guest/john/44')
expect(getAvatarUrl('alice', '64', true)).toBe('//index.php/avatar/guest/alice/64')
expect(getAvatarUrl('alice', { isGuest: true })).toBe('//index.php/avatar/guest/alice/64')
expect(getAvatarUrl('john', { size: 64, isGuest: true })).toBe('//index.php/avatar/guest/john/64')
})

it('should return correct relative URL for guest avatar in dark mode', () => {
document.body.style.setProperty('--background-invert-if-dark', 'invert(100%)')

expect(getAvatarUrl('john', 44, true)).toBe('//index.php/avatar/guest/john/44/dark')
expect(getAvatarUrl('alice', '64', true)).toBe('//index.php/avatar/guest/alice/64/dark')
expect(getAvatarUrl('alice', { isGuest: true })).toBe('//index.php/avatar/guest/alice/64/dark')
expect(getAvatarUrl('john', { size: 64, isGuest: true })).toBe('//index.php/avatar/guest/john/64/dark')
})
})
Loading