Add Shared Media drawer and harden same-room navigation.
Widen the media panel, support skinny full-page mode, keep jump-to-latest and return-to-previous reliable, and stop same-room event jumps from remounting the outlet or yanking the sidebar.
This commit is contained in:
@@ -1,16 +1,53 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
|
||||
const decodeSegment = (segment: string): string => {
|
||||
let decoded = segment;
|
||||
try {
|
||||
let next = decodeURIComponent(decoded);
|
||||
while (next !== decoded) {
|
||||
decoded = next;
|
||||
next = decodeURIComponent(decoded);
|
||||
}
|
||||
} catch {
|
||||
// keep partially decoded value
|
||||
}
|
||||
return decoded;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for Outlet that adds route-based animation
|
||||
* Forces remount on route change by using location as key
|
||||
* Room routes are `:roomIdOrAlias/:eventId?/`. Jumping to an event (or clearing it)
|
||||
* changes the pathname but not the room — keep the outlet mounted so we don't replay
|
||||
* the route enter animation or remount drawers like Shared Media.
|
||||
*
|
||||
* Path segments are decoded so `!room:server` and `%21room%3Aserver` share a key.
|
||||
*/
|
||||
const getOutletTransitionKey = (pathname: string): string => {
|
||||
const segments = pathname.split('/').filter(Boolean).map(decodeSegment);
|
||||
if (segments.length === 0) return pathname;
|
||||
|
||||
// Matrix event IDs start with `$`
|
||||
if (segments[segments.length - 1].startsWith('$')) {
|
||||
segments.pop();
|
||||
}
|
||||
|
||||
return `/${segments.join('/')}/`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for Outlet that adds route-based animation.
|
||||
* Remounts (and animates) when leaving a room / switching rooms, not on same-room event hops.
|
||||
*/
|
||||
export function AnimatedOutlet() {
|
||||
const location = useLocation();
|
||||
|
||||
const transitionKey = useMemo(
|
||||
() => getOutletTransitionKey(location.pathname),
|
||||
[location.pathname]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={location.pathname}
|
||||
key={transitionKey}
|
||||
data-route-transition="true"
|
||||
style={{
|
||||
flex: 1,
|
||||
|
||||
@@ -256,7 +256,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
||||
className={css.EditorTextareaScroll}
|
||||
variant="SurfaceVariant"
|
||||
style={scrollStyle}
|
||||
size="300"
|
||||
size="0"
|
||||
visibility="Hover"
|
||||
hideTrack
|
||||
ref={editableRef}
|
||||
|
||||
@@ -53,6 +53,17 @@ export const Reaction = style([
|
||||
},
|
||||
]);
|
||||
|
||||
export const ReactionText = style([
|
||||
DefaultReset,
|
||||
{
|
||||
minWidth: 0,
|
||||
maxWidth: toRem(150),
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
lineHeight: toRem(20),
|
||||
},
|
||||
]);
|
||||
|
||||
export const ReactionStack = style([
|
||||
DefaultReset,
|
||||
{
|
||||
@@ -66,7 +77,7 @@ export const ReactionStack = style([
|
||||
},
|
||||
]);
|
||||
|
||||
export const ReactionText = style([
|
||||
export const ReactionSticker = style([
|
||||
DefaultReset,
|
||||
{
|
||||
minWidth: 0,
|
||||
@@ -77,11 +88,6 @@ export const ReactionText = style([
|
||||
lineHeight: toRem(20),
|
||||
gridArea: '1 / 1',
|
||||
transformOrigin: 'center center',
|
||||
// Fan each stacked copy so count>1 reads as multiple stickers
|
||||
transform: `translate(
|
||||
calc(var(--sticker-i, 0) * 7px - 2px),
|
||||
calc(var(--sticker-i, 0) * -5px)
|
||||
) rotate(calc((var(--sticker-i, 0) - 1) * 12deg))`,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as css from './Reaction.css';
|
||||
import { getHexcodeForEmoji, getShortcodeFor } from '../../plugins/emoji';
|
||||
import { getMemberDisplayName } from '../../utils/room';
|
||||
import { eventWithShortcode, getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { isStationeryTheme, useTheme } from '../../hooks/useTheme';
|
||||
|
||||
const MAX_STICKER_STACK = 4;
|
||||
|
||||
@@ -18,13 +19,48 @@ export const Reaction = as<
|
||||
useAuthentication?: boolean;
|
||||
}
|
||||
>(({ className, mx, count, reaction, useAuthentication, ...props }, ref) => {
|
||||
const stackCount = Math.min(Math.max(count, 1), MAX_STICKER_STACK);
|
||||
const showCount = count > 2;
|
||||
const theme = useTheme();
|
||||
const stationery = isStationeryTheme(theme);
|
||||
const isCustomEmoji = reaction.startsWith('mxc://');
|
||||
const customSrc = isCustomEmoji
|
||||
? mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction
|
||||
: undefined;
|
||||
|
||||
const emoji = isCustomEmoji ? (
|
||||
<img className={css.ReactionImg} src={customSrc} alt={reaction} />
|
||||
) : (
|
||||
<Text as="span" size="Inherit" truncate>
|
||||
{reaction}
|
||||
</Text>
|
||||
);
|
||||
|
||||
// Stationery: fanned sticker stack. Everywhere else: compact emoji + count.
|
||||
if (!stationery) {
|
||||
return (
|
||||
<Box
|
||||
as="button"
|
||||
className={classNames(css.Reaction, className)}
|
||||
alignItems="Center"
|
||||
shrink="No"
|
||||
gap="200"
|
||||
data-reaction=""
|
||||
aria-label={`${reaction}, ${count}`}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<Text className={css.ReactionText} as="span" size="T400">
|
||||
{emoji}
|
||||
</Text>
|
||||
<Text as="span" size="T300" data-reaction-count="">
|
||||
{count}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const stackCount = Math.min(Math.max(count, 1), MAX_STICKER_STACK);
|
||||
const showCount = count > 2;
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="button"
|
||||
@@ -40,12 +76,11 @@ export const Reaction = as<
|
||||
>
|
||||
<span className={css.ReactionStack} data-reaction-stack-layer="">
|
||||
{Array.from({ length: stackCount }, (_, i) => {
|
||||
// Draw back-to-front so the top sticker is the last layer
|
||||
const layer = stackCount - 1 - i;
|
||||
return (
|
||||
<Text
|
||||
key={layer}
|
||||
className={css.ReactionText}
|
||||
className={css.ReactionSticker}
|
||||
as="span"
|
||||
size="T400"
|
||||
data-reaction-sticker=""
|
||||
|
||||
@@ -242,6 +242,7 @@ export const MessageTextBody = recipe({
|
||||
lineHeight: 1,
|
||||
overflow: 'visible',
|
||||
overflowY: 'visible',
|
||||
paddingBottom: config.space.S200,
|
||||
},
|
||||
},
|
||||
emote: {
|
||||
|
||||
Reference in New Issue
Block a user