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() ?? ""
}
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 backgroundColor = getUserColor(userID)
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 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 {
@ -98,9 +103,15 @@ interface RoomForAvatarURL {
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, {
displayname: room.name,
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
// 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 { CustomEmojiPack, parseCustomEmojiPack } from "@/util/emoji"
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
@ -469,7 +469,7 @@ export class StateStore {
body = body.slice(0, 350) + " […]"
}
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 senderName = memberEvt?.content.displayname ?? evt.sender
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
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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 { Emoji, emojiToMarkdown, useSortedAndFilteredEmojis } from "@/util/emoji"
import { escapeMarkdown } from "@/util/markdown.ts"
@ -138,7 +138,7 @@ const userFuncs = {
<img
className="small avatar"
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=""
/>
{user.displayName}

View file

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

View file

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

View file

@ -14,7 +14,7 @@
// 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/>.
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 { getDisplayname } from "@/util/validation.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}>
<img
className="avatar"
src={getAvatarURL(userID, content)}
src={getAvatarThumbnailURL(userID, content)}
alt=""
loading="lazy"
/>

View file

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

View file

@ -14,7 +14,7 @@
// 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/>.
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 { MemDBEvent, MemberEventContent } from "@/api/types"
import useContentVisibility from "@/util/contentvisibility.ts"
@ -63,7 +63,7 @@ function renderEntry(room: RoomListEntry) {
<img
loading="lazy"
className="avatar room-avatar"
src={getRoomAvatarURL(room)}
src={getRoomAvatarThumbnailURL(room)}
alt=""
/>
</div>

View file

@ -15,7 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import React from "react"
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 { useEventAsState } from "@/util/eventdispatcher.ts"
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}>
<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>
}

View file

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

View file

@ -14,7 +14,7 @@
// 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/>.
import { use } from "react"
import { getRoomAvatarURL } from "@/api/media.ts"
import { getRoomAvatarThumbnailURL, getRoomAvatarURL } from "@/api/media.ts"
import { RoomStateStore } from "@/api/statestore"
import { useEventAsState } from "@/util/eventdispatcher.ts"
import MainScreenContext from "../MainScreenContext.ts"
@ -48,7 +48,8 @@ const RoomViewHeader = ({ room }: RoomViewHeaderProps) => {
<img
className="avatar"
loading="lazy"
src={getRoomAvatarURL(roomMeta)}
src={getRoomAvatarThumbnailURL(roomMeta)}
data-full-src={getRoomAvatarURL(roomMeta)}
onClick={use(LightboxContext)}
alt=""
/>

View file

@ -16,7 +16,7 @@
import { Suspense, lazy, use, useCallback, useRef, useState } from "react"
import { ScaleLoader } from "react-spinners"
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 {
Preference,
@ -355,7 +355,8 @@ const SettingsView = ({ room }: SettingsViewProps) => {
<img
className="avatar large"
loading="lazy"
src={getRoomAvatarURL(roomMeta)}
src={getRoomAvatarThumbnailURL(roomMeta)}
data-full-src={getRoomAvatarURL(roomMeta)}
onClick={use(LightboxContext)}
alt=""
/>

View file

@ -14,7 +14,7 @@
// 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/>.
import { use } from "react"
import { getAvatarURL } from "@/api/media.ts"
import { getAvatarThumbnailURL } from "@/api/media.ts"
import { RoomStateStore, useMultipleRoomMembers, useReadReceipts } from "@/api/statestore"
import { EventID } from "@/api/types"
import { humanJoin } from "@/util/join.ts"
@ -37,7 +37,7 @@ const ReadReceipts = ({ room, eventID }: { room: RoomStateStore, eventID: EventI
key={userID}
className="small avatar"
loading="lazy"
src={getAvatarURL(userID, member)}
src={getAvatarThumbnailURL(userID, member)}
alt=""
/>
})

View file

@ -14,7 +14,7 @@
// 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/>.
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 type { EventID, MemDBEvent, MemberEventContent } from "@/api/types"
import { getDisplayname } from "@/util/validation.ts"
@ -124,7 +124,7 @@ export const ReplyBody = ({
<img
className="small avatar"
loading="lazy"
src={getAvatarURL(perMessageSender?.id ?? event.sender, renderMemberEvtContent)}
src={getAvatarThumbnailURL(perMessageSender?.id ?? event.sender, renderMemberEvtContent)}
alt=""
/>
</div>

View file

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

View file

@ -14,7 +14,7 @@
// 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/>.
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 { LightboxContext } from "../../modal"
import EventContentProps from "./props.ts"
@ -25,7 +25,8 @@ function useChangeDescription(
const targetAvatar = <img
className="small avatar"
loading="lazy"
src={getAvatarURL(target, content)}
src={getAvatarThumbnailURL(target, content)}
data-full-src={getAvatarURL(target, content)}
onClick={use(LightboxContext)!}
alt=""
/>
@ -59,7 +60,8 @@ function useChangeDescription(
className="small avatar"
loading="lazy"
height={16}
src={getAvatarURL(target, prevContent)}
src={getAvatarThumbnailURL(target, prevContent)}
data-full-src={getAvatarURL(target, prevContent)}
onClick={use(LightboxContext)!}
alt=""
/> to {targetAvatar}

View file

@ -14,7 +14,7 @@
// 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/>.
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 { ensureString } from "@/util/validation.ts"
import { LightboxContext } from "../../modal"
@ -31,7 +31,8 @@ const RoomAvatarBody = ({ event, sender, room }: EventContentProps) => {
className="small avatar"
loading="lazy"
height={16}
src={getRoomAvatarURL(room.meta.current, url)}
src={getRoomAvatarThumbnailURL(room.meta.current, url)}
data-full-src={getRoomAvatarURL(room.meta.current, url)}
onClick={openLightbox}
alt=""
/>