initial commit

This commit is contained in:
Hampus Kraft
2026-01-01 20:42:59 +00:00
commit 2f557eda8c
9029 changed files with 1490197 additions and 0 deletions

View File

@@ -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;
}

View 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>
);
});

View File

@@ -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;
}

View File

@@ -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>
);
});

View File

@@ -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;
}

View 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}
/>
);
});