Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c62fb4722 |
@@ -17,7 +17,6 @@ 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 ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -39,6 +38,13 @@ type PolicyRuleContent = {
|
||||
recommendation?: string;
|
||||
};
|
||||
|
||||
type PolicyEntry = {
|
||||
entity: string;
|
||||
reason: string;
|
||||
recommendation: string;
|
||||
stateKey: string;
|
||||
};
|
||||
|
||||
type PolicyTab = 'users' | 'rooms' | 'servers';
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -47,6 +53,13 @@ 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.
|
||||
@@ -71,6 +84,23 @@ 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 }) {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
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');
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -69,8 +69,15 @@ export const Reactions = as<'div', ReactionsProps>(
|
||||
const measure = useCallback(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
const rowWidth = container.getBoundingClientRect().width;
|
||||
const gap = parseFloat(getComputedStyle(container).columnGap) || 8;
|
||||
// [Gitea #305] The width the chips actually get (content box). The border
|
||||
// box grows by 4 px while collapsed (margin -2 / padding 2), and measuring
|
||||
// it made a row within 4 px of fitting flip collapsed↔expanded every frame.
|
||||
const cs = getComputedStyle(container);
|
||||
const rowWidth =
|
||||
container.clientWidth -
|
||||
(parseFloat(cs.paddingLeft) || 0) -
|
||||
(parseFloat(cs.paddingRight) || 0);
|
||||
const gap = parseFloat(cs.columnGap) || 8;
|
||||
const chips = keys ? keys.split('\u0000').map((k) => chipRefs.current.get(k)) : [];
|
||||
const widths = chips.map((el) => el?.getBoundingClientRect().width ?? 0);
|
||||
const moreWidth = moreRef.current?.getBoundingClientRect().width ?? 48;
|
||||
@@ -110,8 +117,21 @@ export const Reactions = as<'div', ReactionsProps>(
|
||||
style={{
|
||||
...props.style,
|
||||
...(collapsed && rowHeight
|
||||
? // 2px breathing room so row-1 focus outlines are not clipped.
|
||||
{ maxHeight: rowHeight + 4, overflow: 'hidden', padding: 2, margin: -2 }
|
||||
? // 2px breathing room so row-1 focus outlines are not clipped. Longhand
|
||||
// margins so the timeline's marginTop survives (Gitea #305: the
|
||||
// `margin` shorthand overrode it and, once removed, cleared it).
|
||||
{
|
||||
maxHeight: rowHeight + 4,
|
||||
overflow: 'hidden',
|
||||
padding: 2,
|
||||
marginLeft: -2,
|
||||
marginRight: -2,
|
||||
marginBottom: -2,
|
||||
marginTop:
|
||||
props.style?.marginTop !== undefined
|
||||
? `calc(${typeof props.style.marginTop === 'number' ? `${props.style.marginTop}px` : props.style.marginTop} - 2px)`
|
||||
: -2,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
ref={(el) => {
|
||||
|
||||
Reference in New Issue
Block a user