web: use thumbnails for small avatars

This commit is contained in:
Tulir Asokan 2025-01-27 23:11:33 +02:00
parent 1b5467cf0e
commit 6d12e6e009
17 changed files with 53 additions and 34 deletions

View file

@ -78,7 +78,7 @@ function getFallbackCharacter(from: unknown, idx: number): string {
return Array.from(from.slice(0, (idx + 1) * 2))[idx]?.toUpperCase().toWellFormed() ?? "" return Array.from(from.slice(0, (idx + 1) * 2))[idx]?.toUpperCase().toWellFormed() ?? ""
} }
export const getAvatarURL = (userID: UserID, content?: UserProfile | null): string | undefined => { export const getAvatarURL = (userID: UserID, content?: UserProfile | null, thumbnail = false): string | undefined => {
const fallbackCharacter = getFallbackCharacter(content?.displayname, 0) || getFallbackCharacter(userID, 1) const fallbackCharacter = getFallbackCharacter(content?.displayname, 0) || getFallbackCharacter(userID, 1)
const backgroundColor = getUserColor(userID) const backgroundColor = getUserColor(userID)
const [server, mediaID] = parseMXC(content?.avatar_file?.url ?? content?.avatar_url) const [server, mediaID] = parseMXC(content?.avatar_file?.url ?? content?.avatar_url)
@ -87,7 +87,12 @@ export const getAvatarURL = (userID: UserID, content?: UserProfile | null): stri
} }
const encrypted = !!content?.avatar_file const encrypted = !!content?.avatar_file
const fallback = `${backgroundColor}:${fallbackCharacter}` const fallback = `${backgroundColor}:${fallbackCharacter}`
return `_gomuks/media/${server}/${mediaID}?encrypted=${encrypted}&fallback=${encodeURIComponent(fallback)}` const url = `_gomuks/media/${server}/${mediaID}?encrypted=${encrypted}&fallback=${encodeURIComponent(fallback)}`
return thumbnail ? `${url}&thumbnail=avatar` : url
}
export const getAvatarThumbnailURL = (userID: UserID, content?: UserProfile | null): string | undefined => {
return getAvatarURL(userID, content, true)
} }
interface RoomForAvatarURL { interface RoomForAvatarURL {
@ -98,9 +103,15 @@ interface RoomForAvatarURL {
avatar_url?: ContentURI avatar_url?: ContentURI
} }
export const getRoomAvatarURL = (room: RoomForAvatarURL, avatarOverride?: ContentURI): string | undefined => { export const getRoomAvatarURL = (
room: RoomForAvatarURL, avatarOverride?: ContentURI, thumbnail = false,
): string | undefined => {
return getAvatarURL(room.dm_user_id ?? room.room_id, { return getAvatarURL(room.dm_user_id ?? room.room_id, {
displayname: room.name, displayname: room.name,
avatar_url: avatarOverride ?? room.avatar ?? room.avatar_url, avatar_url: avatarOverride ?? room.avatar ?? room.avatar_url,
}) }, thumbnail)
}
export const getRoomAvatarThumbnailURL = (room: RoomForAvatarURL, avatarOverride?: ContentURI): string | undefined => {
return getRoomAvatarURL(room, avatarOverride, true)
} }

View file

@ -13,7 +13,7 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { getAvatarURL } from "@/api/media.ts" import { getAvatarThumbnailURL } from "@/api/media.ts"
import { Preferences, getLocalStoragePreferences, getPreferenceProxy } from "@/api/types/preferences" import { Preferences, getLocalStoragePreferences, getPreferenceProxy } from "@/api/types/preferences"
import { CustomEmojiPack, parseCustomEmojiPack } from "@/util/emoji" import { CustomEmojiPack, parseCustomEmojiPack } from "@/util/emoji"
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts" import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
@ -469,7 +469,7 @@ export class StateStore {
body = body.slice(0, 350) + " […]" body = body.slice(0, 350) + " […]"
} }
const memberEvt = room.getStateEvent("m.room.member", evt.sender) const memberEvt = room.getStateEvent("m.room.member", evt.sender)
const icon = `${getAvatarURL(evt.sender, memberEvt?.content)}&image_auth=${this.imageAuthToken}` const icon = `${getAvatarThumbnailURL(evt.sender, memberEvt?.content)}&image_auth=${this.imageAuthToken}`
const roomName = room.meta.current.name ?? "Unnamed room" const roomName = room.meta.current.name ?? "Unnamed room"
const senderName = memberEvt?.content.displayname ?? evt.sender const senderName = memberEvt?.content.displayname ?? evt.sender
const title = senderName === roomName ? senderName : `${senderName} (${roomName})` const title = senderName === roomName ? senderName : `${senderName} (${roomName})`

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { JSX, RefObject, use, useEffect } from "react" import { JSX, RefObject, use, useEffect } from "react"
import { getAvatarURL, getMediaURL } from "@/api/media.ts" import { getAvatarThumbnailURL, getMediaURL } from "@/api/media.ts"
import { AutocompleteMemberEntry, RoomStateStore, useCustomEmojis } from "@/api/statestore" import { AutocompleteMemberEntry, RoomStateStore, useCustomEmojis } from "@/api/statestore"
import { Emoji, emojiToMarkdown, useSortedAndFilteredEmojis } from "@/util/emoji" import { Emoji, emojiToMarkdown, useSortedAndFilteredEmojis } from "@/util/emoji"
import { escapeMarkdown } from "@/util/markdown.ts" import { escapeMarkdown } from "@/util/markdown.ts"
@ -138,7 +138,7 @@ const userFuncs = {
<img <img
className="small avatar" className="small avatar"
loading="lazy" loading="lazy"
src={getAvatarURL(user.userID, { displayname: user.displayName, avatar_url: user.avatarURL })} src={getAvatarThumbnailURL(user.userID, { displayname: user.displayName, avatar_url: user.avatarURL })}
alt="" alt=""
/> />
{user.displayName} {user.displayName}

View file

@ -15,7 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { JSX, use } from "react" import { JSX, use } from "react"
import { PulseLoader } from "react-spinners" import { PulseLoader } from "react-spinners"
import { getAvatarURL } from "@/api/media.ts" import { getAvatarThumbnailURL } from "@/api/media.ts"
import { useMultipleRoomMembers, useRoomTyping } from "@/api/statestore" import { useMultipleRoomMembers, useRoomTyping } from "@/api/statestore"
import { humanJoin } from "@/util/join.ts" import { humanJoin } from "@/util/join.ts"
import { getDisplayname } from "@/util/validation.ts" import { getDisplayname } from "@/util/validation.ts"
@ -40,7 +40,7 @@ const TypingNotifications = () => {
key={sender} key={sender}
className="small avatar" className="small avatar"
loading="lazy" loading="lazy"
src={getAvatarURL(sender, member)} src={getAvatarThumbnailURL(sender, member)}
alt="" alt=""
/>) />)
memberNames.push(getDisplayname(sender, member)) memberNames.push(getDisplayname(sender, member))

View file

@ -36,7 +36,7 @@ const LightboxWrapper = ({ children }: { children: React.ReactNode }) => {
return return
} }
params = { params = {
src: target.src, src: target.getAttribute("data-full-src") ?? target.src,
alt: target.alt, alt: target.alt,
} }
setParams(params) setParams(params)

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import React, { use, useState } from "react" import React, { use, useState } from "react"
import { getAvatarURL } from "@/api/media.ts" import { getAvatarThumbnailURL } from "@/api/media.ts"
import { MemDBEvent, MemberEventContent } from "@/api/types" import { MemDBEvent, MemberEventContent } from "@/api/types"
import { getDisplayname } from "@/util/validation.ts" import { getDisplayname } from "@/util/validation.ts"
import ClientContext from "../ClientContext.ts" import ClientContext from "../ClientContext.ts"
@ -33,7 +33,7 @@ const MemberRow = ({ evt, onClick }: MemberRowProps) => {
return <div className="member" data-target-panel="user" data-target-user={userID} onClick={onClick}> return <div className="member" data-target-panel="user" data-target-user={userID} onClick={onClick}>
<img <img
className="avatar" className="avatar"
src={getAvatarURL(userID, content)} src={getAvatarThumbnailURL(userID, content)}
alt="" alt=""
loading="lazy" loading="lazy"
/> />

View file

@ -61,6 +61,7 @@ const UserInfo = ({ userID }: UserInfoProps) => {
className="avatar-loader" className="avatar-loader"
/> : <img /> : <img
className="avatar" className="avatar"
// this is a big avatar (236px by default), use full resolution
src={getAvatarURL(userID, member ?? globalProfile)} src={getAvatarURL(userID, member ?? globalProfile)}
onClick={openLightbox} onClick={openLightbox}
alt="" alt=""

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { JSX, memo, use } from "react" import { JSX, memo, use } from "react"
import { getRoomAvatarURL } from "@/api/media.ts" import { getRoomAvatarThumbnailURL } from "@/api/media.ts"
import type { RoomListEntry } from "@/api/statestore" import type { RoomListEntry } from "@/api/statestore"
import type { MemDBEvent, MemberEventContent } from "@/api/types" import type { MemDBEvent, MemberEventContent } from "@/api/types"
import useContentVisibility from "@/util/contentvisibility.ts" import useContentVisibility from "@/util/contentvisibility.ts"
@ -63,7 +63,7 @@ function renderEntry(room: RoomListEntry) {
<img <img
loading="lazy" loading="lazy"
className="avatar room-avatar" className="avatar room-avatar"
src={getRoomAvatarURL(room)} src={getRoomAvatarThumbnailURL(room)}
alt="" alt=""
/> />
</div> </div>

View file

@ -15,7 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import React from "react" import React from "react"
import Client from "@/api/client.ts" import Client from "@/api/client.ts"
import { getRoomAvatarURL } from "@/api/media.ts" import { getRoomAvatarThumbnailURL } from "@/api/media.ts"
import type { RoomID } from "@/api/types" import type { RoomID } from "@/api/types"
import { useEventAsState } from "@/util/eventdispatcher.ts" import { useEventAsState } from "@/util/eventdispatcher.ts"
import UnreadCount from "./UnreadCount.tsx" import UnreadCount from "./UnreadCount.tsx"
@ -37,7 +37,7 @@ const Space = ({ roomID, client, onClick, isActive, onClickUnread }: SpaceProps)
} }
return <div className={`space-entry ${isActive ? "active" : ""}`} onClick={onClick} data-target-space={roomID}> return <div className={`space-entry ${isActive ? "active" : ""}`} onClick={onClick} data-target-space={roomID}>
<UnreadCount counts={unreads} space={true} onClick={onClickUnread} /> <UnreadCount counts={unreads} space={true} onClick={onClickUnread} />
<img src={getRoomAvatarURL(room)} alt={room.name} title={room.name} className="avatar" /> <img src={getRoomAvatarThumbnailURL(room)} alt={room.name} title={room.name} className="avatar" />
</div> </div>
} }

View file

@ -15,7 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { use, useEffect, useState } from "react" import { use, useEffect, useState } from "react"
import { ScaleLoader } from "react-spinners" import { ScaleLoader } from "react-spinners"
import { getAvatarURL, getRoomAvatarURL } from "@/api/media.ts" import { getAvatarThumbnailURL, getAvatarURL, getRoomAvatarURL } from "@/api/media.ts"
import { InvitedRoomStore } from "@/api/statestore/invitedroom.ts" import { InvitedRoomStore } from "@/api/statestore/invitedroom.ts"
import { RoomID, RoomSummary } from "@/api/types" import { RoomID, RoomSummary } from "@/api/types"
import { getDisplayname, getServerName } from "@/util/validation.ts" import { getDisplayname, getServerName } from "@/util/validation.ts"
@ -90,7 +90,8 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => {
<img <img
className="small avatar" className="small avatar"
onClick={use(LightboxContext)} onClick={use(LightboxContext)}
src={getAvatarURL(invite.invited_by, invite.inviter_profile)} src={getAvatarThumbnailURL(invite.invited_by, invite.inviter_profile)}
data-full-src={getAvatarURL(invite.invited_by, invite.inviter_profile)}
alt="" alt=""
/> />
<span className="inviter-name" title={invite.invited_by}> <span className="inviter-name" title={invite.invited_by}>
@ -100,6 +101,7 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => {
</div> : null} </div> : null}
<h2 className="room-name">{name}</h2> <h2 className="room-name">{name}</h2>
<img <img
// this is a big avatar (120px), use full resolution
src={getRoomAvatarURL(invite ?? summary ?? { room_id: roomID })} src={getRoomAvatarURL(invite ?? summary ?? { room_id: roomID })}
className="large avatar" className="large avatar"
onClick={use(LightboxContext)} onClick={use(LightboxContext)}

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { use } from "react" import { use } from "react"
import { getRoomAvatarURL } from "@/api/media.ts" import { getRoomAvatarThumbnailURL, getRoomAvatarURL } from "@/api/media.ts"
import { RoomStateStore } from "@/api/statestore" import { RoomStateStore } from "@/api/statestore"
import { useEventAsState } from "@/util/eventdispatcher.ts" import { useEventAsState } from "@/util/eventdispatcher.ts"
import MainScreenContext from "../MainScreenContext.ts" import MainScreenContext from "../MainScreenContext.ts"
@ -48,7 +48,8 @@ const RoomViewHeader = ({ room }: RoomViewHeaderProps) => {
<img <img
className="avatar" className="avatar"
loading="lazy" loading="lazy"
src={getRoomAvatarURL(roomMeta)} src={getRoomAvatarThumbnailURL(roomMeta)}
data-full-src={getRoomAvatarURL(roomMeta)}
onClick={use(LightboxContext)} onClick={use(LightboxContext)}
alt="" alt=""
/> />

View file

@ -16,7 +16,7 @@
import { Suspense, lazy, use, useCallback, useRef, useState } from "react" import { Suspense, lazy, use, useCallback, useRef, useState } from "react"
import { ScaleLoader } from "react-spinners" import { ScaleLoader } from "react-spinners"
import Client from "@/api/client.ts" import Client from "@/api/client.ts"
import { getRoomAvatarURL } from "@/api/media.ts" import { getRoomAvatarThumbnailURL, getRoomAvatarURL } from "@/api/media.ts"
import { RoomStateStore, usePreferences } from "@/api/statestore" import { RoomStateStore, usePreferences } from "@/api/statestore"
import { import {
Preference, Preference,
@ -355,7 +355,8 @@ const SettingsView = ({ room }: SettingsViewProps) => {
<img <img
className="avatar large" className="avatar large"
loading="lazy" loading="lazy"
src={getRoomAvatarURL(roomMeta)} src={getRoomAvatarThumbnailURL(roomMeta)}
data-full-src={getRoomAvatarURL(roomMeta)}
onClick={use(LightboxContext)} onClick={use(LightboxContext)}
alt="" alt=""
/> />

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { use } from "react" import { use } from "react"
import { getAvatarURL } from "@/api/media.ts" import { getAvatarThumbnailURL } from "@/api/media.ts"
import { RoomStateStore, useMultipleRoomMembers, useReadReceipts } from "@/api/statestore" import { RoomStateStore, useMultipleRoomMembers, useReadReceipts } from "@/api/statestore"
import { EventID } from "@/api/types" import { EventID } from "@/api/types"
import { humanJoin } from "@/util/join.ts" import { humanJoin } from "@/util/join.ts"
@ -37,7 +37,7 @@ const ReadReceipts = ({ room, eventID }: { room: RoomStateStore, eventID: EventI
key={userID} key={userID}
className="small avatar" className="small avatar"
loading="lazy" loading="lazy"
src={getAvatarURL(userID, member)} src={getAvatarThumbnailURL(userID, member)}
alt="" alt=""
/> />
}) })

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { use } from "react" import { use } from "react"
import { getAvatarURL, getUserColorIndex } from "@/api/media.ts" import { getAvatarThumbnailURL, getUserColorIndex } from "@/api/media.ts"
import { RoomStateStore, useRoomEvent, useRoomMember } from "@/api/statestore" import { RoomStateStore, useRoomEvent, useRoomMember } from "@/api/statestore"
import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types" import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types"
import { getDisplayname } from "@/util/validation.ts" import { getDisplayname } from "@/util/validation.ts"
@ -124,7 +124,7 @@ export const ReplyBody = ({
<img <img
className="small avatar" className="small avatar"
loading="lazy" loading="lazy"
src={getAvatarURL(perMessageSender?.id ?? event.sender, renderMemberEvtContent)} src={getAvatarThumbnailURL(perMessageSender?.id ?? event.sender, renderMemberEvtContent)}
alt="" alt=""
/> />
</div> </div>

View file

@ -15,7 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import React, { JSX, use, useState } from "react" import React, { JSX, use, useState } from "react"
import { createPortal } from "react-dom" import { createPortal } from "react-dom"
import { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts" import { getAvatarThumbnailURL, getMediaURL, getUserColorIndex } from "@/api/media.ts"
import { useRoomMember } from "@/api/statestore" import { useRoomMember } from "@/api/statestore"
import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types" import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
import { isMobileDevice } from "@/util/ismobile.ts" import { isMobileDevice } from "@/util/ismobile.ts"
@ -233,7 +233,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T
<img <img
className={`${smallAvatar ? "small" : ""} avatar`} className={`${smallAvatar ? "small" : ""} avatar`}
loading="lazy" loading="lazy"
src={getAvatarURL(perMessageSender?.id ?? evt.sender, renderMemberEvtContent)} src={getAvatarThumbnailURL(perMessageSender?.id ?? evt.sender, renderMemberEvtContent)}
alt="" alt=""
/> />
</div>} </div>}

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import React, { use } from "react" import React, { use } from "react"
import { getAvatarURL } from "@/api/media.ts" import { getAvatarThumbnailURL, getAvatarURL } from "@/api/media.ts"
import { MemberEventContent, UserID } from "@/api/types" import { MemberEventContent, UserID } from "@/api/types"
import { LightboxContext } from "../../modal" import { LightboxContext } from "../../modal"
import EventContentProps from "./props.ts" import EventContentProps from "./props.ts"
@ -25,7 +25,8 @@ function useChangeDescription(
const targetAvatar = <img const targetAvatar = <img
className="small avatar" className="small avatar"
loading="lazy" loading="lazy"
src={getAvatarURL(target, content)} src={getAvatarThumbnailURL(target, content)}
data-full-src={getAvatarURL(target, content)}
onClick={use(LightboxContext)!} onClick={use(LightboxContext)!}
alt="" alt=""
/> />
@ -59,7 +60,8 @@ function useChangeDescription(
className="small avatar" className="small avatar"
loading="lazy" loading="lazy"
height={16} height={16}
src={getAvatarURL(target, prevContent)} src={getAvatarThumbnailURL(target, prevContent)}
data-full-src={getAvatarURL(target, prevContent)}
onClick={use(LightboxContext)!} onClick={use(LightboxContext)!}
alt="" alt=""
/> to {targetAvatar} /> to {targetAvatar}

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { JSX, use } from "react" import { JSX, use } from "react"
import { getRoomAvatarURL } from "@/api/media.ts" import { getRoomAvatarThumbnailURL, getRoomAvatarURL } from "@/api/media.ts"
import { ContentURI, RoomAvatarEventContent } from "@/api/types" import { ContentURI, RoomAvatarEventContent } from "@/api/types"
import { ensureString } from "@/util/validation.ts" import { ensureString } from "@/util/validation.ts"
import { LightboxContext } from "../../modal" import { LightboxContext } from "../../modal"
@ -31,7 +31,8 @@ const RoomAvatarBody = ({ event, sender, room }: EventContentProps) => {
className="small avatar" className="small avatar"
loading="lazy" loading="lazy"
height={16} height={16}
src={getRoomAvatarURL(room.meta.current, url)} src={getRoomAvatarThumbnailURL(room.meta.current, url)}
data-full-src={getRoomAvatarURL(room.meta.current, url)}
onClick={openLightbox} onClick={openLightbox}
alt="" alt=""
/> />