Fix timeline jumping issues related to bubble layout (#7529)

This commit is contained in:
Michael Telatynski 2022-01-18 09:31:21 +00:00 committed by GitHub
parent 8ced6e6117
commit 4b5ca1d7a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 57 additions and 34 deletions

View file

@ -215,13 +215,23 @@ limitations under the License.
} }
//noinspection CssReplaceWithShorthandSafely //noinspection CssReplaceWithShorthandSafely
.mx_MImageBody .mx_MImageBody_thumbnail { .mx_MImageBody {
width: 100%;
height: 100%;
//noinspection CssReplaceWithShorthandSafely
.mx_MImageBody_thumbnail {
// Note: This is intentionally not compressed because the browser gets confused // Note: This is intentionally not compressed because the browser gets confused
// when it is all combined. We're effectively unsetting the border radius then // when it is all combined. We're effectively unsetting the border radius then
// setting the two corners we care about manually. // setting the two corners we care about manually.
border-radius: unset; border-radius: unset;
border-top-left-radius: var(--cornerRadius); border-top-left-radius: var(--cornerRadius);
border-top-right-radius: var(--cornerRadius); border-top-right-radius: var(--cornerRadius);
&.mx_MImageBody_thumbnail--blurhash {
position: unset;
}
}
} }
.mx_EventTile_e2eIcon { .mx_EventTile_e2eIcon {
@ -434,7 +444,6 @@ limitations under the License.
} }
&[aria-expanded=true] { &[aria-expanded=true] {
text-align: right; text-align: right;
margin-right: 100px;
} }
} }
@ -457,11 +466,26 @@ limitations under the License.
padding: 0 49px; padding: 0 49px;
} }
// ideally we'd use display=contents here for the layout to all work regardless of the *ELS but
// that breaks ScrollPanel's reliance upon offsetTop so we have to have a bit more finesse.
.mx_EventListSummary[data-expanded=true][data-layout=bubble] { .mx_EventListSummary[data-expanded=true][data-layout=bubble] {
display: contents; margin: 0;
.mx_EventTile { .mx_EventTile {
padding: 2px 0; padding: 2px 0;
margin-right: 0;
.mx_MessageActionBar {
right: 127px; // align with that of right-column bubbles
}
.mx_EventTile_readAvatars {
right: -18px; // match alignment to RRs of chat bubbles
}
&::before {
right: 0; // match alignment of the hover background to that of chat bubbles
}
} }
} }

View file

@ -89,7 +89,7 @@ interface IProps {
* The promise should resolve to true if there is more data to be * The promise should resolve to true if there is more data to be
* retrieved in this direction (in which case onFillRequest may be * retrieved in this direction (in which case onFillRequest may be
* called again immediately), or false if there is no more data in this * called again immediately), or false if there is no more data in this
* directon (at this time) - which will stop the pagination cycle until * direction (at this time) - which will stop the pagination cycle until
* the user scrolls again. * the user scrolls again.
*/ */
onFillRequest?(backwards: boolean): Promise<boolean>; onFillRequest?(backwards: boolean): Promise<boolean>;
@ -683,7 +683,7 @@ export default class ScrollPanel extends React.Component<IProps> {
return; return;
} }
const scrollToken = node.dataset.scrollTokens.split(',')[0]; const scrollToken = node.dataset.scrollTokens.split(',')[0];
debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken); debuglog("saving anchored scroll state to message", node.innerText, scrollToken);
const bottomOffset = this.topFromBottom(node); const bottomOffset = this.topFromBottom(node);
this.scrollState = { this.scrollState = {
stuckAtBottom: false, stuckAtBottom: false,
@ -791,17 +791,16 @@ export default class ScrollPanel extends React.Component<IProps> {
const scrollState = this.scrollState; const scrollState = this.scrollState;
const trackedNode = scrollState.trackedNode; const trackedNode = scrollState.trackedNode;
if (!trackedNode || !trackedNode.parentElement) { if (!trackedNode?.parentElement) {
let node; let node: HTMLElement;
const messages = this.itemlist.current.children; const messages = this.itemlist.current.children;
const scrollToken = scrollState.trackedScrollToken; const scrollToken = scrollState.trackedScrollToken;
for (let i = messages.length-1; i >= 0; --i) { for (let i = messages.length - 1; i >= 0; --i) {
const m = messages[i] as HTMLElement; const m = messages[i] as HTMLElement;
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens // 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
// There might only be one scroll token // There might only be one scroll token
if (m.dataset.scrollTokens && if (m.dataset.scrollTokens?.split(',').includes(scrollToken)) {
m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) {
node = m; node = m;
break; break;
} }

View file

@ -63,7 +63,7 @@ const DEBUG = false;
let debuglog = function(...s: any[]) {}; let debuglog = function(...s: any[]) {};
if (DEBUG) { if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console // using bind means that we get to keep useful line numbers in the console
debuglog = logger.log.bind(console); debuglog = logger.log.bind(console, "TimelinePanel debuglog:");
} }
interface IProps { interface IProps {
@ -240,7 +240,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
debuglog("TimelinePanel: mounting"); debuglog("mounting");
// XXX: we could track RM per TimelineSet rather than per Room. // XXX: we could track RM per TimelineSet rather than per Room.
// but for now we just do it per room for simplicity. // but for now we just do it per room for simplicity.
@ -369,7 +369,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
private onMessageListUnfillRequest = (backwards: boolean, scrollToken: string): void => { private onMessageListUnfillRequest = (backwards: boolean, scrollToken: string): void => {
// If backwards, unpaginate from the back (i.e. the start of the timeline) // If backwards, unpaginate from the back (i.e. the start of the timeline)
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
debuglog("TimelinePanel: unpaginating events in direction", dir); debuglog("unpaginating events in direction", dir);
// All tiles are inserted by MessagePanel to have a scrollToken === eventId, and // All tiles are inserted by MessagePanel to have a scrollToken === eventId, and
// this particular event should be the first or last to be unpaginated. // this particular event should be the first or last to be unpaginated.
@ -384,7 +384,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
const count = backwards ? marker + 1 : this.state.events.length - marker; const count = backwards ? marker + 1 : this.state.events.length - marker;
if (count > 0) { if (count > 0) {
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir); debuglog("Unpaginating", count, "in direction", dir);
this.timelineWindow.unpaginate(count, backwards); this.timelineWindow.unpaginate(count, backwards);
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents(); const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
@ -425,28 +425,28 @@ class TimelinePanel extends React.Component<IProps, IState> {
const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating'; const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
if (!this.state[canPaginateKey]) { if (!this.state[canPaginateKey]) {
debuglog("TimelinePanel: have given up", dir, "paginating this timeline"); debuglog("have given up", dir, "paginating this timeline");
return Promise.resolve(false); return Promise.resolve(false);
} }
if (!this.timelineWindow.canPaginate(dir)) { if (!this.timelineWindow.canPaginate(dir)) {
debuglog("TimelinePanel: can't", dir, "paginate any further"); debuglog("can't", dir, "paginate any further");
this.setState<null>({ [canPaginateKey]: false }); this.setState<null>({ [canPaginateKey]: false });
return Promise.resolve(false); return Promise.resolve(false);
} }
if (backwards && this.state.firstVisibleEventIndex !== 0) { if (backwards && this.state.firstVisibleEventIndex !== 0) {
debuglog("TimelinePanel: won't", dir, "paginate past first visible event"); debuglog("won't", dir, "paginate past first visible event");
return Promise.resolve(false); return Promise.resolve(false);
} }
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards); debuglog("Initiating paginate; backwards:"+backwards);
this.setState<null>({ [paginatingKey]: true }); this.setState<null>({ [paginatingKey]: true });
return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then((r) => { return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then((r) => {
if (this.unmounted) { return; } if (this.unmounted) { return; }
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r); debuglog("paginate complete backwards:"+backwards+"; success:"+r);
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents(); const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
const newState: Partial<IState> = { const newState: Partial<IState> = {
@ -463,7 +463,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate'; const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
if (!this.state[canPaginateOtherWayKey] && if (!this.state[canPaginateOtherWayKey] &&
this.timelineWindow.canPaginate(otherDirection)) { this.timelineWindow.canPaginate(otherDirection)) {
debuglog('TimelinePanel: can now', otherDirection, 'paginate again'); debuglog('can now', otherDirection, 'paginate again');
newState[canPaginateOtherWayKey] = true; newState[canPaginateOtherWayKey] = true;
} }
@ -833,7 +833,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
const roomId = this.props.timelineSet.room.roomId; const roomId = this.props.timelineSet.room.roomId;
const hiddenRR = SettingsStore.getValue("feature_hidden_read_receipts", roomId); const hiddenRR = SettingsStore.getValue("feature_hidden_read_receipts", roomId);
debuglog('TimelinePanel: Sending Read Markers for ', debuglog('Sending Read Markers for ',
this.props.timelineSet.room.roomId, this.props.timelineSet.room.roomId,
'rm', this.state.readMarkerEventId, 'rm', this.state.readMarkerEventId,
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '', lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',

View file

@ -338,8 +338,8 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
content: IMediaEventContent, content: IMediaEventContent,
forcedHeight?: number, forcedHeight?: number,
): JSX.Element { ): JSX.Element {
let infoWidth; let infoWidth: number;
let infoHeight; let infoHeight: number;
if (content && content.info && content.info.w && content.info.h) { if (content && content.info && content.info.w && content.info.h) {
infoWidth = content.info.w; infoWidth = content.info.w;
@ -382,8 +382,8 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
const suggestedAndPossibleHeight = Math.min(suggestedImageSize(imageSize, isPortrait).h, infoHeight); const suggestedAndPossibleHeight = Math.min(suggestedImageSize(imageSize, isPortrait).h, infoHeight);
const aspectRatio = infoWidth / infoHeight; const aspectRatio = infoWidth / infoHeight;
let maxWidth; let maxWidth: number;
let maxHeight; let maxHeight: number;
const maxHeightConstraint = forcedHeight || this.props.maxImageHeight || suggestedAndPossibleHeight; const maxHeightConstraint = forcedHeight || this.props.maxImageHeight || suggestedAndPossibleHeight;
if (maxHeightConstraint * aspectRatio < suggestedAndPossibleWidth || imageSize === ImageSize.Large) { if (maxHeightConstraint * aspectRatio < suggestedAndPossibleWidth || imageSize === ImageSize.Large) {
// The width is dictated by the maximum height that was defined by the props or the function param `forcedHeight` // The width is dictated by the maximum height that was defined by the props or the function param `forcedHeight`
@ -451,7 +451,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
// This has incredibly broken types. // This has incredibly broken types.
const C = CSSTransition as any; const C = CSSTransition as any;
const thumbnail = ( const thumbnail = (
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight, maxWidth: maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}> <div className="mx_MImageBody_thumbnail_container" style={{ maxHeight, maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}>
<SwitchTransition mode="out-in"> <SwitchTransition mode="out-in">
<C <C
classNames="mx_rtg--fade" classNames="mx_rtg--fade"
@ -464,8 +464,8 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
className={classes} className={classes}
style={{ style={{
// Constrain width here so that spinner appears central to the loaded thumbnail // Constrain width here so that spinner appears central to the loaded thumbnail
maxWidth: `min(100%, ${infoWidth}px)`, maxWidth,
maxHeight: maxHeight, maxHeight,
aspectRatio: `${infoWidth}/${infoHeight}`, aspectRatio: `${infoWidth}/${infoHeight}`,
}} }}
> >