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
+2
-2
@@ -6,7 +6,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "com.tpuahsiew.wirc"
|
group = "com.tpuahsiew.wirc"
|
||||||
version = "0.1.8"
|
version = "0.1.9"
|
||||||
|
|
||||||
// No project-level repositories{} block here: settings.gradle.kts sets
|
// No project-level repositories{} block here: settings.gradle.kts sets
|
||||||
// dependencyResolutionManagement.repositoriesMode = FAIL_ON_PROJECT_REPOS and already
|
// dependencyResolutionManagement.repositoriesMode = FAIL_ON_PROJECT_REPOS and already
|
||||||
@@ -38,7 +38,7 @@ compose.desktop {
|
|||||||
appResourcesRootDir.set(project.file("installerResources"))
|
appResourcesRootDir.set(project.file("installerResources"))
|
||||||
targetFormats(TargetFormat.Msi, TargetFormat.Exe)
|
targetFormats(TargetFormat.Msi, TargetFormat.Exe)
|
||||||
packageName = "wIRC"
|
packageName = "wIRC"
|
||||||
packageVersion = "0.1.8"
|
packageVersion = "0.1.9"
|
||||||
windows {
|
windows {
|
||||||
menuGroup = "wIRC"
|
menuGroup = "wIRC"
|
||||||
upgradeUuid = "8f14e45f-ceea-4a5e-9c3b-5b1f0e8c2b7a"
|
upgradeUuid = "8f14e45f-ceea-4a5e-9c3b-5b1f0e8c2b7a"
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ fun main() = application {
|
|||||||
var openBuffers by remember { mutableStateOf(setOf<BufferKey>()) }
|
var openBuffers by remember { mutableStateOf(setOf<BufferKey>()) }
|
||||||
var mainUnread by remember { mutableStateOf(0) }
|
var mainUnread by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
// Full shutdown: same steps /quit (bare, no args) already runs from a channel window -
|
// Full shutdown for the "Exit wIRC" button: disconnect every server (which also writes
|
||||||
// disconnect every server, then end the process. Shared here so the "Exit wIRC" button
|
// all scrollback to disk), then end the process. A bare /quit in a channel window only
|
||||||
// and every channel window's /quit reuse the exact same logic instead of duplicating it.
|
// disconnects that window's server - see ConnectionManager.disconnect.
|
||||||
val shutdownApp: () -> Unit = {
|
val shutdownApp: () -> Unit = {
|
||||||
ConnectionManager.disconnectAll()
|
ConnectionManager.disconnectAll()
|
||||||
exitApplication()
|
exitApplication()
|
||||||
@@ -122,8 +122,10 @@ fun main() = application {
|
|||||||
colorScheme = neutralDarkScheme,
|
colorScheme = neutralDarkScheme,
|
||||||
onOpenBuffer = { newKey -> openBuffers = openBuffers + newKey },
|
onOpenBuffer = { newKey -> openBuffers = openBuffers + newKey },
|
||||||
onCloseSelf = { openBuffers = openBuffers - bufferKey },
|
onCloseSelf = { openBuffers = openBuffers - bufferKey },
|
||||||
onQuitApp = {
|
// openBuffers is read when /quit runs, so every window open at that moment
|
||||||
exitApplication()
|
// gets its "Disconnected" line, even one that hasn't had a line yet.
|
||||||
|
onDisconnectServer = {
|
||||||
|
ConnectionManager.disconnect(bufferKey.serverId, openBuffers)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ object AliasRepository {
|
|||||||
return aliases.size
|
return aliases.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** True if [command] (without the leading '/') is an alias - those run locally, no server needed. */
|
||||||
|
fun hasAlias(command: String): Boolean {
|
||||||
|
val key = command.lowercase()
|
||||||
|
return key != RESERVED_RELOAD && key in aliases
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if [command] (without the leading '/') matched a known alias and its
|
* Returns true if [command] (without the leading '/') matched a known alias and its
|
||||||
* action was run - callers should treat that as "handled, don't fall through to IRC
|
* action was run - callers should treat that as "handled, don't fall through to IRC
|
||||||
|
|||||||
@@ -73,6 +73,17 @@ object ConnectionManager {
|
|||||||
private val _ownNicks = MutableStateFlow<Map<String, String>>(emptyMap())
|
private val _ownNicks = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||||
val ownNicksFlow: StateFlow<Map<String, String>> = _ownNicks
|
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
|
// 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.
|
// "WHOIS <nick>" header line is already in wIRC main.
|
||||||
private class PendingWhois(val nick: String, val sentAtMillis: Long, var headerShown: Boolean)
|
private class PendingWhois(val nick: String, val sentAtMillis: Long, var headerShown: Boolean)
|
||||||
@@ -142,33 +153,100 @@ object ConnectionManager {
|
|||||||
existing.disconnect()
|
existing.disconnect()
|
||||||
}
|
}
|
||||||
setOwnNick(config.id, config.nick)
|
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,
|
config = config,
|
||||||
scope = scope,
|
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 ->
|
onStatus = { msg ->
|
||||||
|
if (connections[config.id] === conn) {
|
||||||
_statuses.update { it + (config.id to msg) }
|
_statuses.update { it + (config.id to msg) }
|
||||||
// Connection progress and errors aren't tied to any channel - wIRC main.
|
// Connection progress and errors aren't tied to any channel - wIRC main.
|
||||||
enqueue { addMainLine(MessageKind.SYSTEM, null, "[${config.label}] $msg") }
|
enqueue { addMainLine(MessageKind.SYSTEM, null, "[${config.label}] $msg") }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
connections[config.id] = conn
|
connections[config.id] = conn
|
||||||
|
_connectedIds.update { it + config.id }
|
||||||
conn.connect()
|
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() {
|
fun disconnectAll() {
|
||||||
connections.values.forEach { it.disconnect() }
|
connections.values.forEach { it.disconnect() }
|
||||||
connections.clear()
|
connections.clear()
|
||||||
|
_connectedIds.value = emptySet()
|
||||||
flushAllSaves()
|
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]
|
fun connectionFor(serverId: String): IrcConnection? = connections[serverId]
|
||||||
|
|
||||||
// IRC servers don't echo PRIVMSG back to its sender, so a sent message has to be
|
// 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
|
// added to our own buffer locally - otherwise it would never show up in the window
|
||||||
// that sent it.
|
// that sent it.
|
||||||
fun sendChannelMessage(serverId: String, target: String, text: String) {
|
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 myNick = ownNicks[serverId] ?: return
|
||||||
val msg = IrcMessage(System.currentTimeMillis(), MessageKind.CHAT, myNick, text)
|
val msg = IrcMessage(System.currentTimeMillis(), MessageKind.CHAT, myNick, text)
|
||||||
enqueue { addOutgoing(serverId, target, msg, "-> *$target* $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
|
// CTCP ACTION wrapper, while the buffer should just hold the plain action text
|
||||||
// (ChannelScreen renders MessageKind.ACTION with its own "* nick ..." prefix).
|
// (ChannelScreen renders MessageKind.ACTION with its own "* nick ..." prefix).
|
||||||
fun sendChannelAction(serverId: String, target: String, action: String) {
|
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 myNick = ownNicks[serverId] ?: return
|
||||||
val msg = IrcMessage(System.currentTimeMillis(), MessageKind.ACTION, myNick, action)
|
val msg = IrcMessage(System.currentTimeMillis(), MessageKind.ACTION, myNick, action)
|
||||||
enqueue { addOutgoing(serverId, target, msg, "-> *$target* * $myNick $action") }
|
enqueue { addOutgoing(serverId, target, msg, "-> *$target* * $myNick $action") }
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ private fun translateOnConnectCommand(cmd: String): String {
|
|||||||
class IrcConnection(
|
class IrcConnection(
|
||||||
val config: ServerConfig,
|
val config: ServerConfig,
|
||||||
private val scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
|
// Channels to rejoin on top of config.autoJoinChannels - the ones a previous connection
|
||||||
|
// to this server was in (see ConnectionManager.connect).
|
||||||
|
initialChannels: Collection<String> = emptyList(),
|
||||||
private val onLine: (ParsedLine) -> Unit,
|
private val onLine: (ParsedLine) -> Unit,
|
||||||
private val onStatus: (String) -> Unit
|
private val onStatus: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -60,13 +63,17 @@ class IrcConnection(
|
|||||||
@Volatile private var socket: Socket? = null
|
@Volatile private var socket: Socket? = null
|
||||||
@Volatile private var writer: PrintWriter? = null
|
@Volatile private var writer: PrintWriter? = null
|
||||||
private var readJob: Job? = null
|
private var readJob: Job? = null
|
||||||
private var wantConnected = false
|
// @Volatile: disconnect() clears it from the UI thread while the read loop checks it on
|
||||||
|
// an IO thread - it's what stops the loop from auto-reconnecting after a /quit.
|
||||||
|
@Volatile private var wantConnected = false
|
||||||
private var backoffMillis = 2000L
|
private var backoffMillis = 2000L
|
||||||
|
|
||||||
// Every channel we're currently in, whether from config.autoJoinChannels or joined
|
// Every channel we're currently in, whether from config.autoJoinChannels or joined
|
||||||
// manually mid-session - so a reconnect rejoins everything, not just the original
|
// manually mid-session - so a reconnect rejoins everything, not just the original
|
||||||
// auto-join list.
|
// auto-join list.
|
||||||
private val joinedChannels = java.util.concurrent.CopyOnWriteArraySet<String>()
|
private val joinedChannels = java.util.concurrent.CopyOnWriteArraySet<String>(initialChannels)
|
||||||
|
|
||||||
|
fun trackedChannels(): Set<String> = joinedChannels.toSet()
|
||||||
|
|
||||||
// Guards runPostRegistration() against running twice for the same connection cycle
|
// Guards runPostRegistration() against running twice for the same connection cycle
|
||||||
// (376 and 422 are mutually exclusive per RFC, but this is cheap insurance) - reset
|
// (376 and 422 are mutually exclusive per RFC, but this is cheap insurance) - reset
|
||||||
@@ -121,6 +128,9 @@ class IrcConnection(
|
|||||||
Socket(config.host, config.port)
|
Socket(config.host, config.port)
|
||||||
}
|
}
|
||||||
socket = sock
|
socket = sock
|
||||||
|
// disconnect() can't interrupt a socket that's still being opened, so
|
||||||
|
// check here before registering on a connection nobody wants anymore.
|
||||||
|
ensureActive()
|
||||||
val out = PrintWriter(OutputStreamWriter(sock.getOutputStream(), Charsets.UTF_8), true)
|
val out = PrintWriter(OutputStreamWriter(sock.getOutputStream(), Charsets.UTF_8), true)
|
||||||
writer = out
|
writer = out
|
||||||
|
|
||||||
@@ -161,6 +171,9 @@ class IrcConnection(
|
|||||||
handleRawLine(line)
|
handleRawLine(line)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
// After disconnect() the read fails because we closed the socket
|
||||||
|
// ourselves - that's not an error worth reporting.
|
||||||
|
if (!wantConnected) break
|
||||||
onStatus("Connection error: ${e.message}")
|
onStatus("Connection error: ${e.message}")
|
||||||
ircDebug(config.id, "connection dropped: ${e.message} - tracked channels at drop time: $joinedChannels")
|
ircDebug(config.id, "connection dropped: ${e.message} - tracked channels at drop time: $joinedChannels")
|
||||||
} finally {
|
} finally {
|
||||||
@@ -277,6 +290,9 @@ class IrcConnection(
|
|||||||
try { writer?.println("QUIT :leaving") } catch (_: Exception) {}
|
try { writer?.println("QUIT :leaving") } catch (_: Exception) {}
|
||||||
readJob?.cancel()
|
readJob?.cancel()
|
||||||
closeSocketQuietly()
|
closeSocketQuietly()
|
||||||
|
// Ends the outbox consumer - this connection is never reused (a reconnect
|
||||||
|
// makes a new IrcConnection), so it would otherwise idle forever.
|
||||||
|
outbox.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,20 @@ import data.WindowPrefsRepository
|
|||||||
import data.isChannelName
|
import data.isChannelName
|
||||||
import data.storageId
|
import data.storageId
|
||||||
import irc.ConnectionManager
|
import irc.ConnectionManager
|
||||||
|
import irc.IrcConnection
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private const val NOT_CONNECTED_TEXT = "Not connected - use Connect in wIRC main to reconnect"
|
||||||
|
|
||||||
|
// Runs [action] on this server's connection, or shows the not-connected line in this
|
||||||
|
// window when there isn't one (e.g. the user-list kick/ban/op menu after a /quit).
|
||||||
|
private fun withConnection(serverId: String, windowName: String, action: (IrcConnection) -> Unit) {
|
||||||
|
val conn = ConnectionManager.connectionFor(serverId)
|
||||||
|
if (conn != null) action(conn)
|
||||||
|
else ConnectionManager.addSystemMessage(serverId, windowName, NOT_CONNECTED_TEXT)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One of these is composed per open buffer key (see Main.kt) - each is a real, independent
|
* One of these is composed per open buffer key (see Main.kt) - each is a real, independent
|
||||||
* top-level OS window, same intent as ChannelWindowActivity.kt's per-channel Activity on
|
* top-level OS window, same intent as ChannelWindowActivity.kt's per-channel Activity on
|
||||||
@@ -36,7 +47,7 @@ fun ChannelWindow(
|
|||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
onOpenBuffer: (BufferKey) -> Unit,
|
onOpenBuffer: (BufferKey) -> Unit,
|
||||||
onCloseSelf: () -> Unit,
|
onCloseSelf: () -> Unit,
|
||||||
onQuitApp: () -> Unit
|
onDisconnectServer: () -> Unit
|
||||||
) {
|
) {
|
||||||
val serverId = bufferKey.serverId
|
val serverId = bufferKey.serverId
|
||||||
val channelName = bufferKey.name
|
val channelName = bufferKey.name
|
||||||
@@ -87,7 +98,19 @@ fun ChannelWindow(
|
|||||||
displaySettings = displaySettings,
|
displaySettings = displaySettings,
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
ownNick = ownNick,
|
ownNick = ownNick,
|
||||||
onSend = { text ->
|
onSend = onSend@{ text ->
|
||||||
|
// Everything except /reload, aliases and /part (which only closes
|
||||||
|
// the window) needs the server - while disconnected it gets this
|
||||||
|
// line in this window instead of going nowhere.
|
||||||
|
val needsServer = if (text.startsWith("/")) {
|
||||||
|
val command = text.substring(1).substringBefore(' ').uppercase()
|
||||||
|
command != "RELOAD" && command !in setOf("PART", "EXIT", "CLOSE") &&
|
||||||
|
!AliasRepository.hasAlias(command)
|
||||||
|
} else true
|
||||||
|
if (needsServer && !ConnectionManager.isConnected(serverId)) {
|
||||||
|
ConnectionManager.addSystemMessage(serverId, channelName, NOT_CONNECTED_TEXT)
|
||||||
|
return@onSend
|
||||||
|
}
|
||||||
if (text.startsWith("/")) {
|
if (text.startsWith("/")) {
|
||||||
val withoutSlash = text.substring(1)
|
val withoutSlash = text.substring(1)
|
||||||
val spaceIdx = withoutSlash.indexOf(' ')
|
val spaceIdx = withoutSlash.indexOf(' ')
|
||||||
@@ -112,19 +135,20 @@ fun ChannelWindow(
|
|||||||
"NICK" -> ConnectionManager.sendRaw(serverId, "NICK $args")
|
"NICK" -> ConnectionManager.sendRaw(serverId, "NICK $args")
|
||||||
"PART", "EXIT", "CLOSE" -> {
|
"PART", "EXIT", "CLOSE" -> {
|
||||||
val target = args.ifBlank { channelName }
|
val target = args.ifBlank { channelName }
|
||||||
ConnectionManager.connectionFor(serverId)?.partChannel(target, "leaving")
|
ConnectionManager.partChannel(serverId, target, "leaving")
|
||||||
if (args.isBlank() || target == channelName) onCloseSelf()
|
if (args.isBlank() || target == channelName) onCloseSelf()
|
||||||
}
|
}
|
||||||
"QUIT" -> {
|
"QUIT" -> {
|
||||||
if (rawArgs.isBlank()) {
|
if (rawArgs.isBlank()) {
|
||||||
ConnectionManager.disconnectAll()
|
// Disconnects this window's server only - every
|
||||||
onQuitApp()
|
// window stays open; see ConnectionManager.disconnect.
|
||||||
|
onDisconnectServer()
|
||||||
} else {
|
} else {
|
||||||
ConnectionManager.sendChannelMessage(serverId, channelName, rawArgs)
|
ConnectionManager.sendChannelMessage(serverId, channelName, rawArgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"JOIN" -> {
|
"JOIN" -> {
|
||||||
ConnectionManager.sendRaw(serverId, "JOIN $args")
|
ConnectionManager.joinChannels(serverId, args)
|
||||||
onOpenBuffer(BufferKey(serverId, args))
|
onOpenBuffer(BufferKey(serverId, args))
|
||||||
}
|
}
|
||||||
// All whois output goes to wIRC main, not this window.
|
// All whois output goes to wIRC main, not this window.
|
||||||
@@ -144,10 +168,10 @@ fun ChannelWindow(
|
|||||||
ConnectionManager.sendChannelMessage(serverId, channelName, text)
|
ConnectionManager.sendChannelMessage(serverId, channelName, text)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onKick = { nick -> ConnectionManager.connectionFor(serverId)?.kick(channelName, nick) },
|
onKick = { nick -> withConnection(serverId, channelName) { it.kick(channelName, nick) } },
|
||||||
onBan = { nick -> ConnectionManager.connectionFor(serverId)?.ban(channelName, "$nick!*@*") },
|
onBan = { nick -> withConnection(serverId, channelName) { it.ban(channelName, "$nick!*@*") } },
|
||||||
onOp = { nick -> ConnectionManager.connectionFor(serverId)?.setMode(channelName, "+o", nick) },
|
onOp = { nick -> withConnection(serverId, channelName) { it.setMode(channelName, "+o", nick) } },
|
||||||
onDeop = { nick -> ConnectionManager.connectionFor(serverId)?.setMode(channelName, "-o", nick) },
|
onDeop = { nick -> withConnection(serverId, channelName) { it.setMode(channelName, "-o", nick) } },
|
||||||
onAlphaChange = { alpha ->
|
onAlphaChange = { alpha ->
|
||||||
scope.launch(Dispatchers.IO) { prefsRepo.setAlpha(bufferKey.storageId(), alpha) }
|
scope.launch(Dispatchers.IO) { prefsRepo.setAlpha(bufferKey.storageId(), alpha) }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ fun MainScreen(
|
|||||||
var servers by remember { mutableStateOf(repo.loadAll()) }
|
var servers by remember { mutableStateOf(repo.loadAll()) }
|
||||||
var showAddDialog by remember { mutableStateOf(false) }
|
var showAddDialog by remember { mutableStateOf(false) }
|
||||||
var editingServer by remember { mutableStateOf<ServerConfig?>(null) }
|
var editingServer by remember { mutableStateOf<ServerConfig?>(null) }
|
||||||
var connectedIds by remember { mutableStateOf(setOf<String>()) }
|
// From ConnectionManager rather than local state, so a /quit typed in a channel window
|
||||||
|
// flips that server's card back to Disconnected with its Connect button.
|
||||||
|
val connectedIds by ConnectionManager.connectedIds.collectAsState()
|
||||||
var channelInputs by remember { mutableStateOf(mapOf<String, String>()) }
|
var channelInputs by remember { mutableStateOf(mapOf<String, String>()) }
|
||||||
var openedChannels by remember { mutableStateOf(mapOf<String, List<String>>()) }
|
var openedChannels by remember { mutableStateOf(mapOf<String, List<String>>()) }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
@@ -85,9 +87,8 @@ fun MainScreen(
|
|||||||
Text("+ Add Server")
|
Text("+ Add Server")
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
// Same end result as typing "/quit" alone in a channel window - disconnects
|
// Disconnects every server and terminates the process. (A bare "/quit" in a
|
||||||
// every server and terminates the process - just reachable without needing
|
// channel window only disconnects that window's server.)
|
||||||
// a channel window open first.
|
|
||||||
TextButton(onClick = onExitApp) {
|
TextButton(onClick = onExitApp) {
|
||||||
Text("Exit wIRC")
|
Text("Exit wIRC")
|
||||||
}
|
}
|
||||||
@@ -108,8 +109,9 @@ fun MainScreen(
|
|||||||
openedChannels = openedChannels[server.id] ?: emptyList(),
|
openedChannels = openedChannels[server.id] ?: emptyList(),
|
||||||
onChannelInputChange = { text -> channelInputs = channelInputs + (server.id to text) },
|
onChannelInputChange = { text -> channelInputs = channelInputs + (server.id to text) },
|
||||||
onConnect = {
|
onConnect = {
|
||||||
|
// Also rejoins the channels this server was in before a /quit - their
|
||||||
|
// windows are still open, and opening one that already is is a no-op.
|
||||||
ConnectionManager.connect(server)
|
ConnectionManager.connect(server)
|
||||||
connectedIds = connectedIds + server.id
|
|
||||||
if (server.autoJoinChannels.isNotEmpty()) {
|
if (server.autoJoinChannels.isNotEmpty()) {
|
||||||
// Auto-join sends JOIN correctly on connect (see IrcConnection),
|
// Auto-join sends JOIN correctly on connect (see IrcConnection),
|
||||||
// but nothing else opens a window for it like the manual
|
// but nothing else opens a window for it like the manual
|
||||||
@@ -243,6 +245,8 @@ private fun ServerCard(
|
|||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
|
Text("Disconnected", style = MaterialTheme.typography.bodySmall)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
Button(onClick = onConnect) { Text("Connect") }
|
Button(onClick = onConnect) { Text("Connect") }
|
||||||
} else {
|
} else {
|
||||||
Text("Connected", style = MaterialTheme.typography.bodySmall)
|
Text("Connected", style = MaterialTheme.typography.bodySmall)
|
||||||
|
|||||||
Reference in New Issue
Block a user