lots of things

This commit is contained in:
Sam Hoffman
2026-01-18 18:30:22 -05:00
parent 5e71721f4d
commit 78a1be39eb
12 changed files with 568 additions and 28 deletions

View File

@@ -1,7 +1,12 @@
<template>
<v-app>
<Login v-if="!ircStore.connected" />
<Chat v-else />
<router-view />
<!-- <Login v-if="!ircStore.connected" /> -->
<!-- <Chat v-else /> -->
<!-- <v-dialog max-width="450px" v-model="showRegistration"> -->
<!-- <Register @success="showRegistration = false" /> -->
<!-- </v-dialog> -->
</v-app>
</template>
@@ -9,6 +14,10 @@
import { useIRCStore } from "@/stores/irc";
import Chat from "@/components/Chat.vue";
import Login from "@/components/Login.vue";
import Register from "@/components/Register.vue";
import { useAccountStore } from "./stores/accountStore";
const ircStore = useIRCStore();
const accountStore = useAccountStore();
const { showRegistration } = storeToRefs(accountStore);
</script>

View File

@@ -10,6 +10,7 @@ const accountStore = useAccountStore();
<template>
<div class="d-flex flex-row" style="height: 100vh">
<router-view />
<v-sheet border class="buffers">
<UserCard />
<v-divider />
@@ -27,16 +28,10 @@ const accountStore = useAccountStore();
:me="accountStore.account.nick"
/>
<v-sheet>
<!-- <v-text-field -->
<!-- variant="outlined" -->
<!-- :placeholder="`Message ${store.activeBufferName}`" -->
<!-- v-model="inputBuffer" -->
<!-- hide-details -->
<!-- class="ma-2" -->
<!-- @keydown.enter.exact.prevent="send" -->
<!-- /> -->
<InputBuffer @send="ircStore.sendActiveBuffer" />
<InputBuffer
@raw="(txt: string) => ircStore.client.raw(txt)"
@send="ircStore.sendActiveBuffer"
/>
</v-sheet>
</div>
<v-sheet class="user-list h-100" border>

View File

@@ -1,10 +1,16 @@
<script setup>
<script setup lang="ts">
import { ref } from "vue";
import { useIRCStore } from "@/stores/irc";
import { useBufferStore } from "@/stores/bufferStore";
const emit = defineEmits(["send"]);
import { useAccountStore } from "@/stores/accountStore";
import { useRouter } from "vue-router";
const emit = defineEmits(["send", "raw"]);
const store = useIRCStore();
const accountStore = useAccountStore();
const { showRegistration } = storeToRefs(accountStore);
const router = useRouter();
const bufferStore = useBufferStore();
const text = ref();
const menu = ref({
@@ -15,7 +21,7 @@ const menu = ref({
const menuList = ref({
density: "compact",
slim: true,
items: [],
items: [] as any[],
itemTitle: "title",
itemValue: "value",
selected: [],
@@ -30,18 +36,26 @@ const completionPos = ref(0);
function clickItem() {}
function send() {
if (!text.value) return;
emit("send", text.value);
if (text.value.slice(0, 2) === "//") {
emit("raw", text.value.substring(2));
} else if (text.value[0] === "/") {
router.push({ path: "/register" });
} else {
emit("send", text.value);
}
menu.value.open = false;
text.value = "";
}
function filterUsers(s) {
function filterUsers(s: string) {
if (store.activeBuffer) {
return store.activeBuffer.users.filter((u) => u.nick.startsWith(s));
}
}
function trigger(ev) {
function trigger(ev: KeyboardEvent) {
const input = ev.target;
const cursor = input.selectionStart;
cursorPos.value = cursor;
@@ -49,7 +63,18 @@ function trigger(ev) {
const textBefore = text.slice(0, cursor);
const mentionMatch = textBefore.match(/@(\w*)$/);
if (mentionMatch) {
if (text[0] === "/") {
menu.value.open = true;
menuList.value.items = [
{
title: "register",
value: "register",
cmd: () => {
showRegistration.value = true;
},
},
];
} else if (mentionMatch) {
menu.value.open = true;
menuList.value.items = filterUsers(mentionMatch[1]);
menuList.value.itemTitle = "nick";

View File

@@ -2,15 +2,21 @@
import { useAccountStore } from "@/stores/accountStore";
import { useIRCStore } from "@/stores/irc";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
const accountStore = useAccountStore();
const ircStore = useIRCStore();
const { account } = storeToRefs(accountStore);
const withAccount = ref(false);
const form = ref(false);
const connecting = ref(false);
const router = useRouter();
function login() {
ircStore.connect();
connecting.value = true;
router.push({ name: "Chat" });
}
function required(v: any) {
@@ -48,7 +54,13 @@ function required(v: any) {
<v-checkbox v-model="withAccount" label="Login with an account" />
</v-card-text>
<v-card-actions>
<v-btn type="submit" color="success" :disabled="!form">Connect</v-btn>
<v-btn
:loading="connecting"
type="submit"
color="success"
:disabled="!form"
>Connect</v-btn
>
</v-card-actions>
</v-form>
</v-card>

View File

@@ -51,6 +51,15 @@ watch(
<span class="message-time" v-if="!!msg.time">{{
formatTime(msg.time)
}}</span>
<v-chip
class="ml-2"
v-bind="props"
label
color="purple-lighten-2"
v-if="msg.kind === 'notice'"
size="x-small"
><v-icon class="mr-2">mdi-eye</v-icon>Only visible to you
</v-chip>
</v-list-item-title>
{{ msg.message }}
</v-list-item></template

View File

@@ -0,0 +1,88 @@
<script lang="ts" setup>
import { HookStatus, useIRCStore } from "@/stores/irc";
import { onBeforeMount, onBeforeUnmount } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const ircStore = useIRCStore();
const emit = defineEmits(["success"]);
const RegistrationSuccess = new RegExp(/Account created/);
function interceptNickServMessage(message: { nick: string; message: string }) {
if (message.nick !== "NickServ") {
return HookStatus.HOOK_OK;
}
registering.value = false;
if (message.message.match(RegistrationSuccess)) {
emit("success");
}
return HookStatus.HOOK_EAT;
}
onBeforeMount(() => {
ircStore.registerHook("message", interceptNickServMessage);
ircStore.registerHook("notice", interceptNickServMessage);
});
onBeforeUnmount(() => {
ircStore.unregisterHook("message", interceptNickServMessage);
ircStore.unregisterHook("notice", interceptNickServMessage);
});
const newAccount = ref({
email: "",
username: "",
password: "",
});
const form = ref(false);
const registering = ref(false);
function register() {
if (!form.value) return;
const { password, email } = newAccount.value;
registering.value = true;
ircStore.client.say("NickServ", `REGISTER ${password} ${email}`);
}
function cancel() {
router.back();
}
const show = ref(true);
</script>
<template>
<v-dialog v-model="show" max-width="400px" persistent>
<v-card title="Account Registration">
<v-form v-model="form" @submit.prevent="register">
<v-card-text>
<v-text-field
:rules="[(v) => !!v || 'Email required']"
v-model="newAccount.email"
label="Email"
role="email"
/>
<v-text-field
:rules="[(v) => !!v || 'Password required']"
v-model="newAccount.password"
label="Password"
role="password"
type="password"
/>
</v-card-text>
<v-card-actions>
<v-btn
:disabled="!form"
type="submit"
:loading="registering"
text="Register"
color="success"
/>
<v-btn @click="cancel">Cancel</v-btn>
</v-card-actions>
</v-form>
</v-card></v-dialog
>
</template>

View File

@@ -1,8 +1,9 @@
import vuetify from "./vuetify";
import pinia from "@/stores";
import router from "./router.ts";
import type { App } from "vue";
export function registerPlugins(app: App) {
app.use(vuetify).use(pinia);
app.use(vuetify).use(router).use(pinia);
}

View File

@@ -0,0 +1,37 @@
import { createMemoryHistory, createRouter } from "vue-router";
import Chat from "@/components/Chat.vue";
import { useIRCStore } from "@/stores/irc";
const routes = [
{
path: "/",
name: "Chat",
component: Chat,
children: [
{
path: "register",
name: "Register",
component: () => import("@/components/Register.vue"),
},
],
},
{
path: "/login",
name: "Login",
component: () => import("@/components/Login.vue"),
},
];
const router = createRouter({
history: createMemoryHistory(),
routes,
});
router.beforeEach(async (to, from) => {
if (!useIRCStore().connected && to.name !== "Login") {
return { name: "Login" };
}
return true;
});
export default router;

View File

@@ -3,6 +3,7 @@ import { ref } from "vue";
export const useAccountStore = defineStore("accountStore", () => {
const authenticated = ref(false);
const showRegistration = ref(false);
const account = ref({
nick: "",
account: "",
@@ -21,5 +22,13 @@ export const useAccountStore = defineStore("accountStore", () => {
account.value.nick = v;
}
return { account, authError, authenticated, setAuthenticated, setNick };
return {
account,
authError,
authenticated,
showRegistration,
setAuthenticated,
setNick,
showRegistration,
};
});

View File

@@ -1,15 +1,50 @@
import { defineStore, storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { Client } from "irc-framework";
import { useBufferStore } from "./bufferStore";
import { ref } from "vue";
import { useAccountStore } from "./accountStore";
export type HookFunction = (event: any) => HookStatus;
export enum HookStatus {
HOOK_OK,
HOOK_EAT,
}
export const useIRCStore = defineStore("ircStore", () => {
const bufferStore = useBufferStore();
const accountStore = useAccountStore();
const connected = ref(false);
const { authError } = storeToRefs(accountStore);
const hooks = {} as Record<string, HookFunction[]>;
function registerHook(event: string, f: HookFunction) {
if (hooks[event]) hooks[event].push(f);
else hooks[event] = [f];
}
function runHook(eventName: string, eventArgs: any): HookStatus {
if (!hooks[eventName]) return HookStatus.HOOK_OK;
let lastRetVal = HookStatus.HOOK_OK;
for (const hookFunction of hooks[eventName]) {
const retVal = hookFunction(eventArgs);
if (retVal === HookStatus.HOOK_EAT) return retVal;
lastRetVal = retVal;
}
return lastRetVal;
}
function unregisterHook(eventName: string, f: HookFunction) {
if (!hooks[eventName]) return;
const idx = hooks[eventName].findIndex((item) => item === f);
if (idx === -1) return;
hooks[eventName].splice(idx, 1);
}
const selfAvatar = ref("https://placekittens.com/128/128");
const bio = ref();
@@ -85,7 +120,6 @@ export const useIRCStore = defineStore("ircStore", () => {
nick: accountStore.account.nick,
};
console.log(connectParams);
client.connect(connectParams);
}
@@ -93,11 +127,12 @@ export const useIRCStore = defineStore("ircStore", () => {
if (!bufferStore.activeBuffer) {
return;
}
bufferStore.activeBuffer.channel.say(message);
if (bufferStore.activeBuffer.channel)
bufferStore.activeBuffer.channel.say(message);
else client.say(bufferStore.activeBuffer.name, message);
}
function isMe(target: string) {
console.log(client.user.nick);
return target === client.user.nick;
}
@@ -129,8 +164,10 @@ export const useIRCStore = defineStore("ircStore", () => {
},
);
const router = useRouter();
client.on("registered", function () {
connected.value = true;
router.push({ name: "Chat" });
client.list();
client.raw("METADATA * SUB avatar");
client.raw("METADATA * SUB bio");
@@ -207,14 +244,22 @@ export const useIRCStore = defineStore("ircStore", () => {
client.on("message", function (message: { nick: string; target: string }) {
let buffer;
const retVal = runHook("message", message);
if (retVal === HookStatus.HOOK_EAT) return;
if (message.nick === "HistServ") return;
if (isMe(message.target)) {
buffer = bufferStore.getBuffer(message.nick);
} else {
buffer = bufferStore.getBuffer(message.target);
}
if (!buffer) {
return;
buffer = bufferStore.addBuffer(message.nick, {
name: message.nick,
channel: null,
});
}
buffer.messages.push(message);
@@ -227,6 +272,14 @@ export const useIRCStore = defineStore("ircStore", () => {
}
});
client.on("notice", function (message) {
const retVal = runHook("notice", message);
if (retVal === HookStatus.HOOK_EAT) return;
if (bufferStore.activeBuffer) {
bufferStore.activeBuffer.messages.push({ ...message, kind: "notice" });
}
});
client.on("join", ({ nick, channel }: { nick: string; channel: string }) => {
if (isMe(nick)) {
bufferStore.addBuffer(channel, {
@@ -294,5 +347,7 @@ export const useIRCStore = defineStore("ircStore", () => {
setBio,
bio,
connected,
registerHook,
unregisterHook,
};
});