Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0e8bc4f
Core: Request history on channel join and create batched history event
Jokler Jul 21, 2026
2078a99
Add untested history_before command
Jokler Jul 21, 2026
f6ae112
Fix incomplete assert
Jokler Jul 21, 2026
1bee42f
Time out response channels after ~1s
Jokler Jul 21, 2026
078d804
Remove target requirment on CommandKey::History for now
Jokler Jul 21, 2026
5b7a302
Create a demo that connects to one channel and can request more history
Jokler Jul 21, 2026
e33a9e0
Associate history batch by label
Jokler Jul 23, 2026
545c5ff
Rename ChannelRole::None to Regular to match standard IRC terms
Jokler Jul 23, 2026
bc8bf48
Always track channel for requested chat history
Jokler Jul 24, 2026
5307001
Remove duplicate history saving, clean up
dolanske Jul 27, 2026
08fae9c
Track all batches and load initial channel history again
Jokler Aug 4, 2026
ac761de
Fix clippy warning
Jokler Aug 4, 2026
0f9ecf0
Set MessageReference fields to None if the Message was not found
Jokler Aug 4, 2026
37125cb
Add message type to msgid fallback and fix typos
Jokler Aug 4, 2026
20873a0
Only recreate timeouts after they timeout
Jokler Aug 4, 2026
96016ef
Remove superfluous chathistory checks
Jokler Aug 4, 2026
43ae29e
Fix reactor state update mix up
Jokler Aug 4, 2026
653ce89
Ignore scrollback request if there are no messages
Jokler Aug 4, 2026
13c7740
Turn debug statement into a warning log
Jokler Aug 4, 2026
19fddc4
Sync import cfg directive with use location
Jokler Aug 4, 2026
9a679d0
Remove unused History ServerEvent
Jokler Aug 4, 2026
dbad4e4
Always get initial server messages from correct server
Jokler Aug 11, 2026
a47b27a
Fix no web compile error
Jokler Aug 11, 2026
587857b
Add support for nested batches and multiline messages
Jokler Aug 11, 2026
fe7ef91
Make IrcChannel::state() return optional
Jokler Aug 11, 2026
fb4d59d
Adjust MessageReference doc comment
Jokler Aug 11, 2026
70a68dc
Add assert for unfulfilled history requests
Jokler Aug 11, 2026
75d96bb
Fix TS error
Jokler Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 49 additions & 5 deletions packages/app/src/components/windows/WindowChat.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,65 @@
<script setup lang="ts">
import { MessageType } from "core-wasm"
import type { WindowChat } from "../../lib/windows"
import { useIrcStore } from "../../stores/irc"

// TODO: if no chat is active, list available chats for a server. We know that
// because channelId will be "__unspecified"
import { ref } from "vue"

const props = defineProps<WindowChat>()

const editmsg = ref("")

const irc = useIrcStore()
const state = irc.getServerState(props.serverId)
const messages = irc.getChannelMessages(props.serverId, "#orbit/testing")

async function sendMessage() {
if (editmsg.value === "") {
return
}
const out = editmsg.value
editmsg.value = ""
await irc.serverChannel?.send_message(out)
}
</script>

<template>
<div>
<pre>
{{ props }}
</pre>
<pre>{{ state }}</pre>
<button @click="irc.requestScrollback(props.serverId, '#orbit/testing')">Get more</button>
<div class="messages">
<div v-for="message in messages" :key="message.metadata.msgid">
<pre class="msg" v-if="message.metadata.message_type === MessageType.Privmsg">{{ message.metadata.user }}: {{ message.text?.content }}</pre>
<pre class="join" v-if="message.metadata.message_type === MessageType.Join">{{ message.metadata.user }} joined</pre>
<pre class="part" v-if="message.metadata.message_type === MessageType.Part">{{ message.metadata.user }} parted</pre>
<pre class="quit" v-if="message.metadata.message_type === MessageType.Quit">{{ message.metadata.user }} quit</pre>
</div>
</div>
<form @submit.prevent="sendMessage">
<input v-model="editmsg" type="text" />
</form>
</div>
</template>
<style scoped>
button {
padding: 10px;
background-color: gray;
}
.messages {
max-height: 500px;
width: 800px;
overflow-y: scroll;

pre {
padding: 2px;
}

.join {
color: green;
}
.part,
.quit {
color: red;
}
}
</style>
68 changes: 61 additions & 7 deletions packages/app/src/stores/irc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineStore } from "pinia"
import { Message, React, type IrcConnection, type Server, type ServerList } from "core-wasm"
import { computed, ref, shallowRef } from "vue"
import { ChannelMessage, Message, React, type IrcConnection, type Server, type ServerList, OrbitError, IrcChannel } from "core-wasm"
import { computed, reactive, ref, shallowRef } from "vue"
import { useUserStore } from "./user"
import { useAppStateStore } from "./app-state"

Expand All @@ -22,7 +22,8 @@ export const useIrcStore = defineStore("irc", () => {
const serverHandlers = shallowRef<Map<number, IrcConnection>>(new Map())

// Holds references to messages per server. This should be actually per `server:channel`
const serverMessages = shallowRef<Map<number, Message[]>>(new Map())
const serverMessages = reactive<Map<number, Map<string, Message[]>>>(new Map())
const serverChannel = ref<IrcChannel>()

let controller: ServerList = {} as ServerList

Expand Down Expand Up @@ -69,8 +70,21 @@ export const useIrcStore = defineStore("irc", () => {
serverHandlers.value.set(state.id, handler)

await handler.sign_in_anonymous(user.me.displayName, user.me.accountName, user.me.accountName)
serverChannel.value = await handler.join_channel("#orbit/testing")
console.log("Signed in")

// Set initial channel messages
const channelState = (await serverChannel.value.state())!

const existingServer = serverMessages.get(state.id) ?? new Map<string, Message[]>()
const existingChannel = existingServer.get(channelState.metadata.name) ?? []

existingChannel.push(...channelState.messages)
existingChannel.sort((a, b) => a.metadata.server_time - b.metadata.server_time)

existingServer.set(channelState.metadata.name, existingChannel)
serverMessages.set(state.id, existingServer)

registerServerEvents(state.id, handler)

return {
Expand All @@ -82,10 +96,15 @@ export const useIrcStore = defineStore("irc", () => {
function registerServerEvents(key: number, handler: IrcConnection) {
// Runs whenever some dataset on the server object changes
handler.on_data((event) => {
if (event instanceof Message) {
const existing = serverMessages.value.get(key) ?? []
existing.push(event)
serverMessages.value.set(key, existing)
if (event instanceof ChannelMessage) {
const existingServer = serverMessages.get(key) ?? new Map()
const existingChannel = existingServer.get(event.channel) ?? []

existingChannel.push(event.message)
existingChannel.sort((a: Message, b: Message) => a.metadata.server_time - b.metadata.server_time)

existingServer.set(event.channel, existingChannel)
serverMessages.set(key, existingServer)
} else if (event instanceof React) {
// TODO
console.log("Received reaction", event)
Expand All @@ -104,10 +123,42 @@ export const useIrcStore = defineStore("irc", () => {
})
}

// TODO: these should be cached not to create a separate computed value on each call
function getServerState(id: number) {
return computed(() => serverState.value.get(id))
}

function getChannelMessages(id: number, channel: string) {
return computed(() => serverMessages.get(id)?.get(channel))
}

// TODO: will be called automatically by a scroll listener to append new messages as user's nearing the top of the window
async function requestScrollback(id: number, channel: string) {
try {
const oldestId = serverMessages.get(id)?.get(channel)?.at(0)?.metadata.msgid
if (!oldestId) {
return
}
const history = await serverHandlers.value.get(id)?.history_before(channel, oldestId)

if (!history) {
return
}

const existingServer = serverMessages.get(id) ?? new Map<string, Message[]>()
const existingChannel = existingServer.get(history.channel) ?? []

existingChannel.push(...history.messages)
existingChannel.sort((a, b) => a.metadata.server_time - b.metadata.server_time)

existingServer.set(history.channel, existingChannel)
serverMessages.set(id, existingServer)
} catch (e: unknown) {
const error = e as OrbitError
console.error(JSON.parse(error.toString()))
}
}

return {
init,
serverConnect,
Expand All @@ -116,5 +167,8 @@ export const useIrcStore = defineStore("irc", () => {
serverData: serverState,
serverControllers: serverHandlers,
getServerState,
getChannelMessages,
requestScrollback,
serverChannel,
}
})
33 changes: 31 additions & 2 deletions packages/core/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 10 additions & 4 deletions packages/core/core-shared/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[package]

name = "core-shared"
version = "0.1.0"
edition = "2024"
Expand All @@ -8,19 +9,24 @@ anyhow = { version = "1.0.103", features = ["backtrace"] }
base64 = "0.22.1"
blake3 = "1.8.5"
futures = "0.3.32"
gloo-console = { version = "0.4.0", optional = true }
gloo-net = { version = "0.7.0", optional = true }
indexed_db_futures = { version = "0.6.4", features = ["serde"] }
irc-proto = "1.1.0"
ordermap = "1.2.0"
thiserror = "2.0.18"
time = { version = "0.3.53", features = ["parsing"] }
tracing = "0.1.44"

tsify = { version = "0.5.6", optional = true, features = ["js"] }
wasm-bindgen = { version = "0.2.126", optional = true }
web-time = { version = "1.1.0", optional = true }
gloo-console = { version = "0.4.0", optional = true }
gloo-net = { version = "0.7.0", optional = true }
tokio = { version = "1.52.3", features = ["macros", "rt", "time", "sync"], optional = true }
gloo-timers = { version = "0.4.0", features = ["futures"], optional = true }
rand = { version = "0.10.2", default-features = false }

[features]
web = ["dep:gloo-net", "dep:wasm-bindgen", "dep:gloo-console", "dep:tsify"]
web = ["dep:gloo-net", "dep:wasm-bindgen", "dep:gloo-console", "dep:tsify", "dep:web-time", "dep:gloo-timers"]
default = ["dep:tokio"]

[dev-dependencies]
assert_matches = "1.5.0"
Expand Down
Loading
Loading