web/roomlist: add option to hide invite avatars

This commit is contained in:
Tulir Asokan 2025-03-09 17:18:27 +02:00
parent 86843d61f6
commit ef05bc71f9
7 changed files with 46 additions and 15 deletions

View file

@ -78,11 +78,16 @@ 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, thumbnail = false): string | undefined => { export const getAvatarURL = (
userID: UserID,
content?: UserProfile | null,
thumbnail = false,
forceFallback = 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)
if (!mediaID) { if (!mediaID || forceFallback) {
return makeFallbackAvatar(backgroundColor, fallbackCharacter) return makeFallbackAvatar(backgroundColor, fallbackCharacter)
} }
const encrypted = !!content?.avatar_file const encrypted = !!content?.avatar_file
@ -91,8 +96,12 @@ export const getAvatarURL = (userID: UserID, content?: UserProfile | null, thumb
return thumbnail ? `${url}&thumbnail=avatar` : url return thumbnail ? `${url}&thumbnail=avatar` : url
} }
export const getAvatarThumbnailURL = (userID: UserID, content?: UserProfile | null): string | undefined => { export const getAvatarThumbnailURL = (
return getAvatarURL(userID, content, true) userID: UserID,
content?: UserProfile | null,
forceFallback = false,
): string | undefined => {
return getAvatarURL(userID, content, true, forceFallback)
} }
interface RoomForAvatarURL { interface RoomForAvatarURL {
@ -104,14 +113,21 @@ interface RoomForAvatarURL {
} }
export const getRoomAvatarURL = ( export const getRoomAvatarURL = (
room: RoomForAvatarURL, avatarOverride?: ContentURI, thumbnail = false, room: RoomForAvatarURL,
avatarOverride?: ContentURI,
thumbnail = false,
forceFallback = false,
): string | undefined => { ): 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) }, thumbnail, forceFallback)
} }
export const getRoomAvatarThumbnailURL = (room: RoomForAvatarURL, avatarOverride?: ContentURI): string | undefined => { export const getRoomAvatarThumbnailURL = (
return getRoomAvatarURL(room, avatarOverride, true) room: RoomForAvatarURL,
avatarOverride?: ContentURI,
forceFallback = false,
): string | undefined => {
return getRoomAvatarURL(room, avatarOverride, true, forceFallback)
} }

View file

@ -45,6 +45,7 @@ export class InvitedRoomStore implements RoomListEntry, RoomSummary {
readonly invited_by?: UserID readonly invited_by?: UserID
readonly inviter_profile?: MemberEventContent readonly inviter_profile?: MemberEventContent
readonly is_direct: boolean readonly is_direct: boolean
readonly is_invite = true
constructor(public readonly meta: DBInvitedRoom, parent: StateStore) { constructor(public readonly meta: DBInvitedRoom, parent: StateStore) {
this.room_id = meta.room_id this.room_id = meta.room_id

View file

@ -55,6 +55,7 @@ export interface RoomListEntry {
unread_notifications: number unread_notifications: number
unread_highlights: number unread_highlights: number
marked_unread: boolean marked_unread: boolean
is_invite?: boolean
} }
export interface GCSettings { export interface GCSettings {

View file

@ -65,6 +65,12 @@ export const preferences = {
allowedContexts: anyContext, allowedContexts: anyContext,
defaultValue: true, defaultValue: true,
}), }),
show_invite_avatars: new Preference<boolean>({
displayName: "Show avatars in invites",
description: "If disabled, the avatar of the room or invitee will not be shown in the invite view.",
allowedContexts: anyGlobalContext,
defaultValue: true,
}),
code_block_line_wrap: new Preference<boolean>({ code_block_line_wrap: new Preference<boolean>({
displayName: "Code block line wrap", displayName: "Code block line wrap",
description: "Whether to wrap long lines in code blocks instead of scrolling horizontally.", description: "Whether to wrap long lines in code blocks instead of scrolling horizontally.",

View file

@ -29,6 +29,7 @@ export interface RoomListEntryProps {
room: RoomListEntry room: RoomListEntry
isActive: boolean isActive: boolean
hidden: boolean hidden: boolean
hideAvatar?: boolean
} }
function getPreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent | null): [string, JSX.Element | null] { function getPreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent | null): [string, JSX.Element | null] {
@ -57,7 +58,7 @@ function getPreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent | null):
return ["", null] return ["", null]
} }
function renderEntry(room: RoomListEntry) { function renderEntry(room: RoomListEntry, hideAvatar: boolean | undefined) {
const [previewText, croppedPreviewText] = getPreviewText(room.preview_event, room.preview_sender) const [previewText, croppedPreviewText] = getPreviewText(room.preview_event, room.preview_sender)
return <> return <>
@ -65,7 +66,7 @@ function renderEntry(room: RoomListEntry) {
<img <img
loading="lazy" loading="lazy"
className="avatar room-avatar" className="avatar room-avatar"
src={getRoomAvatarThumbnailURL(room)} src={getRoomAvatarThumbnailURL(room, undefined, hideAvatar)}
alt="" alt=""
/> />
</div> </div>
@ -77,7 +78,7 @@ function renderEntry(room: RoomListEntry) {
</> </>
} }
const Entry = ({ room, isActive, hidden }: RoomListEntryProps) => { const Entry = ({ room, isActive, hidden, hideAvatar }: RoomListEntryProps) => {
const [isVisible, divRef] = useContentVisibility<HTMLDivElement>() const [isVisible, divRef] = useContentVisibility<HTMLDivElement>()
const openModal = use(ModalContext) const openModal = use(ModalContext)
const mainScreen = use(MainScreenContext) const mainScreen = use(MainScreenContext)
@ -105,7 +106,7 @@ const Entry = ({ room, isActive, hidden }: RoomListEntryProps) => {
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
data-room-id={room.room_id} data-room-id={room.room_id}
> >
{isVisible ? renderEntry(room) : null} {isVisible ? renderEntry(room, hideAvatar) : null}
</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, useCallback, useRef, useState } from "react" import React, { use, useCallback, useRef, useState } from "react"
import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts } from "@/api/statestore" import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts, usePreference } from "@/api/statestore"
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 reverseMap from "@/util/reversemap.ts" import reverseMap from "@/util/reversemap.ts"
@ -103,6 +103,7 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => {
} }
} }
const showInviteAvatars = usePreference(client.store, null, "show_invite_avatars")
const roomListFilter = client.store.roomListFilterFunc const roomListFilter = client.store.roomListFilterFunc
return <div className="room-list-wrapper"> return <div className="room-list-wrapper">
<div className="room-search-wrapper"> <div className="room-search-wrapper">
@ -145,6 +146,7 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => {
isActive={room.room_id === activeRoomID} isActive={room.room_id === activeRoomID}
hidden={roomListFilter ? !roomListFilter(room) : false} hidden={roomListFilter ? !roomListFilter(room) : false}
room={room} room={room}
hideAvatar={room.is_invite && !showInviteAvatars}
/>, />,
)} )}
</div> </div>

View file

@ -16,6 +16,7 @@
import { use, useEffect, useState } from "react" import { use, useEffect, useState } from "react"
import { ScaleLoader } from "react-spinners" import { ScaleLoader } from "react-spinners"
import { getAvatarThumbnailURL, getAvatarURL, getRoomAvatarURL } from "@/api/media.ts" import { getAvatarThumbnailURL, getAvatarURL, getRoomAvatarURL } from "@/api/media.ts"
import { usePreference } from "@/api/statestore/hooks.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"
@ -84,13 +85,15 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => {
const name = summary?.name ?? summary?.canonical_alias ?? invite?.name ?? invite?.canonical_alias ?? alias ?? roomID const name = summary?.name ?? summary?.canonical_alias ?? invite?.name ?? invite?.canonical_alias ?? alias ?? roomID
const memberCount = summary?.num_joined_members || null const memberCount = summary?.num_joined_members || null
const topic = summary?.topic ?? invite?.topic ?? "" const topic = summary?.topic ?? invite?.topic ?? ""
const showInviteAvatars = usePreference(client.store, null, "show_invite_avatars")
const noAvatarPreview = invite && !showInviteAvatars
return <div className="room-view preview"> return <div className="room-view preview">
<div className="preview-inner"> <div className="preview-inner">
{invite?.invited_by && !invite.dm_user_id ? <div className="inviter-info"> {invite?.invited_by && !invite.dm_user_id ? <div className="inviter-info">
<img <img
className="small avatar" className="small avatar"
onClick={use(LightboxContext)} onClick={use(LightboxContext)}
src={getAvatarThumbnailURL(invite.invited_by, invite.inviter_profile)} src={getAvatarThumbnailURL(invite.invited_by, invite.inviter_profile, noAvatarPreview)}
data-full-src={getAvatarURL(invite.invited_by, invite.inviter_profile)} data-full-src={getAvatarURL(invite.invited_by, invite.inviter_profile)}
alt="" alt=""
/> />
@ -102,7 +105,8 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => {
<h2 className="room-name">{name}</h2> <h2 className="room-name">{name}</h2>
<img <img
// this is a big avatar (120px), use full resolution // this is a big avatar (120px), use full resolution
src={getRoomAvatarURL(invite ?? summary ?? { room_id: roomID })} src={getRoomAvatarURL(invite ?? summary ?? { room_id: roomID }, undefined, false, noAvatarPreview)}
data-full-src={getRoomAvatarURL(invite ?? summary ?? { room_id: roomID })}
className="large avatar" className="large avatar"
onClick={use(LightboxContext)} onClick={use(LightboxContext)}
alt="" alt=""