- Scrollback (channel windows and wIRC main): mouse drag selection, double-click word, triple-click line, shift-click extend, Ctrl+C / Ctrl+Insert, Ctrl+A, Esc, and a right-click menu (Copy / Copy line / Select all). Copies one line per message. Clipboard writes go off the UI thread with a timeout, like the paste handler. Auto-scroll pauses while selecting and catches up once the selection is cleared. - /whois: header, every reply line, related numerics (401/402/716/263...) and server or services notices arriving right after it all go to wIRC main under a bold "===== WHOIS <nick> (<server>) =====" header, counted by its unread indicator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
4.1 KiB
Kotlin
91 lines
4.1 KiB
Kotlin
package data
|
|
|
|
/** A configured IRC server/network connection. */
|
|
data class ServerConfig(
|
|
val id: String, // stable key, e.g. "rizon"
|
|
val label: String, // display name, e.g. "Rizon"
|
|
val host: String,
|
|
val port: Int = 6667,
|
|
val useTls: Boolean = false,
|
|
val nick: String,
|
|
val autoJoinChannels: List<String> = emptyList(),
|
|
val saslUser: String? = null,
|
|
val saslPass: String? = null,
|
|
// Raw IRC commands sent right after registration (001), before auto-join channels.
|
|
// e.g. "PRIVMSG NickServ :IDENTIFY hunter2" or "NS IDENTIFY hunter2".
|
|
// This is the "on join perform" list — saved to disk and replayed every connect.
|
|
val onConnectCommands: List<String> = emptyList()
|
|
)
|
|
|
|
/** True for channel-style targets ('#', '&', '+', '!' prefixes per the IRC spec). */
|
|
fun isChannelName(name: String): Boolean =
|
|
name.isNotEmpty() && name[0] in "#&+!"
|
|
|
|
enum class MessageKind { CHAT, ACTION, JOIN, PART, QUIT, KICK, MODE, NOTICE, NICK, TOPIC, SYSTEM }
|
|
|
|
// Process-wide, so every IrcMessage ever constructed while the app is running gets a
|
|
// genuinely unique seq, regardless of which buffer/server it belongs to.
|
|
private val nextMessageSeq = java.util.concurrent.atomic.AtomicLong(0)
|
|
|
|
data class IrcMessage(
|
|
val timestampMillis: Long,
|
|
val kind: MessageKind,
|
|
val sender: String?, // nick, or null for system/server lines
|
|
val rawText: String, // still contains mIRC control codes; parsed at render time
|
|
val target: String? = null, // e.g. for KICK, the nick that got kicked
|
|
// Monotonically increasing, guaranteed-unique identity for this message - used as the
|
|
// LazyColumn item key in ChannelScreen (see there). The old key there was
|
|
// "${timestampMillis}_${sender}_${rawText.hashCode()}" - in a genuinely busy channel,
|
|
// two messages sharing the same millisecond, sender, and text (not rare for short
|
|
// repeated lines - "lol", a single emoji, etc.) would produce the exact same key, and
|
|
// Compose requires unique keys per item. Not persisted to disk - history reloaded on
|
|
// restart just gets fresh seq values in load order, which is fine since uniqueness only
|
|
// needs to hold within a single running session, not across restarts.
|
|
val seq: Long = nextMessageSeq.getAndIncrement()
|
|
)
|
|
|
|
/** Buffer key: server id + channel/query name (channel names include '#'). */
|
|
data class BufferKey(val serverId: String, val name: String)
|
|
|
|
/**
|
|
* wIRC main's own buffer: server messages (MOTD, numerics, errors, all whois output) and
|
|
* services/server notices from every connected server. Server ids are built from the host
|
|
* with every non-alphanumeric character replaced by '_' (see ServerEditDialog), so one can
|
|
* never contain '-' - this id can't collide with a real server. Also used as the
|
|
* window_prefs.json key ("wirc-main__main") for the main window's saved position and size.
|
|
*/
|
|
val MAIN_BUFFER_KEY = BufferKey("wirc-main", "main")
|
|
|
|
// Nicks that are network services rather than people. Their PRIVMSGs and NOTICEs go to
|
|
// wIRC main instead of opening a query window. Anything ending in "serv" is also treated
|
|
// as a service, which covers the less common ones (MemoServ, HostServ, SaslServ, ...).
|
|
private val KNOWN_SERVICE_NICKS = setOf("nickserv", "chanserv", "global", "alis", "q", "x")
|
|
|
|
fun isServiceNick(nick: String): Boolean {
|
|
val lower = nick.lowercase()
|
|
return lower in KNOWN_SERVICE_NICKS || lower.endsWith("serv")
|
|
}
|
|
|
|
/**
|
|
* True when a message's source is a service or the server itself rather than a real user.
|
|
* A user prefix always has the "nick!user@host" form; a missing prefix or one without '!'
|
|
* is a server name (e.g. "irc.rizon.io").
|
|
*/
|
|
fun isServiceOrServerPrefix(prefix: String?): Boolean {
|
|
if (prefix == null || !prefix.contains('!')) return true
|
|
return isServiceNick(prefix.substringBefore('!'))
|
|
}
|
|
|
|
data class ChannelUser(
|
|
val nick: String,
|
|
val isOp: Boolean = false,
|
|
val isVoice: Boolean = false
|
|
)
|
|
|
|
data class ChannelBuffer(
|
|
val key: BufferKey,
|
|
val messages: List<IrcMessage> = emptyList(),
|
|
val users: List<ChannelUser> = emptyList(),
|
|
val topic: String = ""
|
|
)
|