Make bare /quit disconnect only that server, keep windows open, bump to 0.1.9
/quit now disconnects the window's server without closing anything: every buffer of that server and wIRC main get a "Disconnected" line, the server card shows Disconnected with its Connect button, history is flushed to disk, and there's no auto-reconnect. Connect rejoins the channels we were in (including ones joined with /join, now tracked) into the still-open windows. Sending while disconnected shows a "Not connected" line in that window. Exit wIRC still disconnects everything and closes the app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d9a0297cc7
commit
3538274805
@@ -73,6 +73,17 @@ object ConnectionManager {
|
||||
private val _ownNicks = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||
val ownNicksFlow: StateFlow<Map<String, String>> = _ownNicks
|
||||
|
||||
// Servers that have a live connection (or one that's connecting/auto-reconnecting after
|
||||
// a network drop). A server leaves this set only through disconnect()/disconnectAll(),
|
||||
// and it's what wIRC main's server cards show as Connected / Disconnected.
|
||||
private val _connectedIds = MutableStateFlow<Set<String>>(emptySet())
|
||||
val connectedIds: StateFlow<Set<String>> = _connectedIds
|
||||
|
||||
// serverId -> channels we were in when /quit disconnected that server, so the next
|
||||
// connect() rejoins them (and the windows still open for them pick right back up)
|
||||
// instead of only the configured auto-join list.
|
||||
private val channelsToRejoin = ConcurrentHashMap<String, MutableSet<String>>()
|
||||
|
||||
// A /whois we sent and haven't seen the end of yet. headerShown tracks whether its
|
||||
// "WHOIS <nick>" header line is already in wIRC main.
|
||||
private class PendingWhois(val nick: String, val sentAtMillis: Long, var headerShown: Boolean)
|
||||
@@ -142,33 +153,100 @@ object ConnectionManager {
|
||||
existing.disconnect()
|
||||
}
|
||||
setOwnNick(config.id, config.nick)
|
||||
val conn = IrcConnection(
|
||||
// Channels from the connection a /quit ended, or from the one being replaced, so
|
||||
// they're rejoined along with the auto-join list.
|
||||
val rejoin = channelsToRejoin.remove(config.id).orEmpty() + existing?.trackedChannels().orEmpty()
|
||||
lateinit var conn: IrcConnection
|
||||
conn = IrcConnection(
|
||||
config = config,
|
||||
scope = scope,
|
||||
onLine = { line -> enqueue { handleLine(config.id, line) } },
|
||||
initialChannels = rejoin,
|
||||
// Lines and status updates from a connection that's already been disconnected
|
||||
// or replaced (its socket closing, a last line in flight) are dropped, so
|
||||
// nothing shows up after the "Disconnected" line or overwrites that status.
|
||||
onLine = { line -> if (connections[config.id] === conn) enqueue { handleLine(config.id, line) } },
|
||||
onStatus = { msg ->
|
||||
_statuses.update { it + (config.id to msg) }
|
||||
// Connection progress and errors aren't tied to any channel - wIRC main.
|
||||
enqueue { addMainLine(MessageKind.SYSTEM, null, "[${config.label}] $msg") }
|
||||
if (connections[config.id] === conn) {
|
||||
_statuses.update { it + (config.id to msg) }
|
||||
// Connection progress and errors aren't tied to any channel - wIRC main.
|
||||
enqueue { addMainLine(MessageKind.SYSTEM, null, "[${config.label}] $msg") }
|
||||
}
|
||||
}
|
||||
)
|
||||
connections[config.id] = conn
|
||||
_connectedIds.update { it + config.id }
|
||||
conn.connect()
|
||||
}
|
||||
|
||||
/**
|
||||
* Bare /quit: disconnects this one server and nothing else. Windows stay open and keep
|
||||
* their scrollback; every buffer of this server (plus [openKeys], the open windows, in
|
||||
* case one hasn't had a line yet) gets a "Disconnected" line, and so does wIRC main.
|
||||
* There's no auto-reconnect afterward - the next connect() (the server card's Connect
|
||||
* button) rejoins the channels we were in. History is written to disk right away, the
|
||||
* same as disconnectAll() does on exit.
|
||||
*/
|
||||
fun disconnect(serverId: String, openKeys: Collection<BufferKey> = emptyList()) {
|
||||
val conn = connections.remove(serverId) ?: return
|
||||
conn.disconnect()
|
||||
channelsToRejoin[serverId] = conn.trackedChannels().toMutableSet()
|
||||
_connectedIds.update { it - serverId }
|
||||
_statuses.update { it + (serverId to "Disconnected") }
|
||||
val label = conn.config.label
|
||||
enqueue {
|
||||
pendingWhois.remove(serverId)
|
||||
val now = System.currentTimeMillis()
|
||||
val keys = _buffers.value.keys.filter { it.serverId == serverId } +
|
||||
openKeys.filter { it.serverId == serverId }
|
||||
keys.distinct().forEach { key ->
|
||||
addMessage(key, IrcMessage(now, MessageKind.SYSTEM, null, "Disconnected from $label"))
|
||||
}
|
||||
// We're no longer in any channel, so the user lists are stale - the NAMES reply
|
||||
// on rejoin fills them again.
|
||||
_buffers.update { map ->
|
||||
map.mapValues { (key, buf) -> if (key.serverId == serverId) buf.copy(users = emptyList()) else buf }
|
||||
}
|
||||
addMainLine(MessageKind.SYSTEM, null, "[$label] Disconnected")
|
||||
// Runs on the bufferOps queue after the lines above, so they're in the save.
|
||||
flushAllSaves()
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnectAll() {
|
||||
connections.values.forEach { it.disconnect() }
|
||||
connections.clear()
|
||||
_connectedIds.value = emptySet()
|
||||
flushAllSaves()
|
||||
}
|
||||
|
||||
fun isConnected(serverId: String): Boolean = connections.containsKey(serverId)
|
||||
|
||||
// /part from a channel window. While disconnected there's no PART to send, but the
|
||||
// channel is dropped from the rejoin list so reconnecting doesn't rejoin it.
|
||||
fun partChannel(serverId: String, channel: String, reason: String) {
|
||||
val conn = connectionFor(serverId)
|
||||
if (conn != null) conn.partChannel(channel, reason)
|
||||
else channelsToRejoin[serverId]?.remove(channel)
|
||||
}
|
||||
|
||||
// /join from a channel window. A plain channel list is tracked (see
|
||||
// IrcConnection.joinChannel) so a reconnect rejoins it; one with a key is sent as-is,
|
||||
// since rejoining it later without the key wouldn't work anyway.
|
||||
fun joinChannels(serverId: String, args: String) {
|
||||
val conn = connectionFor(serverId) ?: return
|
||||
if (args.contains(' ')) conn.send("JOIN $args")
|
||||
else args.split(',').filter { it.isNotBlank() }.forEach { conn.joinChannel(it) }
|
||||
}
|
||||
|
||||
fun connectionFor(serverId: String): IrcConnection? = connections[serverId]
|
||||
|
||||
// IRC servers don't echo PRIVMSG back to its sender, so a sent message has to be
|
||||
// added to our own buffer locally - otherwise it would never show up in the window
|
||||
// that sent it.
|
||||
fun sendChannelMessage(serverId: String, target: String, text: String) {
|
||||
connectionFor(serverId)?.sendMessage(target, text)
|
||||
// No local echo while disconnected - the line was never sent.
|
||||
val conn = connectionFor(serverId) ?: return
|
||||
conn.sendMessage(target, text)
|
||||
val myNick = ownNicks[serverId] ?: return
|
||||
val msg = IrcMessage(System.currentTimeMillis(), MessageKind.CHAT, myNick, text)
|
||||
enqueue { addOutgoing(serverId, target, msg, "-> *$target* $text") }
|
||||
@@ -178,7 +256,8 @@ object ConnectionManager {
|
||||
// CTCP ACTION wrapper, while the buffer should just hold the plain action text
|
||||
// (ChannelScreen renders MessageKind.ACTION with its own "* nick ..." prefix).
|
||||
fun sendChannelAction(serverId: String, target: String, action: String) {
|
||||
connectionFor(serverId)?.sendMessage(target, "\u0001ACTION $action\u0001")
|
||||
val conn = connectionFor(serverId) ?: return
|
||||
conn.sendMessage(target, "\u0001ACTION $action\u0001")
|
||||
val myNick = ownNicks[serverId] ?: return
|
||||
val msg = IrcMessage(System.currentTimeMillis(), MessageKind.ACTION, myNick, action)
|
||||
enqueue { addOutgoing(serverId, target, msg, "-> *$target* * $myNick $action") }
|
||||
|
||||
Reference in New Issue
Block a user