Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15f2fcc4d1 |
@@ -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>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Page, PageContent, PageHeader } from '../../components/page';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../common-settings/styles.css';
|
||||
import { dedupePolicyEntries, PolicyEntry, recommendationLabel } from './policyEntries';
|
||||
|
||||
// ── Policy event types ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -38,13 +39,6 @@ type PolicyRuleContent = {
|
||||
recommendation?: string;
|
||||
};
|
||||
|
||||
type PolicyEntry = {
|
||||
entity: string;
|
||||
reason: string;
|
||||
recommendation: string;
|
||||
stateKey: string;
|
||||
};
|
||||
|
||||
type PolicyTab = 'users' | 'rooms' | 'servers';
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -53,13 +47,6 @@ function isGlob(entity: string): boolean {
|
||||
return entity.includes('*') || entity.includes('?');
|
||||
}
|
||||
|
||||
function recommendationLabel(rec: string): string {
|
||||
// `m.ban` is the stable value; `org.matrix.mjolnir.ban` is the legacy
|
||||
// (pre-stabilization) recommendation still emitted by older bots.
|
||||
if (rec === 'm.ban' || rec === 'org.matrix.mjolnir.ban') return 'Ban';
|
||||
return rec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all state events of a given type from a room's live forward state.
|
||||
* Uses the raw matrix-js-sdk `getStateEvents` API which accepts any string type.
|
||||
@@ -84,23 +71,6 @@ function extractPolicyEntries(events: MatrixEvent[]): PolicyEntry[] {
|
||||
.filter((entry) => entry.entity !== '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge policy entries from the stable and legacy event types for a rule
|
||||
* kind, de-duplicating by entity+recommendation so a room that emits both a
|
||||
* stable and a legacy rule for the same target isn't double-listed.
|
||||
*/
|
||||
export function dedupePolicyEntries(entries: PolicyEntry[]): PolicyEntry[] {
|
||||
const seen = new Set<string>();
|
||||
const result: PolicyEntry[] = [];
|
||||
entries.forEach((entry) => {
|
||||
const key = `${entry.entity} ${entry.recommendation}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
result.push(entry);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Entry row ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function PolicyEntryRow({ entry }: { entry: PolicyEntry }) {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { dedupePolicyEntries, recommendationLabel } from './policyEntries';
|
||||
|
||||
const e = (entity: string, recommendation: string, stateKey = entity) => ({
|
||||
entity,
|
||||
reason: 'spam waves',
|
||||
recommendation,
|
||||
stateKey,
|
||||
});
|
||||
|
||||
test('dedupePolicyEntries: a stable rule and its legacy twin show once (#321)', () => {
|
||||
const out = dedupePolicyEntries([
|
||||
e('@spammer:evil.example', 'm.ban'),
|
||||
e('@spammer:evil.example', 'org.matrix.mjolnir.ban'),
|
||||
e('@*:spam.example', 'm.ban'),
|
||||
]);
|
||||
assert.deepEqual(
|
||||
out.map((x) => x.entity),
|
||||
['@spammer:evil.example', '@*:spam.example'],
|
||||
);
|
||||
});
|
||||
|
||||
test('dedupePolicyEntries: different recommendations stay separate', () => {
|
||||
assert.equal(dedupePolicyEntries([e('@a:x', 'm.ban'), e('@a:x', 'org.example.mute')]).length, 2);
|
||||
});
|
||||
|
||||
test('recommendationLabel', () => {
|
||||
assert.equal(recommendationLabel('m.ban'), 'Ban');
|
||||
assert.equal(recommendationLabel('org.matrix.mjolnir.ban'), 'Ban');
|
||||
assert.equal(recommendationLabel('org.example.mute'), 'org.example.mute');
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
export type PolicyEntry = {
|
||||
entity: string;
|
||||
reason: string;
|
||||
recommendation: string;
|
||||
stateKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* `m.ban` is the stable value; `org.matrix.mjolnir.ban` is the legacy
|
||||
* (pre-stabilization) recommendation still emitted by older bots.
|
||||
*/
|
||||
export const normalizeRecommendation = (rec: string): string =>
|
||||
rec === 'org.matrix.mjolnir.ban' ? 'm.ban' : rec;
|
||||
|
||||
export function recommendationLabel(rec: string): string {
|
||||
if (normalizeRecommendation(rec) === 'm.ban') return 'Ban';
|
||||
return rec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge stable and legacy rule lists, one entry per entity + recommendation.
|
||||
* [Gitea #321] Keyed on the normalised recommendation: a bot publishing both
|
||||
* `m.ban` and its `org.matrix.mjolnir.ban` twin showed the rule twice.
|
||||
*/
|
||||
export function dedupePolicyEntries(entries: PolicyEntry[]): PolicyEntry[] {
|
||||
const seen = new Set<string>();
|
||||
const result: PolicyEntry[] = [];
|
||||
entries.forEach((entry) => {
|
||||
const key = `${entry.entity} ${normalizeRecommendation(entry.recommendation)}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
result.push(entry);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -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(':');
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -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