mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
fix(app): paint avatars from a real image with a fading placeholder (#1856)
This commit is contained in:
1 parent
b49cf349a8
commit
4c14ba0a5e
13 files changed
+384
-358
No files matched your search
@@ -5,7 +5,6 @@ import styles from '@app/features/app/components/shared/GroupDMAvatar.module.css
|
||||
import {getStatusTypeLabel} from '@app/features/app/constants/AppConstants';
|
||||
import type {Channel} from '@app/features/channel/models/Channel';
|
||||
import {getGroupDMAccentColor} from '@app/features/channel/utils/GroupDMColorUtils';
|
||||
import {cdnUrl} from '@app/features/messaging/utils/MessagingUrlUtils';
|
||||
import Presence from '@app/features/presence/state/Presence';
|
||||
import TransientPresence from '@app/features/presence/state/TransientPresence';
|
||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||
@@ -147,7 +146,7 @@ function renderGroupStatusDot(status: StatusType, size: number, isTyping?: boole
|
||||
width={1}
|
||||
height={1}
|
||||
fill={statusColor}
|
||||
mask={`url(#svg-mask-status-${renderableStatus})`}
|
||||
mask={`url(#flx-mask-presence-${renderableStatus})`}
|
||||
data-flx="app.group-dm-avatar.render-group-status-dot.rect"
|
||||
/>
|
||||
</svg>
|
||||
@@ -446,13 +445,9 @@ export const GroupDMAvatar: React.FC<GroupDMAvatarProps> = observer(
|
||||
const user = Users.getUser(userId);
|
||||
const {top, left, avatarSize} = getAvatarPosition(count, index, clusterSize);
|
||||
const avatarMaskId = `${groupMaskId}-avatar-${index}`;
|
||||
let avatarUrl: string;
|
||||
if (user) {
|
||||
avatarUrl = AvatarUtils.getUserAvatarURL({id: user.id, avatar: user.avatar});
|
||||
} else {
|
||||
const avatarIndex = index % 6;
|
||||
avatarUrl = cdnUrl(`avatars/${avatarIndex}.png`);
|
||||
}
|
||||
const avatarUrl = user
|
||||
? AvatarUtils.getUserAvatarURL({id: user.id, avatar: user.avatar})
|
||||
: AvatarUtils.getDefaultAvatarURL(userId);
|
||||
return (
|
||||
<image
|
||||
key={userId}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
type ConnectionLike = {
|
||||
saveData?: boolean;
|
||||
addEventListener?: (event: string, cb: () => void) => void;
|
||||
removeEventListener?: (event: string, cb: () => void) => void;
|
||||
};
|
||||
|
||||
const getConnection = (): ConnectionLike | null => {
|
||||
if (typeof navigator === 'undefined') return null;
|
||||
const nav = navigator as Navigator & {connection?: ConnectionLike};
|
||||
return nav.connection ?? null;
|
||||
};
|
||||
|
||||
const readSaveData = (): boolean => getConnection()?.saveData === true;
|
||||
|
||||
const saveDataListeners = new Set<(value: boolean) => void>();
|
||||
|
||||
let cachedSaveData = readSaveData();
|
||||
let detachConnectionListener: (() => void) | null = null;
|
||||
|
||||
function publishSaveData(): void {
|
||||
const next = readSaveData();
|
||||
if (next === cachedSaveData) return;
|
||||
cachedSaveData = next;
|
||||
for (const listener of saveDataListeners) listener(next);
|
||||
}
|
||||
|
||||
export function subscribeSaveData(listener: (value: boolean) => void): () => void {
|
||||
saveDataListeners.add(listener);
|
||||
if (detachConnectionListener === null) {
|
||||
const connection = getConnection();
|
||||
if (connection?.addEventListener) {
|
||||
connection.addEventListener('change', publishSaveData);
|
||||
const {removeEventListener} = connection;
|
||||
detachConnectionListener = removeEventListener
|
||||
? () => removeEventListener.call(connection, 'change', publishSaveData)
|
||||
: () => {};
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
saveDataListeners.delete(listener);
|
||||
if (saveDataListeners.size > 0) return;
|
||||
detachConnectionListener?.();
|
||||
detachConnectionListener = null;
|
||||
};
|
||||
}
|
||||
|
||||
export function useSaveData(): boolean {
|
||||
const [saveData, setSaveData] = useState(readSaveData);
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscribeSaveData(setSaveData);
|
||||
cachedSaveData = readSaveData();
|
||||
setSaveData(cachedSaveData);
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
return saveData;
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import Accessibility from '@app/features/accessibility/state/Accessibility';
|
||||
import {useAnimatedMediaPlaybackAllowed} from '@app/features/app/hooks/useAnimatedMediaPlayback';
|
||||
import {useSaveData} from '@app/features/app/hooks/useSaveData';
|
||||
import UserSettings from '@app/features/user/state/UserSettings';
|
||||
import {StickerAnimationOptions} from '@fluxer/constants/src/UserConstants';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export type ShouldAnimateKind =
|
||||
| 'avatar'
|
||||
@@ -17,65 +16,13 @@ export type ShouldAnimateKind =
|
||||
|
||||
export interface UseShouldAnimateOptions {
|
||||
kind: ShouldAnimateKind;
|
||||
isAnimated?: boolean;
|
||||
isHovering?: boolean;
|
||||
isFocused?: boolean;
|
||||
entitlementOk?: boolean;
|
||||
respectPlaybackAllowed?: boolean;
|
||||
}
|
||||
|
||||
type ConnectionLike = {
|
||||
saveData?: boolean;
|
||||
addEventListener?: (event: string, cb: () => void) => void;
|
||||
};
|
||||
|
||||
const getConnection = (): ConnectionLike | undefined => {
|
||||
const nav =
|
||||
typeof navigator === 'undefined'
|
||||
? undefined
|
||||
: (navigator as Navigator & {
|
||||
connection?: ConnectionLike;
|
||||
});
|
||||
return nav?.connection;
|
||||
};
|
||||
|
||||
let cachedSaveData: boolean = getConnection()?.saveData === true;
|
||||
|
||||
const saveDataListeners = new Set<(value: boolean) => void>();
|
||||
|
||||
let saveDataListenerInstalled = false;
|
||||
|
||||
function installSaveDataListener(): void {
|
||||
if (saveDataListenerInstalled) return;
|
||||
const connection = getConnection();
|
||||
if (!connection?.addEventListener) {
|
||||
saveDataListenerInstalled = true;
|
||||
return;
|
||||
}
|
||||
connection.addEventListener('change', () => {
|
||||
const next = getConnection()?.saveData === true;
|
||||
if (next !== cachedSaveData) {
|
||||
cachedSaveData = next;
|
||||
saveDataListeners.forEach((listener) => listener(next));
|
||||
}
|
||||
});
|
||||
saveDataListenerInstalled = true;
|
||||
}
|
||||
|
||||
function useSaveData(): boolean {
|
||||
const [saveData, setSaveData] = useState(() => getConnection()?.saveData === true);
|
||||
useEffect(() => {
|
||||
installSaveDataListener();
|
||||
const live = getConnection()?.saveData === true;
|
||||
if (live !== cachedSaveData) cachedSaveData = live;
|
||||
setSaveData(live);
|
||||
saveDataListeners.add(setSaveData);
|
||||
return () => {
|
||||
saveDataListeners.delete(setSaveData);
|
||||
};
|
||||
}, []);
|
||||
return saveData;
|
||||
}
|
||||
|
||||
function isKeptUnderReducedMotion(kind: ShouldAnimateKind): boolean {
|
||||
if (kind === 'emoji' || kind === 'gif' || kind === 'sticker') {
|
||||
return Accessibility.isAnimationKeptUnderReducedMotion(kind);
|
||||
@@ -106,27 +53,27 @@ function getKindAllowance(kind: ShouldAnimateKind): AnimationAllowanceMode {
|
||||
}
|
||||
|
||||
export interface ShouldAnimateDecisionInput {
|
||||
isAnimated?: boolean;
|
||||
allowance: AnimationAllowanceMode;
|
||||
reducedMotion: boolean;
|
||||
keptUnderReducedMotion: boolean;
|
||||
isInteracting: boolean;
|
||||
entitlementOk?: boolean;
|
||||
saveData: boolean;
|
||||
animatedMediaPlaybackAllowed: boolean;
|
||||
}
|
||||
|
||||
export function resolveShouldAnimateDecision({
|
||||
isAnimated = true,
|
||||
allowance,
|
||||
reducedMotion,
|
||||
keptUnderReducedMotion,
|
||||
isInteracting,
|
||||
entitlementOk,
|
||||
saveData,
|
||||
animatedMediaPlaybackAllowed,
|
||||
}: ShouldAnimateDecisionInput): boolean {
|
||||
if (!isAnimated) return false;
|
||||
if (entitlementOk === false) return false;
|
||||
if (saveData) return false;
|
||||
if (!animatedMediaPlaybackAllowed) return false;
|
||||
if (allowance === 'NEVER') return false;
|
||||
if (reducedMotion && !keptUnderReducedMotion) return isInteracting;
|
||||
if (allowance === 'ALWAYS') return true;
|
||||
@@ -135,21 +82,20 @@ export function resolveShouldAnimateDecision({
|
||||
|
||||
export function useShouldAnimate({
|
||||
kind,
|
||||
isAnimated = true,
|
||||
isHovering = false,
|
||||
isFocused = false,
|
||||
entitlementOk,
|
||||
respectPlaybackAllowed = true,
|
||||
}: UseShouldAnimateOptions): boolean {
|
||||
const saveData = useSaveData();
|
||||
const animatedMediaPlaybackAllowed = useAnimatedMediaPlaybackAllowed({enabled: respectPlaybackAllowed});
|
||||
const allowance = getKindAllowance(kind);
|
||||
return resolveShouldAnimateDecision({
|
||||
isAnimated,
|
||||
allowance,
|
||||
reducedMotion: Accessibility.useReducedMotion,
|
||||
keptUnderReducedMotion: isKeptUnderReducedMotion(kind),
|
||||
isInteracting: isHovering || isFocused,
|
||||
entitlementOk,
|
||||
saveData,
|
||||
animatedMediaPlaybackAllowed,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user