Add Discord collectibles to profiles, messages, and settings.
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 1s
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 1s
Integrate profile effects, nameplates, and avatar decorations with live catalog browsing, Matrix profile storage, and rendering across user heroes, DMs, messages, and the sidebar avatar.
This commit is contained in:
@@ -29,7 +29,26 @@ export const TimelineFloat = recipe({
|
||||
},
|
||||
});
|
||||
|
||||
export type TimelineFloatVariants = RecipeVariants<typeof TimelineFloat>;
|
||||
export const DmNameplateBackground = style({
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const DmNameplateBackgroundMedia = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 'auto',
|
||||
width: toRem(280),
|
||||
maxWidth: '70%',
|
||||
height: toRem(64),
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'right top',
|
||||
opacity: 0.12,
|
||||
});
|
||||
|
||||
export const CarouselScroller = style([
|
||||
DefaultReset,
|
||||
|
||||
@@ -117,6 +117,8 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
|
||||
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
|
||||
import { useIsDirectRoom } from '../../hooks/useRoom';
|
||||
import { useOtherUserCollectibles } from '../../hooks/useUserCollectibles';
|
||||
import { pickNameplateUrl } from '../../utils/collectibleAssets';
|
||||
import { setupCopyHandler } from '../../utils/copyHandler';
|
||||
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
|
||||
import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||
@@ -570,6 +572,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const direct = useIsDirectRoom();
|
||||
const dmUserId = direct ? room.guessDMUserId() ?? undefined : undefined;
|
||||
const [timelineHovered, setTimelineHovered] = useState(false);
|
||||
const { assetUrls: dmAssetUrls } = useOtherUserCollectibles(dmUserId ?? '');
|
||||
const dmNameplateUrl = pickNameplateUrl(dmAssetUrls);
|
||||
const dmNameplateIsVideo = Boolean(
|
||||
dmAssetUrls['nameplate:animated'] || dmAssetUrls['nameplate:asset.webm']
|
||||
);
|
||||
const [hideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents');
|
||||
const [hideNickAvatarEvents] = useSetting(settingsAtom, 'hideNickAvatarEvents');
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
@@ -2530,7 +2539,33 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const atLiveBottom = atBottom && liveTimelineLinked && rangeAtEnd;
|
||||
|
||||
return (
|
||||
<Box grow="Yes" style={{ position: 'relative' }}>
|
||||
<Box
|
||||
grow="Yes"
|
||||
style={{ position: 'relative' }}
|
||||
onMouseEnter={() => setTimelineHovered(true)}
|
||||
onMouseLeave={() => setTimelineHovered(false)}
|
||||
>
|
||||
{direct && timelineHovered && dmNameplateUrl && (
|
||||
<div className={css.DmNameplateBackground} aria-hidden="true">
|
||||
{dmNameplateIsVideo ? (
|
||||
<video
|
||||
className={css.DmNameplateBackgroundMedia}
|
||||
src={dmNameplateUrl}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className={css.DmNameplateBackgroundMedia}
|
||||
src={dmNameplateUrl}
|
||||
alt=""
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Scroll ref={scrollRef} visibility="Hover" data-room-timeline-scroll="">
|
||||
<Box
|
||||
ref={timelineContentRef}
|
||||
|
||||
@@ -62,6 +62,10 @@ import colorMXID from '../../../../util/colorMXID';
|
||||
import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag';
|
||||
import { Presence, useUserPresence } from '../../../hooks/useUserPresence';
|
||||
import { useOtherUserColor } from '../../../hooks/useUserColor';
|
||||
import { useIsDirectRoom } from '../../../hooks/useRoom';
|
||||
import { useOtherUserCollectibles } from '../../../hooks/useUserCollectibles';
|
||||
import { pickAvatarDecorationUrl, pickNameplateUrl } from '../../../utils/collectibleAssets';
|
||||
import * as avatarDecorationCss from '../../../styles/AvatarDecoration.css';
|
||||
|
||||
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
|
||||
|
||||
@@ -754,8 +758,15 @@ export const Message = as<'div', MessageProps>(
|
||||
) => {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const direct = useIsDirectRoom();
|
||||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderPresence = useUserPresence(senderId);
|
||||
const { assetUrls } = useOtherUserCollectibles(senderId);
|
||||
const nameplateUrl = pickNameplateUrl(assetUrls);
|
||||
const nameplateIsVideo = Boolean(
|
||||
assetUrls['nameplate:animated'] || assetUrls['nameplate:asset.webm']
|
||||
);
|
||||
const avatarDecorationUrl = pickAvatarDecorationUrl(assetUrls);
|
||||
|
||||
const [hover, setHover] = useState(false);
|
||||
const { hoverProps } = useHover({ onHoverChange: setHover });
|
||||
@@ -796,6 +807,7 @@ export const Message = as<'div', MessageProps>(
|
||||
|
||||
// Priority: custom user color > tag color (non-legacy) > colorMXID (legacy)
|
||||
const usernameColor = customUserColor ?? (legacyUsernameColor ? colorMXID(senderId) : tagColor);
|
||||
const showNameplate = direct && hover && Boolean(nameplateUrl);
|
||||
|
||||
const headerJSX = !collapse && (
|
||||
<Box
|
||||
@@ -824,23 +836,41 @@ export const Message = as<'div', MessageProps>(
|
||||
</Username>
|
||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||
</Box>
|
||||
<Box shrink="No" gap="100">
|
||||
{messageLayout === MessageLayout.Modern && hover && (
|
||||
<>
|
||||
<Text as="span" size="T200" priority="300">
|
||||
{senderId}
|
||||
</Text>
|
||||
<Text as="span" size="T200" priority="300">
|
||||
|
|
||||
</Text>
|
||||
</>
|
||||
<Box shrink="No" className={css.MessageNameplateAnchor}>
|
||||
<Box shrink="No" gap="100" alignItems="Center">
|
||||
{messageLayout === MessageLayout.Modern && hover && (
|
||||
<>
|
||||
<Text as="span" size="T200" priority="300">
|
||||
{senderId}
|
||||
</Text>
|
||||
<Text as="span" size="T200" priority="300">
|
||||
|
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
</Box>
|
||||
{showNameplate && (
|
||||
<div className={css.MessageNameplateWrap}>
|
||||
{nameplateIsVideo ? (
|
||||
<video
|
||||
className={css.MessageNameplateOverlay}
|
||||
src={nameplateUrl}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
) : (
|
||||
<img className={css.MessageNameplateOverlay} src={nameplateUrl} alt="" draggable={false} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -857,24 +887,38 @@ export const Message = as<'div', MessageProps>(
|
||||
<AvatarBase
|
||||
className={messageLayout === MessageLayout.Bubble ? css.BubbleAvatarBase : undefined}
|
||||
>
|
||||
<Avatar
|
||||
className={classNames(css.MessageAvatar, presenceClass)}
|
||||
as="button"
|
||||
size="300"
|
||||
data-user-id={senderId}
|
||||
onClick={onUserClick}
|
||||
>
|
||||
<UserAvatar
|
||||
userId={senderId}
|
||||
src={
|
||||
senderAvatarMxc
|
||||
? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined
|
||||
: undefined
|
||||
}
|
||||
alt={senderDisplayName}
|
||||
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
<div className={css.MessageAvatarStack}>
|
||||
<Avatar
|
||||
className={classNames(
|
||||
avatarDecorationUrl ? css.MessageAvatarCircular : css.MessageAvatar,
|
||||
!avatarDecorationUrl && presenceClass
|
||||
)}
|
||||
radii={avatarDecorationUrl ? 'Pill' : undefined}
|
||||
as="button"
|
||||
size="300"
|
||||
data-user-id={senderId}
|
||||
onClick={onUserClick}
|
||||
>
|
||||
<UserAvatar
|
||||
userId={senderId}
|
||||
src={
|
||||
senderAvatarMxc
|
||||
? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined
|
||||
: undefined
|
||||
}
|
||||
alt={senderDisplayName}
|
||||
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
{avatarDecorationUrl && (
|
||||
<img
|
||||
className={avatarDecorationCss.AvatarDecorationOverlay}
|
||||
src={avatarDecorationUrl}
|
||||
alt=""
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AvatarBase>
|
||||
);
|
||||
|
||||
@@ -954,6 +998,10 @@ export const Message = as<'div', MessageProps>(
|
||||
{...focusWithinProps}
|
||||
ref={ref}
|
||||
>
|
||||
<div
|
||||
className={css.MessageContentLayer}
|
||||
data-message-nameplate-readable={showNameplate ? '' : undefined}
|
||||
>
|
||||
{!edit && (hover || !!menuAnchor || !!emojiBoardAnchor) && (
|
||||
<div className={css.MessageOptionsBase}>
|
||||
<Menu className={css.MessageOptionsBar} variant="SurfaceVariant">
|
||||
@@ -1242,6 +1290,7 @@ export const Message = as<'div', MessageProps>(
|
||||
{msgContentJSX}
|
||||
</ModernLayout>
|
||||
)}
|
||||
</div>
|
||||
</MessageBase>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
import { DefaultReset, config, toRem, color } from 'folds';
|
||||
import * as layoutCss from '../../../components/message/layout/layout.css';
|
||||
|
||||
const NAMEPLATE_READABLE_TEXT_OUTLINE = [
|
||||
`0 0 2px ${color.Surface.ContainerHover}`,
|
||||
`0 0 4px ${color.Surface.ContainerHover}`,
|
||||
`-1px -1px 0 ${color.Surface.ContainerHover}`,
|
||||
`1px -1px 0 ${color.Surface.ContainerHover}`,
|
||||
`-1px 1px 0 ${color.Surface.ContainerHover}`,
|
||||
`1px 1px 0 ${color.Surface.ContainerHover}`,
|
||||
`0 -1px 0 ${color.Surface.ContainerHover}`,
|
||||
`0 1px 0 ${color.Surface.ContainerHover}`,
|
||||
`-1px 0 0 ${color.Surface.ContainerHover}`,
|
||||
`1px 0 0 ${color.Surface.ContainerHover}`,
|
||||
].join(', ');
|
||||
|
||||
globalStyle(`[data-message-nameplate-readable] [data-message-header] button`, {
|
||||
textShadow: NAMEPLATE_READABLE_TEXT_OUTLINE,
|
||||
});
|
||||
|
||||
globalStyle(`[data-message-nameplate-readable] [data-message-header] time`, {
|
||||
textShadow: NAMEPLATE_READABLE_TEXT_OUTLINE,
|
||||
});
|
||||
|
||||
globalStyle(`[data-message-nameplate-readable] [data-message-header] span`, {
|
||||
textShadow: NAMEPLATE_READABLE_TEXT_OUTLINE,
|
||||
});
|
||||
|
||||
globalStyle(`[data-message-nameplate-readable] .${layoutCss.MessageTextBody.classNames.base}`, {
|
||||
textShadow: NAMEPLATE_READABLE_TEXT_OUTLINE,
|
||||
});
|
||||
|
||||
export const MessageBase = style({
|
||||
position: 'relative',
|
||||
@@ -51,6 +81,12 @@ export const MessageAvatar = style({
|
||||
},
|
||||
});
|
||||
|
||||
export const MessageAvatarCircular = style({
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
overflow: 'visible',
|
||||
});
|
||||
|
||||
export const MessageAvatarOnline = style({
|
||||
'::before': {
|
||||
backgroundColor: '#38842b',
|
||||
@@ -69,8 +105,41 @@ export const MessageAvatarOffline = style({
|
||||
},
|
||||
});
|
||||
|
||||
export const MessageQuickReaction = style({
|
||||
minWidth: toRem(32),
|
||||
export const MessageAvatarStack = style({
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
lineHeight: 0,
|
||||
overflow: 'visible',
|
||||
});
|
||||
|
||||
export const MessageNameplateAnchor = style({
|
||||
position: 'relative',
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
export const MessageNameplateWrap = style({
|
||||
position: 'absolute',
|
||||
top: `calc(100% + ${toRem(6)})`,
|
||||
right: 0,
|
||||
width: toRem(220),
|
||||
maxWidth: toRem(280),
|
||||
height: toRem(42),
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
export const MessageNameplateOverlay = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'right center',
|
||||
pointerEvents: 'none',
|
||||
borderRadius: toRem(6),
|
||||
});
|
||||
|
||||
export const MessageContentLayer = style({
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
});
|
||||
|
||||
export const MessageMenuGroup = style({
|
||||
|
||||
Reference in New Issue
Block a user