Files
thelounge/client/components/StatusIcon.vue
Max Leiter 78cadc5ff1 ircv3: monitor and extended-monitor (#5045)
Updated version of #4550. Closes #337, closes #872 (#337 can't be fully
addressed — tracking nick changes isn't really possible atm).

<img width="253" height="149" alt="image"
src="https://github.com/user-attachments/assets/8a961868-0aa9-406b-92e2-f9d4858e2d0d"
/>

Away support:
<img width="214" alt="image"
src="https://user-images.githubusercontent.com/8675906/165882385-6f928c8e-5a0b-4502-8bd3-3cd0bd70e0e0.png">

## Summary

Query windows show whether the person behind them is online, and whether
they are away, using
[MONITOR](https://ircv3.net/specs/extensions/monitor) rather than
polling. `Network` keeps the monitor list: queries are registered in
batched `MONITOR +` commands once RPL_ISUPPORT tells us the server
supports it, and one at a time as new query windows open. The limit
advertised in `MONITOR=<n>` is respected, with anything over it waiting
for a slot; `MONITOR=0` means no monitoring at all, a bare `MONITOR`
means no limit. A nick change on a monitored query swaps the target in
place so the query keeps its slot.

[extended-monitor](https://ircv3.net/specs/extensions/extended-monitor)
is requested too, so away, nick, account and chghost updates arrive for
monitored nicks we share no channel with. Away state for people we do
share a channel with comes from a WHO on join.

The indicator is a dot on the query in the sidebar and next to the nick
in the message bubble, with a tooltip and an `img` role so it isn't
colour-only, and it can be turned off under Appearance.

Known limitation: a server that enforces a monitor limit it doesn't
advertise will reject part of a batch with `ERR_MONLISTFULL`; the user
gets a message in the lobby, but the client's own list isn't reconciled
until reconnect.

## Test Plan

- Unit tests for the monitor list: add, case-insensitive dedupe,
queueing at the limit, removal draining the queue, batching, and the
rename path.
- Ran the server against a local IRCd and watched the wire: opening
queries sends `MONITOR + <nick>` each, a reconnect with four open
queries sends a single `MONITOR + bob,carol,dave,mallory`, 730/731 drive
the online/offline state, a rename sends `MONITOR - bob` then `MONITOR +
bob2`, `MONITOR=0` sends nothing at all, and a bare `MONITOR` behaves as
unlimited.
2026-07-22 09:46:58 -07:00

80 lines
1.4 KiB
Vue

<template>
<div
:class="['status-icon', 'tooltipped tooltipped-no-touch', tooltipDirClass]"
:data-tooltip="label"
role="img"
:aria-label="label"
>
<span :class="statusClass" />
</div>
</template>
<style>
.status-icon {
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-left: 4px;
z-index: 0;
}
.status-icon > span::after {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.status-icon > .status-online::after {
background-color: #2ecc40;
}
.status-icon > .status-offline::after {
background-color: #ff4136;
}
.status-icon > .status-away::after {
background-color: gray;
}
</style>
<script lang="ts">
import {computed, PropType, defineComponent} from "vue";
export default defineComponent({
name: "StatusIcon",
props: {
online: Boolean,
away: Boolean,
tooltipDir: String as PropType<"n" | "s" | "e" | "w">,
},
setup(props) {
const tooltipDirClass = computed(
() => `tooltipped-${props.tooltipDir ? props.tooltipDir : "w"}`
);
const label = computed(() => {
if (props.away) {
return "Away";
} else if (props.online) {
return "Online";
}
return "Offline";
});
const statusClass = computed(() => `status-${label.value.toLowerCase()}`);
return {
tooltipDirClass,
statusClass,
label,
};
},
});
</script>