Skip to content

Commit 585cb22

Browse files
committed
Add subset of operator-only, settings, and user slash-commands
- Adjust styles for channel messages and message types - Fix select flickering for DraggableWindow component - Fix styling for user profile popover
1 parent c1cd396 commit 585cb22

16 files changed

Lines changed: 388 additions & 135 deletions

File tree

src/commands/handlers/channel.ts

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,70 @@
1-
import type { TODO } from '#models';
1+
import type { CommandHandler } from '#models';
22

3-
/**
4-
* Join a channel.
5-
* `/join <channel>`
6-
*/
7-
export const join: TODO = null;
8-
/**
9-
* Leave a channel.
10-
* `/leave <channel>`
11-
*/
12-
export const leave: TODO = null;
13-
/**
14-
* Switch channel
15-
* `/goto <channel>`
16-
* `/goto #general`
17-
*/
18-
export const goto: TODO = null;
3+
export const join: CommandHandler = {
4+
name: 'join',
5+
description: 'Join and go to a channel.',
6+
usage: '/join <channel>',
7+
examples: ['/join #random'],
8+
execute({ currentChannel, args, stores }) {
9+
if (args.length === 0) {
10+
stores.channel.addSystemMessage(currentChannel, 'Select a channel using /join <channel>');
11+
}
12+
13+
const inputChannel = args[0];
14+
if (!inputChannel.startsWith('#')) {
15+
stores.channel.addSystemMessage(
16+
currentChannel,
17+
'Selected channel must start with a "#" sign.',
18+
);
19+
return;
20+
}
21+
stores.channel.joinChannel(inputChannel);
22+
},
23+
};
24+
25+
export const leave: CommandHandler = {
26+
name: 'leave',
27+
description: 'Leave a channel. Defaults to current channel.',
28+
usage: '/leave [channel]',
29+
examples: ['/leave', '/leave #random', '/leave user'],
30+
execute({ currentChannel, args, stores }) {
31+
if (args.length === 0) {
32+
stores.channel.leaveChannel(currentChannel);
33+
return;
34+
}
35+
36+
const inputChannel = args[0];
37+
stores.channel.leaveChannel(inputChannel);
38+
},
39+
};
40+
41+
export const goto: CommandHandler = {
42+
name: 'goto',
43+
description: 'Switch channel.',
44+
usage: '/goto <channel>',
45+
examples: ['/goto #general', '#goto username'],
46+
execute({ currentChannel, args, stores }) {
47+
if (args.length === 0) {
48+
stores.channel.addSystemMessage(currentChannel, 'Select a channel using /goto <channel>');
49+
stores.channel.addSystemMessage(
50+
currentChannel,
51+
`Channels currently available are: ${stores.channel.channelList.join(', ')}`,
52+
);
53+
return;
54+
}
55+
56+
const inputChannel = args[0];
57+
if (inputChannel.toLocaleLowerCase().localeCompare(currentChannel.toLocaleLowerCase()) === 0) {
58+
stores.channel.addSystemMessage(currentChannel, `You are already in ${inputChannel}`);
59+
return;
60+
}
61+
const targetChannel = stores.channel.channelList.find(
62+
(c) => inputChannel.toLocaleLowerCase().localeCompare(c.toLocaleLowerCase()) === 0,
63+
);
64+
if (!targetChannel) {
65+
stores.channel.addSystemMessage(currentChannel, `Unknown channel: ${inputChannel}`);
66+
return;
67+
}
68+
stores.channel.goToChannel(targetChannel);
69+
},
70+
};

src/commands/handlers/operator.ts

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TODO } from '#models';
1+
import type { CommandHandler, TODO } from '#models';
22

33
/**
44
* Ban user from channel(s).
@@ -24,12 +24,37 @@ export const quiet: TODO = null;
2424
* `/unquiet * <username>`
2525
*/
2626
export const unquiet: TODO = null;
27-
/**
28-
* Send notice to channel(s).
29-
* `/notice <channel> <message>`
30-
* `/notice * <message>`
31-
*/
32-
export const notice: TODO = null;
27+
28+
export const notice: CommandHandler = {
29+
name: 'notice',
30+
description: 'Send notice to specified channel or all channels if * is specified.',
31+
usage: '/notice <channel> <message>',
32+
examples: ['/notice #general Hello, everyone', '/notice * Hello, everyone'],
33+
requiresOperator: true,
34+
execute({ currentChannel, args, sendMessage, stores }) {
35+
if (args.length < 2) {
36+
stores.channel.addSystemMessage(
37+
currentChannel,
38+
'Please provide a channel and the notice message',
39+
);
40+
return;
41+
}
42+
43+
const text = args.slice(1).join(' ');
44+
const targetChannels: string[] = [];
45+
if (args[0] === '*') {
46+
stores.channel.channelList.forEach((channel) => {
47+
targetChannels.push(channel);
48+
});
49+
} else {
50+
targetChannels.push(args[0]);
51+
}
52+
53+
targetChannels.forEach((channel) => {
54+
sendMessage(channel, text, 'notice');
55+
});
56+
},
57+
};
3358
/**
3459
* Ban hostmask (form: `user!nick@host`).
3560
* `/banmask <channel> <mask>`
@@ -40,16 +65,52 @@ export const banmask: TODO = null;
4065
* `/unbanmask <channel> <mask>`
4166
*/
4267
export const unbanmask: TODO = null;
43-
/**
44-
* Get username by nick.
45-
* `/user <nick>`
46-
*/
47-
export const user: TODO = null;
48-
/**
49-
* Get nicks by username.
50-
* `/nicks <username>`
51-
*/
52-
export const nicks: TODO = null;
68+
69+
export const user: CommandHandler = {
70+
name: 'user',
71+
description: 'Get username by nick.',
72+
requiresOperator: true,
73+
usage: '/user <nick>',
74+
execute({ currentChannel, args, stores }) {
75+
if (args.length === 0) {
76+
stores.channel.addSystemMessage(currentChannel, '"/user" requires a <nick> argument.');
77+
return;
78+
}
79+
const nick = args[0];
80+
const user = stores.userList.getUserByNick(nick);
81+
if (!user) {
82+
stores.channel.addSystemMessage(currentChannel, `"${nick}" not found.`);
83+
} else {
84+
stores.channel.addSystemMessage(currentChannel, `"${nick}" username: ${user.username}`);
85+
}
86+
},
87+
};
88+
89+
export const nicks: CommandHandler = {
90+
name: 'nicks',
91+
description: 'Get nicks by username.',
92+
usage: '/nicks <username>',
93+
requiresOperator: true,
94+
execute({ currentChannel, args, stores }) {
95+
if (args.length === 0) {
96+
stores.channel.addSystemMessage(currentChannel, '"/nicks" requires a <username> argument.');
97+
return;
98+
}
99+
100+
const username = args[0];
101+
const user = stores.userList.getUserByUsername(username);
102+
if (!user) {
103+
stores.channel.addSystemMessage(currentChannel, `"${username}" not found.`);
104+
} else if (user.status === 'offline') {
105+
stores.channel.addSystemMessage(currentChannel, `"${username}" is currently offline.`);
106+
} else {
107+
stores.channel.addSystemMessage(
108+
currentChannel,
109+
`"${username}" nicks: ${Array.from(user.nicks).join(', ')}`,
110+
);
111+
}
112+
},
113+
};
53114
/**
54115
* Kick user from channel(s).
55116
* `/kick <channel> <username> [reason]`
@@ -65,8 +126,16 @@ export const banlist: TODO = null;
65126
* `/changenick <newNick>`
66127
*/
67128
export const changenick: TODO = null;
68-
/**
69-
* Allow operator to send a RAW command through the websocket.
70-
* `/execute <line>`
71-
*/
72-
export const execute: TODO = null;
129+
130+
export const execute: CommandHandler = {
131+
name: 'execute',
132+
description:
133+
'Allow operator to send an IRC command directly through the websocket. For using commands not yet available in the client.',
134+
usage: '/execute <IRC commands>',
135+
requiresOperator: true,
136+
execute({ fullText, stores }) {
137+
if (fullText.length > 0) {
138+
stores.irc.client?.raw(fullText);
139+
}
140+
},
141+
};

src/commands/handlers/settings.ts

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import type { CommandHandler, TODO } from '#models';
2+
import {
3+
getNamedColor,
4+
isAccessibleColor,
5+
isAccessibleHexColor,
6+
NAMED_COLORS,
7+
rgbToHex,
8+
} from '#utils';
29

310
/**
411
* Change custom emoticon slot.
@@ -26,7 +33,7 @@ export const size: CommandHandler = {
2633
description: 'Update font size (px). Min: 10; Max: 18.',
2734
usage: '/size <number>',
2835
examples: ['/size 14'],
29-
execute({ target, args, stores }) {
36+
execute({ currentChannel: target, args, stores }) {
3037
const value = Number.parseInt(args[0], 10);
3138
if (Number.isNaN(value)) {
3239
stores.channel.addSystemMessage(target, `${args[0]} is not a number`);
@@ -48,13 +55,63 @@ export const keywords: TODO = null;
4855
* - `/notifications disable <channel>`
4956
*/
5057
export const notifications: TODO = null;
51-
/**
52-
* Updates username color.
53-
* - `/color <hexColor>`
54-
* - `/color <r>, <g>, <b>`
55-
* - `/color <namedColor>`
56-
*/
57-
export const color: TODO = null;
58+
59+
export const color: CommandHandler = {
60+
name: 'color',
61+
description:
62+
'Set username color. Supports hex, RGB, or a preset color name. Ignores unreadable colors.',
63+
usage: '/color <color>',
64+
examples: ['/color #ffffff', '/color 255 255 255', '/color orange'],
65+
execute({ currentChannel, args, stores }) {
66+
if (args.length === 0) {
67+
stores.channel.addSystemMessage(currentChannel, '"/color" requires a color input');
68+
return;
69+
}
70+
71+
let inputColor: string;
72+
73+
if (args.length >= 3) {
74+
const r = Number.parseInt(args[0], 10);
75+
const g = Number.parseInt(args[1], 10);
76+
const b = Number.parseInt(args[2], 10);
77+
78+
if (!isAccessibleColor(r, g, b)) {
79+
// RGB input
80+
stores.channel.addSystemMessage(
81+
currentChannel,
82+
`${r}, ${g}, ${b} is not a readable color.`,
83+
);
84+
return;
85+
}
86+
inputColor = rgbToHex(r, g, b);
87+
} else if (args[0].startsWith('#')) {
88+
// HEX input
89+
if (!isAccessibleHexColor(args[0])) {
90+
stores.channel.addSystemMessage(currentChannel, `${args[0]} is not a readable color.`);
91+
return;
92+
}
93+
inputColor = args[0];
94+
} else {
95+
// Named color input
96+
const namedColor = getNamedColor(args[0]);
97+
if (!namedColor) {
98+
stores.channel.addSystemMessage(currentChannel, `${args[0]} is not a preset color.`);
99+
stores.channel.addSystemMessage(
100+
currentChannel,
101+
`Available colors: ${Object.keys(NAMED_COLORS).join(', ')}`,
102+
);
103+
return;
104+
}
105+
106+
inputColor = namedColor;
107+
}
108+
109+
stores.settings.usernameColor = inputColor;
110+
if (currentChannel.startsWith('#')) {
111+
stores.irc.client?.tagmsg(currentChannel, { ['+color']: inputColor });
112+
}
113+
},
114+
};
58115
/**
59116
* Toggle chat feature.
60117
* `/toolbar enable <chatFeature>`

src/commands/handlers/user.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ export const me: CommandHandler = {
55
description: 'Posts message formatted as an action.',
66
usage: '/me <message>',
77
examples: ['/me laughs'],
8-
execute({ target, fullText, sendMessage }) {
9-
sendMessage(target, fullText, 'action');
8+
execute({ currentChannel, fullText, sendMessage }) {
9+
sendMessage(currentChannel, fullText, 'action');
1010
},
1111
};
1212
/**
@@ -35,7 +35,7 @@ export const away: CommandHandler = {
3535
usage: '/away [reason]',
3636
examples: ['/away', '/away Lunch'],
3737
execute({ fullText, stores }) {
38-
stores.irc.client?.raw(`AWAY ${fullText.trim() || 'User is currently away'}`);
38+
stores.irc.client?.raw(`AWAY :${fullText.trim() || 'User is currently away'}`);
3939
},
4040
};
4141

src/components/ChatApp.vue

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
11
<template>
2-
<DraggableWindow>
3-
<template v-slot:header>
4-
<span>{{ channel.currentChannelName }}</span>
5-
</template>
6-
<template v-slot:main>
7-
<OnlineUsers />
8-
<ChannelMessages />
9-
<ChatInput />
10-
</template>
11-
</DraggableWindow>
2+
<div class="chat-app" :style="{ fontSize: `${settings.fontSize}px` }">
3+
<DraggableWindow>
4+
<template v-slot:header>
5+
<span>{{ channel.currentChannelName }}</span>
6+
</template>
7+
<template v-slot:main>
8+
<div class="channel d-flex flex-column">
9+
<ChannelMessages class="flex-grow-1 flex-shrink-1" />
10+
<ChatInput class="flex-grow-1 flex-shrink-0" />
11+
</div>
12+
</template>
13+
</DraggableWindow>
14+
</div>
1215
</template>
1316

1417
<script setup lang="ts">
1518
import { AUTO_JOIN_CHANNELS } from '#constants';
16-
import { useChannelStore, useIrcStore } from '#stores';
19+
import { useChannelStore, useIrcStore, useSettingsStore } from '#stores';
1720
import { ref, watch } from 'vue';
1821
import ChannelMessages from './channel/ChannelMessages.vue';
1922
import DraggableWindow from './layout/DraggableWindow.vue';
20-
import OnlineUsers from './user/OnlineUsers.vue';
2123
import ChatInput from './chat/ChatInput.vue';
2224
// TODO: Re-implement resizing tracking
2325
2426
const irc = useIrcStore();
2527
const channel = useChannelStore();
28+
const settings = useSettingsStore();
2629
const joined = ref(false);
2730
2831
watch([joined, () => irc.connectionStatus], ([hasJoined, connectionStatus]) => {

0 commit comments

Comments
 (0)