Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ddcda122a |
@@ -297,7 +297,7 @@ export function AccountDataEditor({
|
||||
</Chip>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton type="button" onClick={requestClose} variant="Surface" aria-label="Close">
|
||||
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -73,7 +73,7 @@ import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { useCallPreferences } from '../state/hooks/callPreferences';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { getStateEvent, getMemberName, isFromFilteredTimelineSet } from '../utils/room';
|
||||
import { getStateEvent, getMemberName } from '../utils/room';
|
||||
import { declinedRing, hasActiveCallMembership, isRingableRoom } from '../utils/callRing';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels';
|
||||
@@ -496,7 +496,6 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
||||
|
||||
const handleTimelineEvent: EventTimelineSetHandlerMap[RoomEvent.Timeline] = useCallback(
|
||||
async (event, room, toStartOfTimeline, removed, data) => {
|
||||
if (isFromFilteredTimelineSet(data)) return; // [Gitea #259]
|
||||
// only process rtc notification reference events.
|
||||
// we do not want to wait to decrypt all events.
|
||||
if (event.getRelation()?.rel_type !== RelationType.Reference) return;
|
||||
@@ -684,7 +683,6 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
||||
data,
|
||||
) => {
|
||||
if (eventRoom?.roomId !== room.roomId || !data.liveEvent) return;
|
||||
if (isFromFilteredTimelineSet(data)) return; // [Gitea #259]
|
||||
if (event.getType() === EventType.RTCDecline && event.getSender() === myUserId) {
|
||||
const related = event.getRelation()?.event_id;
|
||||
if (!related || related === refEventId) dismiss(); // declined elsewhere
|
||||
|
||||
@@ -315,7 +315,7 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
|
||||
Setup Device Verification
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton type="button" size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
@@ -361,7 +361,7 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
|
||||
Reset Device Verification
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton type="button" size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -275,7 +275,7 @@ export function StateEventEditor({ type, stateKey, requestClose }: StateEventEdi
|
||||
</Chip>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton type="button" onClick={requestClose} variant="Surface" aria-label="Close">
|
||||
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -143,7 +143,7 @@ export function PollCreator({ roomId, threadRootId, onClose }: PollCreatorProps)
|
||||
Create Poll
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton type="button" size="300" radii="300" onClick={onClose} aria-label="Close">
|
||||
<IconButton size="300" radii="300" onClick={onClose} aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -118,7 +118,7 @@ export function ReportRoomModal({ roomId, onClose }: ReportRoomModalProps) {
|
||||
Report Room
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton type="button" size="300" onClick={onClose} radii="300" aria-label="Close">
|
||||
<IconButton size="300" onClick={onClose} radii="300" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -124,7 +124,7 @@ export function ReportUserModal({ userId, onClose }: ReportUserModalProps) {
|
||||
Report User
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton type="button" size="300" onClick={onClose} radii="300" aria-label="Close">
|
||||
<IconButton size="300" onClick={onClose} radii="300" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -90,7 +90,6 @@ import {
|
||||
isMembershipChanged,
|
||||
reactionOrEditEvent,
|
||||
sendRoomEvent,
|
||||
isFromFilteredTimelineSet,
|
||||
} from '../../utils/room';
|
||||
import { getLastEditDiff } from '../../utils/editDiff';
|
||||
import { tick } from '../../utils/haptics';
|
||||
@@ -411,9 +410,6 @@ const useLiveEventArrive = (room: Room, onArrive: (mEvent: MatrixEvent) => void)
|
||||
data,
|
||||
) => {
|
||||
if (eventRoom?.roomId !== room.roomId || !data.liveEvent) return;
|
||||
// [Gitea #259] The media gallery / activity log register filtered timeline
|
||||
// sets whose events the room re-emits as live: don't count them twice.
|
||||
if (isFromFilteredTimelineSet(data)) return;
|
||||
onArrive(mEvent);
|
||||
};
|
||||
const handleRedaction: RoomEventHandlerMap[RoomEvent.Redaction] = (mEvent, eventRoom) => {
|
||||
|
||||
@@ -202,7 +202,7 @@ export function ScheduleMessageModal({
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton type="button" size="300" radii="300" onClick={onClose} aria-label="Close">
|
||||
<IconButton size="300" radii="300" onClick={onClose} aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -725,13 +725,7 @@ export const MessageDeleteItem = as<
|
||||
Delete Message
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton
|
||||
type="button"
|
||||
size="300"
|
||||
onClick={handleClose}
|
||||
radii="300"
|
||||
aria-label="Close"
|
||||
>
|
||||
<IconButton size="300" onClick={handleClose} radii="300" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
@@ -871,13 +865,7 @@ export const MessageReportItem = as<
|
||||
Report Message
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton
|
||||
type="button"
|
||||
size="300"
|
||||
onClick={handleClose}
|
||||
radii="300"
|
||||
aria-label="Close"
|
||||
>
|
||||
<IconButton size="300" onClick={handleClose} radii="300" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -14,7 +14,6 @@ import { GeneralWidgetDriver } from './GeneralWidgetDriver';
|
||||
import { WidgetPermissionRequest, isWidgetUrlSafe } from './widgetUtils';
|
||||
import { restrictWidgetMessages } from '../../../plugins/widgetTransport';
|
||||
import { WidgetPermissionPrompt } from './WidgetPermissionPrompt';
|
||||
import { isFromFilteredTimelineSet } from '../../../utils/room';
|
||||
|
||||
type PendingAsk = {
|
||||
requests: WidgetPermissionRequest[];
|
||||
@@ -112,7 +111,6 @@ export function RoomWidgetView({ room, widget }: RoomWidgetViewProps) {
|
||||
};
|
||||
const onTimeline: RoomEventHandlerMap[RoomEvent.Timeline] = (ev, r, toStart, removed, data) => {
|
||||
if (toStart || removed || !data?.liveEvent) return;
|
||||
if (isFromFilteredTimelineSet(data)) return; // [Gitea #259] don't feed widgets each event twice
|
||||
if (ev.isBeingDecrypted()) ev.once(MatrixEventEvent.Decrypted, feed);
|
||||
else feed(ev);
|
||||
};
|
||||
|
||||
@@ -49,7 +49,6 @@ import {
|
||||
getNotificationType,
|
||||
getUnreadInfo,
|
||||
isNotificationEvent,
|
||||
isFromFilteredTimelineSet,
|
||||
} from '../../utils/room';
|
||||
import { NotificationType } from '../../../types/matrix/room';
|
||||
import { mxcUrlToHttp } from '../../utils/matrix';
|
||||
@@ -685,7 +684,6 @@ function MessageNotifications() {
|
||||
data,
|
||||
) => {
|
||||
if (mx.getSyncState() !== 'SYNCING') return;
|
||||
if (isFromFilteredTimelineSet(data)) return; // [Gitea #259] no duplicate notifications
|
||||
if (document.hasFocus() && (selectedRoomId === room?.roomId || notificationSelected)) return;
|
||||
if (
|
||||
!room ||
|
||||
@@ -1128,7 +1126,6 @@ function ClockSkewFeature() {
|
||||
data,
|
||||
) => {
|
||||
if (!data.liveEvent) return;
|
||||
if (isFromFilteredTimelineSet(data)) return; // [Gitea #259]
|
||||
// Only events our homeserver stamped: a federated event's
|
||||
// origin_server_ts is the other server's clock.
|
||||
const senderServer = mEvent.getSender()?.split(':').slice(1).join(':');
|
||||
|
||||
@@ -28,7 +28,10 @@ import {
|
||||
} from 'matrix-js-sdk';
|
||||
import { getCallCapabilities } from './utils';
|
||||
import { downloadMedia, mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { dedupeInFlight } from '../../utils/inFlight';
|
||||
|
||||
/** A little longer than Element Call's own 2 s timeout for a restart. */
|
||||
const RESTART_TIMEOUT_MS = 5000;
|
||||
export class CallWidgetDriver extends WidgetDriver {
|
||||
private allowedCapabilities: Set<Capability>;
|
||||
|
||||
@@ -129,8 +132,18 @@ export class CallWidgetDriver extends WidgetDriver {
|
||||
await this.mx._unstable_cancelScheduledDelayedEvent(delayId);
|
||||
}
|
||||
|
||||
// [Gitea #318] While the homeserver is stalled, Element Call times out each
|
||||
// restart locally after 2 s and asks again, so requests for the same delay
|
||||
// id piled up and hit the server together on recovery (404s, then 429s).
|
||||
// Share one in-flight restart per id, and give it a timeout of its own.
|
||||
private readonly restartOnce = dedupeInFlight<unknown>();
|
||||
|
||||
public async restartScheduledDelayedEvent(delayId: string): Promise<void> {
|
||||
await this.mx._unstable_restartScheduledDelayedEvent(delayId);
|
||||
await this.restartOnce(delayId, () =>
|
||||
this.mx._unstable_restartScheduledDelayedEvent(delayId, {
|
||||
localTimeoutMs: RESTART_TIMEOUT_MS,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async sendScheduledDelayedEvent(delayId: string): Promise<void> {
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
getUnreadInfo,
|
||||
getUnreadInfos,
|
||||
isNotificationEvent,
|
||||
isFromFilteredTimelineSet,
|
||||
} from '../../utils/room';
|
||||
import { roomToParentsAtom } from './roomToParents';
|
||||
import { useStateEventCallback } from '../../hooks/useStateEventCallback';
|
||||
@@ -237,7 +236,6 @@ export const useBindRoomToUnreadAtom = (mx: MatrixClient, unreadAtom: typeof roo
|
||||
data: IRoomTimelineData,
|
||||
) => {
|
||||
if (!room || !data.liveEvent || room.isSpaceRoom() || !isNotificationEvent(mEvent)) return;
|
||||
if (isFromFilteredTimelineSet(data)) return; // [Gitea #259]
|
||||
// Single-owner rule: thread replies drive the room badge via
|
||||
// RoomEvent.UnreadNotifications below — ignore them here so the count is
|
||||
// never double-driven / mis-attributed to the main timeline.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import type { IRoomTimelineData } from 'matrix-js-sdk';
|
||||
import { isFromFilteredTimelineSet } from './room';
|
||||
|
||||
const data = (filter: unknown): IRoomTimelineData =>
|
||||
({
|
||||
liveEvent: true,
|
||||
timeline: { getTimelineSet: () => ({ getFilter: () => filter }) },
|
||||
}) as unknown as IRoomTimelineData;
|
||||
|
||||
test('isFromFilteredTimelineSet: gallery/activity-log sets are filtered, the main set is not (#259)', () => {
|
||||
assert.equal(isFromFilteredTimelineSet(data({ filterId: 'media' })), true);
|
||||
assert.equal(isFromFilteredTimelineSet(data(undefined)), false);
|
||||
assert.equal(isFromFilteredTimelineSet(undefined), false);
|
||||
assert.equal(
|
||||
isFromFilteredTimelineSet({ liveEvent: true } as unknown as IRoomTimelineData),
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { dedupeInFlight } from './inFlight';
|
||||
|
||||
test('dedupeInFlight: one request per key while pending, a new one after (#318)', async () => {
|
||||
const once = dedupeInFlight<number>();
|
||||
let calls = 0;
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>((r) => {
|
||||
release = r;
|
||||
});
|
||||
const run = async () => {
|
||||
calls += 1;
|
||||
await gate;
|
||||
return calls;
|
||||
};
|
||||
const a = once('d1', run);
|
||||
const b = once('d1', run);
|
||||
const c = once('d2', run);
|
||||
assert.equal(a, b);
|
||||
release();
|
||||
assert.deepEqual(await Promise.all([a, b, c]), [2, 2, 2]);
|
||||
assert.equal(calls, 2);
|
||||
await once('d1', run);
|
||||
assert.equal(calls, 3, 'settled requests are not reused');
|
||||
});
|
||||
|
||||
test('dedupeInFlight: a rejection clears the key', async () => {
|
||||
const once = dedupeInFlight<void>();
|
||||
await assert.rejects(once('k', () => Promise.reject(new Error('boom'))));
|
||||
await once('k', () => Promise.resolve());
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Share one in-flight promise per key: callers that ask again while a request
|
||||
* for the same key is still pending get that request instead of a new one.
|
||||
*/
|
||||
export function dedupeInFlight<T>(): (key: string, run: () => Promise<T>) => Promise<T> {
|
||||
const pending = new Map<string, Promise<T>>();
|
||||
return (key, run) => {
|
||||
const existing = pending.get(key);
|
||||
if (existing) return existing;
|
||||
const p = run().finally(() => pending.delete(key));
|
||||
pending.set(key, p);
|
||||
return p;
|
||||
};
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
Room,
|
||||
RoomMember,
|
||||
TimelineEvents,
|
||||
IRoomTimelineData,
|
||||
} from 'matrix-js-sdk';
|
||||
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
|
||||
import type { RoomMessageEventContent } from 'matrix-js-sdk/lib/@types/events';
|
||||
@@ -848,14 +847,3 @@ export const guessPerfectParent = (
|
||||
|
||||
return perfectParent;
|
||||
};
|
||||
|
||||
/**
|
||||
* [Gitea #259] Whether a RoomEvent.Timeline emit came from a FILTERED timeline
|
||||
* set. The media gallery and activity log register filtered sets on the room
|
||||
* (getOrCreateFilteredTimelineSet), and the room re-emits their events with
|
||||
* liveEvent: true, so listeners that mean "a new event in the room" saw each
|
||||
* matching event twice (the timeline window drifted, notifications doubled).
|
||||
* The room's main and thread sets have no filter.
|
||||
*/
|
||||
export const isFromFilteredTimelineSet = (data?: IRoomTimelineData): boolean =>
|
||||
!!data?.timeline?.getTimelineSet()?.getFilter();
|
||||
|
||||
Reference in New Issue
Block a user