Skip to content

Commit 9359cea

Browse files
committed
Updated channel, user list, and chat stores for better clarity and chat input handling
- Emphasize differences between regular channels and user channels - Add ability to get user by username or by nick - Add markdown renderer in message display - Add initial commands for /me, /away, /unaway, /size
1 parent 5a95b1a commit 9359cea

23 files changed

Lines changed: 430 additions & 153 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"#services/*": "./src/services/*",
2929
"#stores": "./src/stores/index.ts",
3030
"#stores/*": "./src/stores/*",
31+
"#styles/*": "./src/styles/*",
3132
"#utils": "./src/utils/index.ts",
3233
"#utils/*": "./src/utils/*"
3334
},

src/commands/handlers/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
export * as channel from './channel';
2-
export * as help from './help';
3-
export * as operator from './operator';
4-
export * as settings from './settings';
5-
export * as user from './user';
1+
export * from './channel';
2+
export * from './help';
3+
export * from './operator';
4+
export * from './settings';
5+
export * from './user';

src/commands/handlers/settings.ts

Lines changed: 16 additions & 2 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
* Change custom emoticon slot.
@@ -20,7 +20,21 @@ export const indicator: TODO = null;
2020
* `/size <newFontSize>`
2121
* Aliases: `/textsize`, `/fontsize`
2222
*/
23-
export const size: TODO = null;
23+
export const size: CommandHandler = {
24+
name: 'size',
25+
aliases: ['textsize', 'fontsize'],
26+
description: 'Update font size (px). Min: 10; Max: 18.',
27+
usage: '/size <number>',
28+
examples: ['/size 14'],
29+
execute({ target, args, stores }) {
30+
const value = Number.parseInt(args[0], 10);
31+
if (Number.isNaN(value)) {
32+
stores.channel.addSystemMessage(target, `${args[0]} is not a number`);
33+
return;
34+
}
35+
stores.settings.setFontSize(value);
36+
},
37+
};
2438
/**
2539
* View/update notification keywords.
2640
* - `/keywords`

src/commands/handlers/user.ts

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

3-
/**
4-
* Post message as an action.
5-
* `/me <message>`
6-
*/
7-
export const me: TODO = null;
3+
export const me: CommandHandler = {
4+
name: 'me',
5+
description: 'Posts message formatted as an action.',
6+
usage: '/me <message>',
7+
examples: ['/me laughs'],
8+
execute({ target, fullText, sendMessage }) {
9+
sendMessage(target, fullText, 'action');
10+
},
11+
};
812
/**
913
* Hide messages from a user.
1014
* `/ignore <username>`
@@ -24,16 +28,23 @@ export const unignore: TODO = null;
2428
* `/disconnect`
2529
*/
2630
export const disconnect: TODO = null;
27-
/**
28-
* Set self as away.
29-
*
30-
* `/away <reason>`
31-
* If `<reason>` is not given, the default away message is provided instead.
32-
*/
33-
export const away: TODO = null;
34-
/**
35-
* Set self as back.
36-
*
37-
* `/unaway`
38-
*/
39-
export const unaway: TODO = null;
31+
32+
export const away: CommandHandler = {
33+
name: 'away',
34+
description: 'Set self as away. A default away message is sent if no reason is provide.',
35+
usage: '/away [reason]',
36+
examples: ['/away', '/away Lunch'],
37+
execute({ fullText, stores }) {
38+
stores.irc.client?.raw(`AWAY ${fullText.trim() || 'User is currently away'}`);
39+
},
40+
};
41+
42+
export const unaway: CommandHandler = {
43+
name: 'unaway',
44+
aliases: ['back'],
45+
description: 'Set self as unaway/back.',
46+
usage: '/unaway',
47+
execute({ stores }) {
48+
stores.irc.client?.raw('AWAY');
49+
},
50+
};

src/commands/index.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
import log from 'loglevel';
2-
import * as Handlers from './handlers';
1+
import type { CommandHandler } from '#models';
2+
import * as handlers from './handlers';
33

4-
log.debug(Handlers);
4+
export function createCommandRegistry() {
5+
const registry = new Map<string, CommandHandler>();
6+
for (const handler of Object.values(handlers)) {
7+
if (!handler) {
8+
continue;
9+
}
10+
// TODO: Check for accidental command name collisions
11+
// Add commands and their aliases to the registry
12+
registry.set(handler.name, handler);
13+
handler.aliases?.forEach((alias) => registry.set(alias, handler));
14+
}
15+
return registry;
16+
}

src/components/ChatApp.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<DraggableWindow>
33
<template v-slot:header>
4-
<span>{{ channel.activeChannelName }}</span>
4+
<span>{{ channel.currentChannelName }}</span>
55
</template>
66
<template v-slot:main>
77
<OnlineUsers />
@@ -33,7 +33,7 @@
3333
for (const channelName of AUTO_JOIN_CHANNELS) {
3434
channel.joinChannel(channelName);
3535
}
36-
channel.changeActiveChannel(AUTO_JOIN_CHANNELS[0]);
36+
channel.goToChannel(AUTO_JOIN_CHANNELS[0]);
3737
});
3838
</script>
3939

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
<template>
2-
<MessageGroup v-for="group in messageGroups" :key="group.id" :message-group="group" />
2+
<div class="channel-messages d-flex flex-column" :style="{ fontSize }">
3+
<MessageGroup v-for="group in messageGroups" :key="group.id" :message-group="group" />
4+
</div>
35
</template>
46

57
<script setup lang="ts">
68
import { useMessageGroups } from '#composables/useMessageGroups.ts';
7-
import { useChannelStore } from '#stores';
9+
import { useChannelStore, useSettingsStore } from '#stores';
10+
import { computed } from 'vue';
811
import MessageGroup from './MessageGroup.vue';
912
1013
const channel = useChannelStore();
11-
const { messageGroups } = useMessageGroups(() => channel.activeMessages);
14+
const settings = useSettingsStore();
15+
16+
const fontSize = computed(() => `${settings.fontSize}px`);
17+
const { messageGroups } = useMessageGroups(() => channel.currentMessages);
1218
</script>
1319

14-
<style scoped></style>
20+
<style scoped>
21+
.channel-messages {
22+
gap: 0.5em;
23+
}
24+
</style>
Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
<template>
2-
<UsernameDisplay :nick="messageGroup.nick" />
3-
<MessageGroupItem v-for="message in messageGroup.messages" :key="message.id" :message="message" />
2+
<UsernameDisplay v-if="messageGroup.type !== 'system'" :username="messageGroup.username" />
3+
<div class="messages d-flex flex-column">
4+
<MessageGroupItem
5+
v-for="message in messageGroup.messages"
6+
:key="message.id"
7+
:message="message"
8+
/>
9+
</div>
410
</template>
511

612
<script setup lang="ts">
@@ -10,4 +16,8 @@
1016
defineProps<{ messageGroup: MessageGroup }>();
1117
</script>
1218

13-
<style scoped></style>
19+
<style scoped>
20+
.messages {
21+
gap: 0.25em;
22+
}
23+
</style>
Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,105 @@
11
<template>
2-
<time>[{{ formatTime(message.time) }}]</time>
3-
<p>{{ formattedMessage }}</p>
2+
<div
3+
v-if="message.type === 'system'"
4+
class="message-container d-flex flex-row flex-nowrap justify-content-center"
5+
>
6+
<p class="message-content m-0 message-type--system">
7+
{{ props.message.message }}
8+
</p>
9+
</div>
10+
<div v-else class="message-container d-flex flex-row flex-nowrap justify-content-between">
11+
<div
12+
class="message-content"
13+
:class="{
14+
'message-status--error': message.status === 'error',
15+
'message-status--pending': message.status === 'pending',
16+
'message-status--sent': message.status === 'sent',
17+
'message-type--action': message.type === 'action',
18+
'message-type--notice': message.type === 'notice',
19+
'message-type--privmsg': message.type === 'privmsg',
20+
}"
21+
v-html="formattedMessage"
22+
/>
23+
<time v-if="message.type !== 'notice'" class="message-timestamp flex-shrink-0"
24+
>[{{ formatTime(message.time) }}]</time
25+
>
26+
</div>
427
</template>
528

629
<script setup lang="ts">
730
import type { Message } from '#models';
8-
import { formatTime } from '#utils';
31+
import { formatTime, md } from '#utils';
932
import { computed } from 'vue';
1033
1134
const props = defineProps<{ message: Message }>();
12-
// TODO: Use markdown parser for `privmsg` entries
13-
const formattedMessage = computed(() => props.message.message);
35+
const formattedMessage = computed(() => {
36+
if (props.message.type !== 'system') {
37+
// TODO: Add event handlers via event delegation
38+
return md.renderInline(props.message.message);
39+
}
40+
return '';
41+
});
1442
</script>
1543

16-
<style scoped></style>
44+
<style scoped lang="scss">
45+
@import '#styles/_variables.scss';
46+
47+
.message-container {
48+
gap: 1em;
49+
}
50+
51+
.message-status--pending {
52+
opacity: 0.5;
53+
}
54+
.message-type--action {
55+
font-style: italic;
56+
}
57+
.message-type--system {
58+
font-style: italic;
59+
}
60+
61+
:deep(.message-content) {
62+
word-break: break-all;
63+
64+
.screenshot {
65+
object-fit: contain;
66+
max-width: 500px;
67+
width: 100%;
68+
}
69+
.cursive {
70+
font-family: cursive;
71+
}
72+
.serif {
73+
font-family: serif;
74+
}
75+
.highlight {
76+
background-color: yellow;
77+
color: black;
78+
}
79+
blockquote {
80+
display: inline;
81+
border-left: 5px solid gray;
82+
padding-left: 0.125em;
83+
quotes: '' '' '' '';
84+
}
85+
blockquote:before {
86+
margin-left: 2px;
87+
content: open-quote;
88+
}
89+
blockquote:after {
90+
content: close-quote;
91+
}
92+
mark {
93+
background-color: darken($dark-blue, 5%) !important;
94+
color: #c0dce7;
95+
}
96+
mark:hover {
97+
color: white;
98+
cursor: pointer;
99+
}
100+
}
101+
102+
.message-timestamp {
103+
font-size: 0.8em;
104+
}
105+
</style>

src/components/chat/ChatInput.vue

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,14 @@
2828
const channel = useChannelStore();
2929
const chat = useChatStore();
3030
const inputBuffer = ref('');
31-
const placeholder = computed(() => `Message ${channel.activeChannelName}`);
31+
const placeholder = computed(() => `Message ${channel.currentChannelName}`);
3232
3333
function onSubmit() {
3434
const text = inputBuffer.value.trim();
3535
if (!text) {
3636
return;
3737
}
38-
39-
const isCommand = text.startsWith('/');
40-
41-
if (isCommand) {
42-
// TODO: Add command handler
43-
} else {
44-
chat.say(text);
45-
}
46-
38+
chat.handleUserInput(inputBuffer.value);
4739
inputBuffer.value = '';
4840
}
4941
</script>

0 commit comments

Comments
 (0)