Ignore somebody on the account, not just in this window

Right-clicking a Matrix message offers to ignore its sender, which writes
the account's own list - so it holds on every client you sign in from and
agrees with Element, unlike the local block it sits beside.

The account panel lists who is ignored with a way back, because the person
you want to un-ignore is by definition somebody whose messages you can no
longer see.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-04 15:02:44 -04:00
co-authored by Claude Opus 5
parent f8083dd4e3
commit 9eb3d33b5e
4 changed files with 83 additions and 1 deletions
+1 -1
Submodule nobilis updated: 93f2d5c571...ffa406a2ad
@@ -32,6 +32,7 @@ interface CrossSigningStatus {
export function MatrixAccountTools({ account }: { account: Account }): JSX.Element {
const store = useStore()
const verification = useChat((s) => s.matrixVerification)
const ignored = useChat((s) => s.matrixIgnored)[account.id] || []
const [devices, setDevices] = useState<MatrixDevice[] | null>(null)
const [loading, setLoading] = useState(false)
const [recoveryKey, setRecoveryKey] = useState('')
@@ -52,6 +53,10 @@ export function MatrixAccountTools({ account }: { account: Account }): JSX.Eleme
.then(setDevices)
.catch((e: Error) => store.toast('error', e.message))
.finally(() => setLoading(false))
void window.moho
.rpc<{ users: string[] }>('listMatrixIgnored', { accountId: account.id })
.then((answer) => store.noteIgnored(account.id, answer.users))
.catch(() => {})
void window.moho
.rpc<CrossSigningStatus>('matrixCrossSigningStatus', { accountId: account.id })
.then(setCrossSigning)
@@ -173,6 +178,36 @@ export function MatrixAccountTools({ account }: { account: Account }): JSX.Eleme
</p>
)}
{/* Who this account has asked never to hear from, and the way back.
Listed here rather than only offered from a message, because the
person you want to un-ignore is by definition somebody whose
messages you can no longer see. */}
{ignored.length > 0 && (
<div className="setting-row">
<div className="setting-text">
<div>Ignored</div>
<div className="small muted">
Kept on the account, so it holds on every client you sign in from.
</div>
</div>
</div>
)}
{ignored.map((userId) => (
<div key={userId} className="device-row">
<Icon name="block" size={18} color="var(--warning)" />
<div className="setting-text">
<div className="ellipsis">{userId}</div>
</div>
<button
type="button"
className="button subtle"
onClick={() => store.setIgnored(account.id, userId, false)}
>
Stop ignoring
</button>
</div>
))}
{loading && <p className="small muted">Loading sessions…</p>}
{devices?.length === 0 && <p className="small muted">No other sessions signed in.</p>}
@@ -253,6 +253,22 @@ export function MessageRow({
] as MenuEntry[])
: []),
{ label: 'Reply', icon: 'reply', onClick: () => reply() },
// Never hearing from them again, on every client this account uses -
// Matrix keeps the list on the account rather than in one window, which
// is the difference between blocking somebody and hiding them here.
...(service === 'matrix' && !isSystem && !message.isOwn && message.senderId
? ([
{
label: `Ignore ${message.from}`,
icon: 'block',
danger: true,
onClick: () => {
const account = store.accountFor(bufferId)
if (account) store.setIgnored(account.id, message.senderId!, true)
}
}
] as MenuEntry[])
: []),
// What the room tells everyone to read first. Offered to everybody
// rather than gated on a power level read here: the server decides, and
// it refuses in words worth showing - hiding the action from somebody who
+31
View File
@@ -366,6 +366,8 @@ export interface ChatState {
reviewCard: LiveCard | null
/** Which messages each Matrix room has pinned, newest last. */
matrixPinned: Record<string, string[]>
/** Who each Matrix account has asked never to hear from. */
matrixIgnored: Record<string, string[]>
/**
* The profile being shown, or null.
*
@@ -441,6 +443,7 @@ const INITIAL: ChatState = {
hiddenPolls: [],
reviewCard: null,
matrixPinned: {},
matrixIgnored: {},
profile: null,
replyingTo: null,
toasts: [],
@@ -1440,6 +1443,25 @@ export class ChatStore {
this.set({ hiddenPolls: this.state.hiddenPolls.filter((k) => k !== key) })
}
/** Seeds the ignore list from a direct read, before any event arrives. */
noteIgnored(accountId: string, users: string[]): void {
this.set({ matrixIgnored: { ...this.state.matrixIgnored, [accountId]: users } })
}
/**
* Ignores somebody on Matrix, or stops.
*
* The account's own list rather than this window's `blockedNicks`: it
* travels to every client signed in, which is what blocking somebody
* means, and it agrees with Element.
*/
setIgnored(accountId: string, userId: string, ignored: boolean): void {
void window.moho
.rpc('setMatrixIgnored', { accountId, userId, ignored })
.then(() => this.toast('info', ignored ? `Ignoring ${userId}` : `No longer ignoring ${userId}`))
.catch((e: Error) => this.toast('error', e.message))
}
/**
* Pins a message in a Matrix room, or takes the pin off.
*
@@ -1614,6 +1636,15 @@ export class ChatStore {
break
}
case 'matrixIgnored':
this.set({
matrixIgnored: {
...this.state.matrixIgnored,
[data.accountId as string]: (data.users as string[]) || []
}
})
break
case 'matrixPinned':
this.set({
matrixPinned: {