feat(ui): WelcomeHeader polish — AvatarStack, Owner label, Open Group Settings, Leave hover fix
This commit is contained in:
@@ -14,6 +14,8 @@ import {
|
|||||||
type PendingBubble,
|
type PendingBubble,
|
||||||
} from '../../stores/pendingMessageStore';
|
} from '../../stores/pendingMessageStore';
|
||||||
import { Avatar } from '../ui/Avatar';
|
import { Avatar } from '../ui/Avatar';
|
||||||
|
import { AvatarStack } from '../ui/AvatarStack';
|
||||||
|
import { useUIStore } from '../../stores/uiStore';
|
||||||
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
||||||
import { isSelf, parseFederatedUsername } from '../../utils/identity';
|
import { isSelf, parseFederatedUsername } from '../../utils/identity';
|
||||||
import { useDelayedLoading } from '../../hooks/useDelayedLoading';
|
import { useDelayedLoading } from '../../hooks/useDelayedLoading';
|
||||||
@@ -757,6 +759,8 @@ function WelcomeHeader({ channelId }: { channelId: string }) {
|
|||||||
const authUser = useAuthStore((s) => s.user);
|
const authUser = useAuthStore((s) => s.user);
|
||||||
const removeFriend = useSocialStore((s) => s.removeFriend);
|
const removeFriend = useSocialStore((s) => s.removeFriend);
|
||||||
const friends = useSocialStore((s) => s.friends);
|
const friends = useSocialStore((s) => s.friends);
|
||||||
|
const openUserProfile = useUIStore((s) => s.openUserProfile);
|
||||||
|
const openModal = useUIStore((s) => s.openModal);
|
||||||
const isDm = isDmChannel(channelId);
|
const isDm = isDmChannel(channelId);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -767,7 +771,7 @@ function WelcomeHeader({ channelId }: { channelId: string }) {
|
|||||||
const isGroupDm = !!dm.ownerId;
|
const isGroupDm = !!dm.ownerId;
|
||||||
|
|
||||||
if (isGroupDm) {
|
if (isGroupDm) {
|
||||||
const groupName = otherMembers
|
const groupName = dm.name ?? otherMembers
|
||||||
.map(m => m.displayName ?? (m.username?.includes('@') ? m.username.split('@')[0] : m.username))
|
.map(m => m.displayName ?? (m.username?.includes('@') ? m.username.split('@')[0] : m.username))
|
||||||
.join(', ');
|
.join(', ');
|
||||||
const ownerMember = dm.members.find(m => m.id === dm.ownerId);
|
const ownerMember = dm.members.find(m => m.id === dm.ownerId);
|
||||||
@@ -783,41 +787,57 @@ function WelcomeHeader({ channelId }: { channelId: string }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenSettings = () => {
|
||||||
|
openModal('groupDmSettings', { dmChannelId: channelId, initialTab: 'overview' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOwnerClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
if (!ownerMember) return;
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
openUserProfile(ownerMember, {
|
||||||
|
top: Math.min(rect.bottom + 8, window.innerHeight - 450),
|
||||||
|
left: rect.left,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 pt-8 pb-4">
|
<div className="px-4 pt-8 pb-4">
|
||||||
<div className="mb-2 relative" style={{ width: 80, height: 80 }}>
|
<div className="mb-2">
|
||||||
{otherMembers.slice(0, 2).map((m, idx) => (
|
<AvatarStack members={otherMembers} size={80} border="chat" iconUrl={dm.icon} />
|
||||||
<div
|
|
||||||
key={m.id}
|
|
||||||
className="absolute rounded-full overflow-hidden border-2 border-surface-chat"
|
|
||||||
style={{
|
|
||||||
width: 56,
|
|
||||||
height: 56,
|
|
||||||
left: idx * 28,
|
|
||||||
top: idx * 12,
|
|
||||||
zIndex: 2 - idx,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar src={m.avatar} name={m.displayName ?? m.username ?? ''} size={56} user={m} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-[32px] leading-10 font-bold text-txt-primary mt-2">{groupName}</h3>
|
<h3 className="text-[32px] leading-10 font-bold text-txt-primary mt-2">{groupName}</h3>
|
||||||
<p className="text-txt-secondary text-[14px] mt-1">
|
<p className="text-txt-secondary text-[14px] mt-1">
|
||||||
This is the beginning of your group conversation.
|
This is the beginning of your group conversation.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-txt-tertiary mt-1">
|
<p className="text-xs text-txt-tertiary mt-1">
|
||||||
Group created by <strong>@{ownerName}</strong>
|
Owner:{' '}
|
||||||
|
{ownerMember ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleOwnerClick}
|
||||||
|
className="font-bold text-txt-secondary hover:text-txt-primary hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
@{ownerName}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<strong>@{ownerName}</strong>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
{hasFederated && (
|
{hasFederated && (
|
||||||
<p className="text-xs text-txt-tertiary mt-1">
|
<p className="text-xs text-txt-tertiary mt-1">
|
||||||
Messages are stored on your and your recipients' home instances. They are not end-to-end encrypted.
|
Messages are stored on your and your recipients' home instances. They are not end-to-end encrypted.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="mt-4">
|
<div className="mt-4 flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleOpenSettings}
|
||||||
|
className="px-4 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-[14px] font-medium rounded-[3px] transition-colors"
|
||||||
|
>
|
||||||
|
Open Group Settings
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleLeaveGroup}
|
onClick={handleLeaveGroup}
|
||||||
className="px-4 py-1.5 bg-surface-elevated hover:bg-surface-elevated text-[14px] font-medium text-txt-primary rounded-[3px] transition-colors"
|
className="px-4 py-1.5 bg-surface-elevated hover:bg-interactive-hover text-[14px] font-medium text-txt-primary rounded-[3px] transition-colors"
|
||||||
>
|
>
|
||||||
Leave Group
|
Leave Group
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user