Files
cinny/src/app/hooks/useUserBanner.ts
litruv e65a516350
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 1s
Add MSC4522 username colors, release notes dialog, and profile layout fixes.
Replace legacy profile style metadata with MSC4133 profile fields, add in-app update notes, tighten account profile name spacing, and wire desktop media save helpers through the client.
2026-08-23 08:06:48 +10:00

143 lines
3.6 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import { useMatrixClient } from './useMatrixClient';
import { useMediaAuthentication } from './useMediaAuthentication';
import { mxcUrlToHttp } from '../utils/matrix';
import { getCurrentAccessToken } from '../utils/auth';
import {
deleteBannerUrl,
loadBannerUrl,
registerBannerCacheClear,
saveBannerUrl,
} from '../utils/profileFields';
/**
* Hook to manage the user's profile banner (MSC4427 via MSC4133).
*/
export function useUserBanner(): [
string | undefined,
(banner: string | undefined) => Promise<void>,
boolean
] {
const mx = useMatrixClient();
const [banner, setBanner] = useState<string | undefined>();
const [loading, setLoading] = useState(true);
useEffect(() => {
const userId = mx.getUserId();
if (!userId) {
setLoading(false);
return undefined;
}
let cancelled = false;
const load = async () => {
setLoading(true);
try {
const bannerMxc = await loadBannerUrl(mx, userId);
if (!cancelled) setBanner(bannerMxc);
} catch {
if (!cancelled) setBanner(undefined);
}
if (!cancelled) setLoading(false);
};
load();
return () => {
cancelled = true;
};
}, [mx]);
const updateBanner = useCallback(
async (newBanner: string | undefined) => {
if (!newBanner) {
await deleteBannerUrl(mx);
setBanner(undefined);
return;
}
await saveBannerUrl(mx, newBanner);
setBanner(newBanner);
},
[mx]
);
return [banner, updateBanner, loading];
}
const userBannerCache = new Map<string, { bannerMxc: string | undefined; timestamp: number }>();
const CACHE_TTL_MS = 5 * 60 * 1000;
registerBannerCacheClear((userId: string) => {
userBannerCache.delete(userId);
});
async function fetchBannerBlobUrl(
mx: ReturnType<typeof useMatrixClient>,
bannerMxc: string,
useAuthentication: boolean
): Promise<string | undefined> {
const bannerHttpUrl = mxcUrlToHttp(mx, bannerMxc, useAuthentication);
if (!bannerHttpUrl) return undefined;
const accessToken = getCurrentAccessToken();
const headers: HeadersInit = {};
if (useAuthentication && accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const response = await fetch(bannerHttpUrl, { headers });
if (!response.ok) return undefined;
const blob = await response.blob();
return URL.createObjectURL(blob);
}
/**
* Hook to get another user's profile banner as a blob URL.
*/
export function useOtherUserBanner(userId: string): string | undefined {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [bannerBlobUrl, setBannerBlobUrl] = useState<string | undefined>();
useEffect(() => {
if (!userId) {
setBannerBlobUrl(undefined);
return undefined;
}
let blobUrl: string | undefined;
let cancelled = false;
const load = async () => {
const cached = userBannerCache.get(userId);
let bannerMxc: string | undefined;
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
bannerMxc = cached.bannerMxc;
} else {
bannerMxc = await loadBannerUrl(mx, userId);
userBannerCache.set(userId, { bannerMxc, timestamp: Date.now() });
}
if (!bannerMxc) {
if (!cancelled) setBannerBlobUrl(undefined);
return;
}
blobUrl = await fetchBannerBlobUrl(mx, bannerMxc, useAuthentication);
if (!cancelled) setBannerBlobUrl(blobUrl);
};
load();
return () => {
cancelled = true;
if (blobUrl) URL.revokeObjectURL(blobUrl);
};
}, [mx, useAuthentication, userId]);
return bannerBlobUrl;
}