initial commit
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: calc(var(--avatar-size) + var(--avatar-overlap));
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
height: var(--avatar-size);
|
||||
flex-shrink: 0;
|
||||
filter: drop-shadow(0 0 0 var(--avatar-outline, 2px) var(--background-tertiary));
|
||||
}
|
||||
|
||||
.container > *:first-child {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.avatar.withMask {
|
||||
mask: radial-gradient(
|
||||
50% 50% at calc(150% + var(--avatar-overlap)),
|
||||
transparent calc(100% + var(--avatar-outline, 2px)),
|
||||
black calc(100% + var(--avatar-outline, 2px) + 1px)
|
||||
);
|
||||
}
|
||||
|
||||
.remainingCount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: var(--avatar-outline, 2px) solid var(--background-tertiary);
|
||||
background: var(--background-modifier-selected);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
grid-row: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
60
fluxer_app/src/components/uikit/avatars/AvatarStack.tsx
Normal file
60
fluxer_app/src/components/uikit/avatars/AvatarStack.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {clsx} from 'clsx';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import styles from './AvatarStack.module.css';
|
||||
|
||||
export interface AvatarStackProps {
|
||||
children: React.ReactNode;
|
||||
size?: number;
|
||||
maxVisible?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AvatarStack: React.FC<AvatarStackProps> = observer(({children, size = 28, maxVisible = 3, className}) => {
|
||||
const childArray = React.Children.toArray(children).filter(Boolean);
|
||||
const totalCount = childArray.length;
|
||||
|
||||
const visibleChildren = childArray.slice(0, maxVisible);
|
||||
const remainingCount = Math.max(0, totalCount - maxVisible);
|
||||
const computedOutline = Math.min(3, Math.max(1, Math.round(size * 0.07)));
|
||||
const overlap = Math.round(-0.35 * size);
|
||||
|
||||
const cssVars = {
|
||||
'--avatar-size': `${size}px`,
|
||||
'--avatar-overlap': `${overlap}px`,
|
||||
'--avatar-outline': `${computedOutline}px`,
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.container, className)} style={cssVars}>
|
||||
{visibleChildren.map((child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={clsx(styles.avatar, (index < visibleChildren.length - 1 || remainingCount > 0) && styles.withMask)}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
{remainingCount > 0 && <div className={styles.remainingCount}>+{remainingCount}</div>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.imageWrapperSpeaking {
|
||||
outline-style: solid;
|
||||
outline-width: 2px;
|
||||
outline-color: #22c55e;
|
||||
}
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
border-radius: 9999px;
|
||||
image-rendering: auto;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {clsx} from 'clsx';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import type {UserRecord} from '~/records/UserRecord';
|
||||
import GuildMemberStore from '~/stores/GuildMemberStore';
|
||||
import * as AvatarUtils from '~/utils/AvatarUtils';
|
||||
import styles from './AvatarWithPresence.module.css';
|
||||
|
||||
interface Props {
|
||||
user: UserRecord;
|
||||
size: number;
|
||||
speaking?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
borderClassName?: string;
|
||||
guildId?: string | null;
|
||||
}
|
||||
|
||||
export const AvatarWithPresence: React.FC<Props> = observer(function AvatarWithPresence({
|
||||
user,
|
||||
size,
|
||||
speaking,
|
||||
className,
|
||||
title,
|
||||
borderClassName,
|
||||
guildId,
|
||||
}) {
|
||||
const guildMember = GuildMemberStore.getMember(guildId || '', user.id);
|
||||
const src =
|
||||
guildId && guildMember?.avatar
|
||||
? (AvatarUtils.getGuildMemberAvatarURL({
|
||||
guildId,
|
||||
userId: user.id,
|
||||
avatar: guildMember.avatar,
|
||||
animated: false,
|
||||
}) ?? AvatarUtils.getUserAvatarURL(user, false))
|
||||
: AvatarUtils.getUserAvatarURL(user, false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.container, borderClassName, className)}
|
||||
style={{width: size, height: size}}
|
||||
title={title ?? user.username}
|
||||
>
|
||||
<div className={clsx(styles.imageWrapper, speaking && styles.imageWrapperSpeaking)}>
|
||||
<img src={src} alt={user.username} draggable={false} loading="lazy" decoding="async" className={styles.image} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
62
fluxer_app/src/components/uikit/avatars/StackUserAvatar.tsx
Normal file
62
fluxer_app/src/components/uikit/avatars/StackUserAvatar.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {clsx} from 'clsx';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type {ChannelRecord} from '~/records/ChannelRecord';
|
||||
import type {GuildRecord} from '~/records/GuildRecord';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import MediaEngineStore from '~/stores/voice/MediaEngineFacade';
|
||||
import {AvatarWithPresence} from './AvatarWithPresence';
|
||||
import styles from './StackUserAvatar.module.css';
|
||||
|
||||
interface StackUserAvatarProps {
|
||||
guild: GuildRecord;
|
||||
channel: ChannelRecord;
|
||||
userId: string;
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const StackUserAvatar = observer(({guild, channel, userId, size = 28, className}: StackUserAvatarProps) => {
|
||||
const channelStates = MediaEngineStore.getAllVoiceStatesInChannel(guild.id, channel.id);
|
||||
const user = UserStore.getUser(userId);
|
||||
if (!user) return null;
|
||||
|
||||
let speaking = false;
|
||||
for (const state of Object.values(channelStates)) {
|
||||
if (state.user_id !== userId) continue;
|
||||
const connectionId = state.connection_id ?? '';
|
||||
const participant = MediaEngineStore.getParticipantByUserIdAndConnectionId(userId, connectionId);
|
||||
const selfMuted = state.self_mute ?? (participant ? !participant.isMicrophoneEnabled : false);
|
||||
const guildMuted = state.mute ?? false;
|
||||
|
||||
speaking ||= !!(participant?.isSpeaking && !selfMuted && !guildMuted);
|
||||
}
|
||||
|
||||
return (
|
||||
<AvatarWithPresence
|
||||
user={user}
|
||||
size={size}
|
||||
speaking={speaking}
|
||||
className={clsx(styles.container, className)}
|
||||
title={user.username}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user