Skip to content

Commit e2e5f09

Browse files
authored
Merge branch 'develop' into langleyd/improve-poll-ended-ux
2 parents 6155fac + d610c3d commit e2e5f09

File tree

15 files changed

+357
-90
lines changed

15 files changed

+357
-90
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
},
8282
"dependencies": {
8383
"@babel/runtime": "^7.12.5",
84-
"@element-hq/element-web-module-api": "1.8.0",
84+
"@element-hq/element-web-module-api": "1.9.0",
8585
"@element-hq/web-shared-components": "link:packages/shared-components",
8686
"@fontsource/fira-code": "^5",
8787
"@fontsource/inter": "^5",

playwright/e2e/devtools/devtools.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,12 @@ test.describe("Devtools", () => {
2929
display: none;
3030
}`,
3131
});
32+
33+
// Try entering a value for the Developer.elementCallUrl setting
34+
const input = page.getByRole("textbox", { name: "Element Call URL" });
35+
await input.fill("https://example.com");
36+
await input.press("Enter");
37+
// expect EW NOT to reload
38+
await page.getByText("Saved").isVisible();
3239
});
3340
});

src/components/structures/RoomView.tsx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,13 @@ interface IRoomProps extends RoomViewProps {
190190
* If true, hide the widgets
191191
*/
192192
hideWidgets?: boolean;
193+
194+
/**
195+
* If true, enable sending read receipts and markers on user activity in the room view. When the user interacts with the room view, read receipts and markers are sent.
196+
* If false, the read receipts and markers are only send when the room view is focused. The user has to focus the room view in order to clear any unreads and to move the unread marker to the bottom of the view.
197+
* @default true
198+
*/
199+
enableReadReceiptsAndMarkersOnActivity?: boolean;
193200
}
194201

195202
export { MainSplitContentType };
@@ -418,6 +425,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
418425
public static contextType = SDKContext;
419426
declare public context: React.ContextType<typeof SDKContext>;
420427

428+
public static readonly defaultProps = {
429+
enableReadReceiptsAndMarkersOnActivity: true,
430+
};
431+
421432
public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
422433
super(props, context);
423434

@@ -2182,6 +2193,19 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
21822193
}
21832194
};
21842195

2196+
/**
2197+
* Handles the focus event on the RoomView component.
2198+
*
2199+
* Sends read receipts and updates the read marker if the
2200+
* disableReadReceiptsAndMarkersOnActivity prop is set.
2201+
*/
2202+
private onFocus = (): void => {
2203+
if (this.props.enableReadReceiptsAndMarkersOnActivity) return;
2204+
2205+
this.messagePanel?.sendReadReceipts();
2206+
this.messagePanel?.updateReadMarker();
2207+
};
2208+
21852209
public render(): ReactNode {
21862210
if (!this.context.client) return null;
21872211
const { isRoomEncrypted } = this.state;
@@ -2539,7 +2563,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
25392563
timelineSet={this.state.room.getUnfilteredTimelineSet()}
25402564
showReadReceipts={this.state.showReadReceipts}
25412565
manageReadReceipts={!this.state.isPeeking}
2542-
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
2566+
sendReadReceiptOnLoad={
2567+
!this.state.wasContextSwitch && this.props.enableReadReceiptsAndMarkersOnActivity
2568+
}
25432569
manageReadMarkers={!this.state.isPeeking}
25442570
hidden={hideMessagePanel}
25452571
highlightedEventId={highlightedEventId}
@@ -2556,6 +2582,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
25562582
showReactions={true}
25572583
layout={this.state.layout}
25582584
editState={this.state.editState}
2585+
enableReadReceiptsAndMarkersOnActivity={this.props.enableReadReceiptsAndMarkersOnActivity}
25592586
/>
25602587
);
25612588
}
@@ -2622,7 +2649,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
26222649
<Measured sensor={this.roomViewBody} onMeasurement={this.onMeasurement} />
26232650
{auxPanel}
26242651
{pinnedMessageBanner}
2625-
<main className={timelineClasses}>
2652+
<main className={timelineClasses} data-testid="timeline">
26262653
<FileDropTarget
26272654
parent={this.roomView.current}
26282655
onFileDrop={this.onFileDrop}
@@ -2683,7 +2710,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
26832710

26842711
return (
26852712
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
2686-
<div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
2713+
<div
2714+
className={mainClasses}
2715+
ref={this.roomView}
2716+
onKeyDown={this.onReactKeyDown}
2717+
onFocus={this.onFocus}
2718+
tabIndex={-1}
2719+
>
26872720
{showChatEffects && this.roomView.current && (
26882721
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
26892722
)}

src/components/structures/TimelinePanel.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ interface IProps {
139139

140140
hideThreadedMessages?: boolean;
141141
disableGrouping?: boolean;
142+
143+
/**
144+
* Enable updating the read receipts and markers on user activity.
145+
* @default true
146+
*/
147+
enableReadReceiptsAndMarkersOnActivity?: boolean;
142148
}
143149

144150
interface IState {
@@ -228,6 +234,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
228234
sendReadReceiptOnLoad: true,
229235
hideThreadedMessages: true,
230236
disableGrouping: false,
237+
enableReadReceiptsAndMarkersOnActivity: true,
231238
};
232239

233240
private lastRRSentEventId: string | null | undefined = undefined;
@@ -302,10 +309,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
302309

303310
this.props.timelineSet.room?.on(ThreadEvent.Update, this.onThreadUpdate);
304311

305-
if (this.props.manageReadReceipts) {
312+
if (this.props.manageReadReceipts && this.props.enableReadReceiptsAndMarkersOnActivity) {
306313
this.updateReadReceiptOnUserActivity();
307314
}
308-
if (this.props.manageReadMarkers) {
315+
if (this.props.manageReadMarkers && this.props.enableReadReceiptsAndMarkersOnActivity) {
309316
this.updateReadMarkerOnUserActivity();
310317
}
311318
this.initTimeline(this.props);
@@ -1028,7 +1035,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
10281035
);
10291036
}
10301037

1031-
private sendReadReceipts = async (): Promise<void> => {
1038+
/**
1039+
* Sends read receipts and fully read markers as appropriate.
1040+
*/
1041+
public sendReadReceipts = async (): Promise<void> => {
10321042
if (SettingsStore.getValue("lowBandwidth")) return;
10331043
if (!this.messagePanel.current) return;
10341044
if (!this.props.manageReadReceipts) return;
@@ -1134,9 +1144,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
11341144
}
11351145
}
11361146

1137-
// if the read marker is on the screen, we can now assume we've caught up to the end
1138-
// of the screen, so move the marker down to the bottom of the screen.
1139-
private updateReadMarker = async (): Promise<void> => {
1147+
/**
1148+
* Move the marker to the bottom of the screen.
1149+
* If the read marker is on the screen, we can now assume we've caught up to the end
1150+
* of the screen, so move the marker down to the bottom of the screen.
1151+
*/
1152+
public updateReadMarker = async (): Promise<void> => {
11401153
if (!this.props.manageReadMarkers) return;
11411154
if (this.getReadMarkerPosition() === 1) {
11421155
// the read marker is at an event below the viewport,

src/components/views/dialogs/DevtoolsDialog.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,16 @@ const DevtoolsDialog: React.FC<IProps> = ({ roomId, threadRootId, onFinished })
113113
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
114114
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
115115
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
116-
<SettingsField settingKey="Developer.elementCallUrl" level={SettingLevel.DEVICE} />
117116
</Form.Root>
117+
{/* The settings field needs to be outside `Form.Root` because `SettingsField` will have a inner Form,
118+
Otherwise we end up with a nester `Form` and that prohibits `preventDefault` so setting the value
119+
will reload the page.
120+
*/}
121+
<SettingsField
122+
settingKey="Developer.elementCallUrl"
123+
level={SettingLevel.DEVICE}
124+
aria-label="elementCallUrl"
125+
/>
118126
</BaseTool>
119127
);
120128
}

src/components/views/rooms/BasicMessageComposer.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
482482

483483
private onKeyDown = (event: React.KeyboardEvent): void => {
484484
if (!this.editorRef.current) return;
485+
// Ignore any keypress while doing IME compositions to prevent cursor position issues
486+
// This matches the behavior in SendMessageComposer and EditMessageComposer
487+
if (this.isComposing(event)) {
488+
return;
489+
}
485490
if (this.isSafari && event.which == 229) {
486491
// Swallow the extra keyDown by Safari
487492
event.stopPropagation();

src/settings/watchers/ThemeWatcher.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ export default class ThemeWatcher extends TypedEventEmitter<ThemeWatcherEvent, T
122122
return theme;
123123
}
124124
}
125-
logger.log("returning theme value");
126125
return SettingsStore.getValue("theme");
127126
}
128127

test/unit-tests/components/structures/RoomView-test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,54 @@ describe("RoomView", () => {
335335
expect(asFragment()).toMatchSnapshot();
336336
});
337337

338+
describe("enableReadReceiptsAndMarkersOnActivity", () => {
339+
it.each([
340+
{
341+
enabled: false,
342+
testName: "should send read receipts and update read marker on focus when disabled",
343+
checkCall: (sendReadReceiptsSpy: jest.Mock, updateReadMarkerSpy: jest.Mock) => {
344+
expect(sendReadReceiptsSpy).toHaveBeenCalled();
345+
expect(updateReadMarkerSpy).toHaveBeenCalled();
346+
},
347+
},
348+
{
349+
enabled: true,
350+
testName: "should not send read receipts and update read marker on focus when enabled",
351+
checkCall: (sendReadReceiptsSpy: jest.Mock, updateReadMarkerSpy: jest.Mock) => {
352+
expect(sendReadReceiptsSpy).not.toHaveBeenCalled();
353+
expect(updateReadMarkerSpy).not.toHaveBeenCalled();
354+
},
355+
},
356+
])("$testName", async ({ enabled, checkCall }) => {
357+
// Join the room
358+
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
359+
const ref = createRef<RoomView>();
360+
await mountRoomView(ref, {
361+
enableReadReceiptsAndMarkersOnActivity: enabled,
362+
});
363+
364+
// Wait for the timeline to be rendered
365+
await waitFor(() => expect(screen.getByTestId("timeline")).not.toBeNull());
366+
367+
// Get the RoomView instance and mock the messagePanel methods
368+
const instance = ref.current!;
369+
const sendReadReceiptsSpy = jest.fn();
370+
const updateReadMarkerSpy = jest.fn();
371+
// @ts-ignore - accessing private property for testing
372+
instance.messagePanel = {
373+
sendReadReceipts: sendReadReceiptsSpy,
374+
updateReadMarker: updateReadMarkerSpy,
375+
};
376+
377+
// Find the main RoomView div and trigger focus
378+
const timeline = screen.getByTestId("timeline");
379+
fireEvent.focus(timeline);
380+
381+
// Verify that sendReadReceipts and updateReadMarker were called or not based on the enabled state
382+
checkCall(sendReadReceiptsSpy, updateReadMarkerSpy);
383+
});
384+
});
385+
338386
describe("invites", () => {
339387
beforeEach(() => {
340388
const member = new RoomMember(room.roomId, cli.getSafeUserId());

test/unit-tests/components/structures/TimelinePanel-test.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { Action } from "../../../../src/dispatcher/actions";
5151
import { SettingLevel } from "../../../../src/settings/SettingLevel";
5252
import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController";
5353
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
54+
import type Timer from "../../../../src/utils/Timer";
5455

5556
// ScrollPanel calls this, but jsdom doesn't mock it for us
5657
HTMLDivElement.prototype.scrollBy = () => {};
@@ -369,6 +370,56 @@ describe("TimelinePanel", () => {
369370
});
370371
});
371372

373+
describe("enableReadReceiptsAndMarkersOnActivity", () => {
374+
it.each([
375+
{
376+
enabled: false,
377+
testName: "should not set up activity timers when disabled",
378+
checkCall: (readReceiptTimer: Timer | null, readMarkerTimer: Timer | null) => {
379+
expect(readReceiptTimer).toBeNull();
380+
expect(readMarkerTimer).toBeNull();
381+
},
382+
},
383+
{
384+
enabled: true,
385+
testName: "should set up activity timers when enabled",
386+
checkCall: (readReceiptTimer: Timer | null, readMarkerTimer: Timer | null) => {
387+
expect(readReceiptTimer).toBeTruthy();
388+
expect(readMarkerTimer).toBeTruthy();
389+
},
390+
},
391+
])("$testName", async ({ enabled, checkCall }) => {
392+
const room = mkRoom(client, "roomId");
393+
const events = mockEvents(room);
394+
const [, timelineSet] = mkTimeline(room, events);
395+
396+
let timelinePanel: TimelinePanel | null = null;
397+
398+
render(
399+
<TimelinePanel
400+
timelineSet={timelineSet}
401+
manageReadMarkers={true}
402+
manageReadReceipts={true}
403+
enableReadReceiptsAndMarkersOnActivity={enabled}
404+
ref={(ref) => {
405+
timelinePanel = ref;
406+
}}
407+
/>,
408+
clientAndSDKContextRenderOptions(client, sdkContext),
409+
);
410+
411+
await waitFor(() => expect(timelinePanel).toBeTruthy());
412+
413+
// Check if the activity timers were set up
414+
// @ts-ignore - accessing private property for testing
415+
const readReceiptTimer = timelinePanel!.readReceiptActivityTimer;
416+
// @ts-ignore - accessing private property for testing
417+
const readMarkerTimer = timelinePanel!.readMarkerActivityTimer;
418+
419+
checkCall(readReceiptTimer, readMarkerTimer);
420+
});
421+
});
422+
372423
it("should scroll event into view when props.eventId changes", () => {
373424
const client = MatrixClientPeg.safeGet();
374425
const room = mkRoom(client, "roomId");

0 commit comments

Comments
 (0)