mirror of
https://github.com/thelounge/thelounge.git
synced 2024-06-03 14:22:19 +02:00
Merge d03ec1ef1d
into 549c445853
This commit is contained in:
commit
ff0cc5ce03
|
@ -1,4 +1,20 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="
|
||||
channel.users.filter((user) => user.isTyping && user.nick !== network.nick).length > 0
|
||||
"
|
||||
id="activeTypers"
|
||||
>
|
||||
<span
|
||||
v-for="(user, index) in channel.users.filter(
|
||||
(user) => user.isTyping && user.nick !== network.nick
|
||||
)"
|
||||
>
|
||||
<span v-if="index != 0">, </span>
|
||||
<Username :user="user" :key="user.nick + '-typing'" />
|
||||
</span>
|
||||
is typing...
|
||||
</span>
|
||||
<form id="form" method="post" action="" @submit.prevent="onSubmit">
|
||||
<span id="upload-progressbar" />
|
||||
<span id="nick">{{ network.nick }}</span>
|
||||
|
@ -63,6 +79,9 @@ import eventbus from "../js/eventbus";
|
|||
import {watch, defineComponent, nextTick, onMounted, PropType, ref, onUnmounted} from "vue";
|
||||
import type {ClientNetwork, ClientChan} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
import {TypingStatus} from "../../server/models/client-tags";
|
||||
import Username from "./Username.vue";
|
||||
import _ from "lodash";
|
||||
|
||||
const formattingHotkeys = {
|
||||
"mod+k": "\x03",
|
||||
|
@ -91,6 +110,9 @@ const bracketWraps = {
|
|||
|
||||
export default defineComponent({
|
||||
name: "ChatInput",
|
||||
components: {
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
|
@ -123,9 +145,35 @@ export default defineComponent({
|
|||
});
|
||||
};
|
||||
|
||||
const sendTypingNotification = _.debounce(
|
||||
(status) => {
|
||||
const {channel} = props;
|
||||
|
||||
if (channel.type === "channel" || channel.type === "query") {
|
||||
console.log("emitting", status, Date.now());
|
||||
console.trace();
|
||||
socket.emit("input:typing", {target: props.channel.id, status});
|
||||
}
|
||||
},
|
||||
3 * 1000,
|
||||
{leading: true, trailing: false}
|
||||
); // At least, every 3 seconds
|
||||
|
||||
const setPendingMessage = (e: Event) => {
|
||||
props.channel.pendingMessage = (e.target as HTMLInputElement).value;
|
||||
props.channel.inputHistoryPosition = 0;
|
||||
|
||||
if (input.value?.value.length === 0) {
|
||||
sendTypingNotification(TypingStatus.DONE);
|
||||
} else if (
|
||||
input.value &&
|
||||
input.value.value.length > 0 &&
|
||||
input.value.value[0] !== "/"
|
||||
) {
|
||||
console.log("send active", Date.now());
|
||||
sendTypingNotification(TypingStatus.ACTIVE);
|
||||
}
|
||||
|
||||
setInputSize();
|
||||
};
|
||||
|
||||
|
@ -165,6 +213,7 @@ export default defineComponent({
|
|||
props.channel.inputHistoryPosition = 0;
|
||||
props.channel.pendingMessage = "";
|
||||
input.value.value = "";
|
||||
sendTypingNotification.cancel();
|
||||
setInputSize();
|
||||
|
||||
// Store new message in history if last message isn't already equal
|
||||
|
@ -218,6 +267,10 @@ export default defineComponent({
|
|||
if (autocompletionRef.value) {
|
||||
autocompletionRef.value.hide();
|
||||
}
|
||||
|
||||
if (input.value?.value.length > 0 && input.value?.value[0] !== "/") {
|
||||
sendTypingNotification(TypingStatus.PAUSED);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
|
|
|
@ -86,6 +86,18 @@
|
|||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<h2>IRCv3 features</h2>
|
||||
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="store.state.settings.enableTypingNotifications"
|
||||
type="checkbox"
|
||||
name="enableTypingNotifications"
|
||||
/>
|
||||
Send typing notifications to others
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -2198,6 +2198,12 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
|||
transition: 0.3s width ease-in-out;
|
||||
}
|
||||
|
||||
#activeTypers {
|
||||
border-top: 1px solid #e7e7e7;
|
||||
padding: 6px;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
#form {
|
||||
flex: 0 0 auto;
|
||||
border: 0;
|
||||
|
|
|
@ -61,6 +61,10 @@ const defaultConfig = {
|
|||
default: "",
|
||||
sync: "always",
|
||||
},
|
||||
enableTypingNotifications: {
|
||||
default: false,
|
||||
sync: "never",
|
||||
},
|
||||
links: {
|
||||
default: true,
|
||||
},
|
||||
|
|
|
@ -25,3 +25,4 @@ import "./history_clear";
|
|||
import "./mentions";
|
||||
import "./search";
|
||||
import "./mute_changed";
|
||||
import "./typing";
|
||||
|
|
|
@ -4,6 +4,7 @@ import {cleanIrcMessage} from "../../../shared/irc";
|
|||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {ClientChan, ClientMention, ClientMessage, NetChan} from "../types";
|
||||
import {MessageType} from "../../../server/models/msg";
|
||||
|
||||
let pop;
|
||||
|
||||
|
@ -66,6 +67,15 @@ socket.on("msg", function (data) {
|
|||
}
|
||||
}
|
||||
|
||||
// Reset typing indicator
|
||||
if (data.msg.type === MessageType.MESSAGE) {
|
||||
const user = channel.users.find((u) => u.nick === data.msg.from.nick);
|
||||
|
||||
if (user) {
|
||||
user.stopTyping();
|
||||
}
|
||||
}
|
||||
|
||||
channel.messages.push(data.msg);
|
||||
|
||||
if (data.msg.self) {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import socket from "../socket";
|
||||
import {User} from "../../../server/models/user";
|
||||
import {store} from "../store";
|
||||
|
||||
socket.on("names", function (data) {
|
||||
const netChan = store.getters.findChannel(data.id);
|
||||
|
||||
if (netChan) {
|
||||
netChan.channel.users = data.users;
|
||||
netChan.channel.users = data.users?.map((u) => new User(u));
|
||||
}
|
||||
});
|
||||
|
|
31
client/js/socket-events/typing.ts
Normal file
31
client/js/socket-events/typing.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {User} from "../../../server/models/user";
|
||||
import {TypingStatus} from "../../../server/models/client-tags";
|
||||
|
||||
type ServerTypingNotification = {
|
||||
status: TypingStatus;
|
||||
from: Partial<User>;
|
||||
chanId: number;
|
||||
};
|
||||
|
||||
socket.on("channel:isTyping", (data: ServerTypingNotification) => {
|
||||
const receivingChannel = store.getters.findChannel(data.chanId);
|
||||
|
||||
if (!receivingChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = receivingChannel.channel;
|
||||
const user = channel.users.find((u) => u.nick === data.from.nick);
|
||||
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status !== TypingStatus.DONE) {
|
||||
user.startTyping(data.status);
|
||||
} else {
|
||||
user.stopTyping();
|
||||
}
|
||||
});
|
|
@ -36,6 +36,7 @@
|
|||
"../server/models/user.ts",
|
||||
"../server/models/msg.ts",
|
||||
"../server/models/prefix.ts",
|
||||
"../server/models/client-tags.ts",
|
||||
"./js/helpers/fullnamemap.json",
|
||||
"./js/helpers/simplemap.json",
|
||||
"../webpack.config.ts",
|
||||
|
|
|
@ -8,6 +8,7 @@ import colors from "chalk";
|
|||
import log from "./log";
|
||||
import Chan, {ChanConfig, Channel, ChanType} from "./models/chan";
|
||||
import Msg, {MessageType, UserInMessage} from "./models/msg";
|
||||
import {ClientTagKey, TypingStatus} from "./models/client-tags";
|
||||
import Config from "./config";
|
||||
import {condensedTypes} from "../shared/irc";
|
||||
|
||||
|
@ -447,6 +448,22 @@ class Client {
|
|||
});
|
||||
}
|
||||
|
||||
setTyping({target, status}: {target: string; status: TypingStatus}) {
|
||||
const targetNode = this.find(+target);
|
||||
|
||||
if (!targetNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const irc = targetNode.network.irc;
|
||||
|
||||
if (irc && irc.connection && irc.connection.connected) {
|
||||
irc!.tagmsg(targetNode.chan.name, {
|
||||
[`+${ClientTagKey.TYPING}`]: status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
input(data) {
|
||||
const client = this;
|
||||
data.text.split("\n").forEach((line) => {
|
||||
|
|
|
@ -189,7 +189,7 @@ class Chan {
|
|||
return this.users.get(nick.toLowerCase());
|
||||
}
|
||||
getUser(nick: string) {
|
||||
return this.findUser(nick) || new User({nick}, new Prefix([]));
|
||||
return this.findUser(nick) || new User({nick});
|
||||
}
|
||||
setUser(user: User) {
|
||||
this.users.set(user.nick.toLowerCase(), user);
|
||||
|
|
42
server/models/client-tags.ts
Normal file
42
server/models/client-tags.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import _ from "lodash";
|
||||
|
||||
export enum ClientTagKey {
|
||||
// https://ircv3.net/specs/client-tags/reply
|
||||
DRAFT_REPLY = "draft/reply",
|
||||
// https://ircv3.net/specs/client-tags/react
|
||||
DRAFT_REACT = "draft/react",
|
||||
// https://ircv3.net/specs/client-tags/channel-context
|
||||
DRAFT_CHANNEL_CONTEXT = "draft/channel-context",
|
||||
|
||||
// https://ircv3.net/specs/client-tags/typing.html
|
||||
TYPING = "typing",
|
||||
}
|
||||
|
||||
export enum TypingStatus {
|
||||
ACTIVE = "active",
|
||||
PAUSED = "paused",
|
||||
DONE = "done",
|
||||
}
|
||||
|
||||
export class ClientTags {
|
||||
reaction?: string;
|
||||
repliedTo?: string;
|
||||
channelContext?: string;
|
||||
rawTags: Record<string, string>;
|
||||
|
||||
public constructor(rawClientTags: Record<string, string>) {
|
||||
this.rawTags = rawClientTags;
|
||||
|
||||
this.reaction = this.get(ClientTagKey.DRAFT_REACT);
|
||||
this.repliedTo = this.get(ClientTagKey.DRAFT_REPLY);
|
||||
this.channelContext = this.get(ClientTagKey.DRAFT_CHANNEL_CONTEXT);
|
||||
}
|
||||
|
||||
public get(key: string): string | undefined {
|
||||
return this.rawTags[`+${key}`];
|
||||
}
|
||||
|
||||
public has(key: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(this.rawTags, `+${key}`);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import _ from "lodash";
|
||||
import {LinkPreview} from "../plugins/irc-events/link";
|
||||
import User from "./user";
|
||||
import {ClientTags} from "./client-tags";
|
||||
|
||||
export type UserInMessage = Partial<User> & {
|
||||
mode: string;
|
||||
|
@ -18,6 +19,7 @@ export enum MessageType {
|
|||
LOGIN = "login",
|
||||
LOGOUT = "logout",
|
||||
MESSAGE = "message",
|
||||
TAGMSG = "tagmsg",
|
||||
MODE = "mode",
|
||||
MODE_CHANNEL = "mode_channel",
|
||||
MODE_USER = "mode_user", // RPL_UMODEIS
|
||||
|
@ -61,6 +63,8 @@ class Msg {
|
|||
gecos!: string;
|
||||
account!: boolean;
|
||||
|
||||
client_tags!: ClientTags;
|
||||
|
||||
// these are all just for error:
|
||||
error!: string;
|
||||
nick!: string;
|
||||
|
@ -94,6 +98,7 @@ class Msg {
|
|||
text: "",
|
||||
type: MessageType.MESSAGE,
|
||||
self: false,
|
||||
client_tags: {},
|
||||
});
|
||||
|
||||
if (this.time) {
|
||||
|
|
|
@ -1,20 +1,35 @@
|
|||
import _ from "lodash";
|
||||
import Prefix from "./prefix";
|
||||
import {TypingStatus} from "./client-tags";
|
||||
|
||||
class User {
|
||||
const ELAPSED_SINCE_LAST_ACTIVE_TYPING = 6 * 1000; // 6 seconds
|
||||
const ELAPSED_SINCE_LAST_PAUSED_TYPING = 30 * 1000; // 30 seconds
|
||||
|
||||
export class User {
|
||||
modes!: string[];
|
||||
// Users in the channel have only one mode assigned
|
||||
mode!: string;
|
||||
away!: string;
|
||||
nick!: string;
|
||||
lastMessage!: number;
|
||||
lastActiveTyping!: number;
|
||||
lastPausedTyping!: number;
|
||||
|
||||
constructor(attr: Partial<User>, prefix?: Prefix) {
|
||||
// Client-side
|
||||
isTyping!: boolean;
|
||||
|
||||
_waitForPausedNotificationHandle?: ReturnType<typeof setTimeout>;
|
||||
_waitForActiveNotificationHandle?: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(attr: Partial<User>) {
|
||||
_.defaults(this, attr, {
|
||||
modes: [],
|
||||
away: "",
|
||||
nick: "",
|
||||
lastMessage: 0,
|
||||
lastActiveTyping: 0,
|
||||
lastPausedTyping: 0,
|
||||
isTyping: false,
|
||||
});
|
||||
|
||||
Object.defineProperty(this, "mode", {
|
||||
|
@ -23,11 +38,55 @@ class User {
|
|||
return this.modes[0] || "";
|
||||
},
|
||||
});
|
||||
|
||||
this.setModes(this.modes, prefix || new Prefix([]));
|
||||
}
|
||||
|
||||
setModes(modes: string[], prefix: Prefix) {
|
||||
static withPrefixLookup(attr: Partial<User>, prefix: Prefix): User {
|
||||
const user = new User(attr);
|
||||
user.setModesForServer(attr.modes || [], prefix);
|
||||
return user;
|
||||
}
|
||||
|
||||
_clearTypingTimers() {
|
||||
if (this._waitForActiveNotificationHandle) {
|
||||
clearTimeout(this._waitForActiveNotificationHandle);
|
||||
this._waitForActiveNotificationHandle = undefined;
|
||||
}
|
||||
|
||||
if (this._waitForPausedNotificationHandle) {
|
||||
clearTimeout(this._waitForPausedNotificationHandle);
|
||||
this._waitForPausedNotificationHandle = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
stopTyping() {
|
||||
this.isTyping = false;
|
||||
|
||||
this._clearTypingTimers();
|
||||
}
|
||||
|
||||
startTyping(status: TypingStatus) {
|
||||
this.isTyping = true;
|
||||
|
||||
if (status === TypingStatus.ACTIVE) {
|
||||
this._clearTypingTimers();
|
||||
this._waitForActiveNotificationHandle = setTimeout(() => {
|
||||
if (Date.now() - this.lastActiveTyping > ELAPSED_SINCE_LAST_ACTIVE_TYPING) {
|
||||
this.stopTyping();
|
||||
}
|
||||
}, ELAPSED_SINCE_LAST_ACTIVE_TYPING);
|
||||
}
|
||||
|
||||
if (status === TypingStatus.PAUSED) {
|
||||
this._clearTypingTimers();
|
||||
this._waitForActiveNotificationHandle = setTimeout(() => {
|
||||
if (Date.now() - this.lastPausedTyping > ELAPSED_SINCE_LAST_PAUSED_TYPING) {
|
||||
this.stopTyping();
|
||||
}
|
||||
}, ELAPSED_SINCE_LAST_PAUSED_TYPING);
|
||||
}
|
||||
}
|
||||
|
||||
setModesForServer(modes: string[], prefix: Prefix) {
|
||||
// irc-framework sets character mode, but The Lounge works with symbols
|
||||
this.modes = modes.map((mode) => prefix.modeToSymbol[mode]);
|
||||
}
|
||||
|
@ -37,6 +96,8 @@ class User {
|
|||
nick: this.nick,
|
||||
modes: this.modes,
|
||||
lastMessage: this.lastMessage,
|
||||
lastActiveTyping: this.lastActiveTyping,
|
||||
lastPausedTyping: this.lastPausedTyping,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ function getTarget(cmd: string, args: string[], chan: Chan) {
|
|||
|
||||
const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||
let targetName = getTarget(cmd, args, chan);
|
||||
const tags = {};
|
||||
|
||||
if (cmd === "query") {
|
||||
if (!targetName) {
|
||||
|
@ -91,7 +92,7 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
return true;
|
||||
}
|
||||
|
||||
network.irc.say(targetName, msg);
|
||||
network.irc.say(targetName, msg, tags);
|
||||
|
||||
// If the IRCd does not support echo-message, simulate the message
|
||||
// being sent back to us.
|
||||
|
|
|
@ -5,6 +5,7 @@ import Helper from "../../helper";
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import Chan, {ChanType} from "../../models/chan";
|
||||
import User from "../../models/user";
|
||||
import {ClientTags, ClientTagKey, TypingStatus} from "../../models/client-tags";
|
||||
|
||||
const nickRegExp = /(?:\x03[0-9]{1,2}(?:,[0-9]{1,2})?)?([\w[\]\\`^{|}-]+)/g;
|
||||
|
||||
|
@ -26,6 +27,11 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
handleMessage(data);
|
||||
});
|
||||
|
||||
irc.on("tagmsg", function (data) {
|
||||
data.type = MessageType.TAGMSG;
|
||||
handleMessage(data);
|
||||
});
|
||||
|
||||
irc.on("privmsg", function (data) {
|
||||
data.type = MessageType.MESSAGE;
|
||||
handleMessage(data);
|
||||
|
@ -44,6 +50,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
target: string;
|
||||
type: MessageType;
|
||||
time: number;
|
||||
tags: Record<string, string>;
|
||||
text?: string;
|
||||
from_server?: boolean;
|
||||
message: string;
|
||||
|
@ -114,6 +121,33 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
|
||||
from = chan.getUser(data.nick);
|
||||
|
||||
if (data.type === MessageType.TAGMSG) {
|
||||
const typingTag = `+${ClientTagKey.TYPING}` as const;
|
||||
|
||||
if (Object.hasOwn(data.tags, typingTag)) {
|
||||
const status = data.tags[typingTag];
|
||||
|
||||
if (status === TypingStatus.ACTIVE) {
|
||||
from.lastActiveTyping = data.time || Date.now();
|
||||
} else if (status === TypingStatus.PAUSED) {
|
||||
from.lastPausedTyping = data.time || Date.now();
|
||||
}
|
||||
|
||||
client.emit("channel:isTyping", {
|
||||
network: network.uuid,
|
||||
chanId: chan.id,
|
||||
from: from.toJSON(),
|
||||
status,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Any other message should stop
|
||||
// the typing indicator.
|
||||
from.stopTyping();
|
||||
|
||||
// Query messages (unless self or muted) always highlight
|
||||
if (chan.type === ChanType.QUERY) {
|
||||
highlight = !self;
|
||||
|
@ -131,14 +165,20 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
from: from,
|
||||
highlight: highlight,
|
||||
users: [],
|
||||
client_tags: new ClientTags(data.tags),
|
||||
});
|
||||
|
||||
if (showInActive) {
|
||||
msg.showInActive = true;
|
||||
}
|
||||
|
||||
// remove IRC formatting for custom highlight testing
|
||||
const cleanMessage = cleanIrcMessage(data.message);
|
||||
// Not all messages have bodies.
|
||||
let cleanMessage = data.message;
|
||||
|
||||
if (data.message) {
|
||||
// remove IRC formatting for custom highlight testing
|
||||
cleanMessage = cleanIrcMessage(data.message);
|
||||
}
|
||||
|
||||
// Self messages in channels are never highlighted
|
||||
// Non-self messages are highlighted as soon as the nick is detected
|
||||
|
@ -174,10 +214,19 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
LinkPrefetch(client, chan, msg, cleanMessage);
|
||||
}
|
||||
|
||||
if (!data.message) {
|
||||
return;
|
||||
}
|
||||
|
||||
chan.pushMessage(client, msg, !msg.self);
|
||||
|
||||
// Do not send notifications if the channel is muted or for messages older than 15 minutes (znc buffer for example)
|
||||
if (!chan.muted && msg.highlight && (!data.time || data.time > Date.now() - 900000)) {
|
||||
if (
|
||||
!chan.muted &&
|
||||
msg.highlight &&
|
||||
(!data.time || data.time > Date.now() - 900000) &&
|
||||
msg.type !== MessageType.TAGMSG
|
||||
) {
|
||||
let title = chan.name;
|
||||
let body = cleanMessage;
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
|
||||
data.users.forEach((user) => {
|
||||
const newUser = chan.getUser(user.nick);
|
||||
newUser.setModes(user.modes, network.serverOptions.PREFIX);
|
||||
newUser.setModesForServer(user.modes, network.serverOptions.PREFIX);
|
||||
|
||||
newUsers.set(user.nick.toLowerCase(), newUser);
|
||||
});
|
||||
|
|
|
@ -468,6 +468,12 @@ function initializeClient(
|
|||
}
|
||||
});
|
||||
|
||||
socket.on("input:typing", (data) => {
|
||||
if (_.isPlainObject(data)) {
|
||||
client.setTyping(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("more", (data) => {
|
||||
if (_.isPlainObject(data)) {
|
||||
const history = client.more(data);
|
||||
|
|
13
server/types/modules/irc-framework.d.ts
vendored
13
server/types/modules/irc-framework.d.ts
vendored
|
@ -174,11 +174,18 @@ declare module "irc-framework" {
|
|||
|
||||
changeNick(nick: string): void;
|
||||
|
||||
sendMessage(commandName: string, target: string, message: string): string[];
|
||||
sendMessage(
|
||||
commandName: string,
|
||||
target: string,
|
||||
message: string,
|
||||
tags: Record<string, string> = {}
|
||||
): string[];
|
||||
|
||||
say(target: string, message: string): string[];
|
||||
say(target: string, message: string, tags: Record<string, string> = {}): string[];
|
||||
|
||||
notice(target: string, message: string): string[];
|
||||
notice(target: string, message: string, tags: Record<string, string> = {}): string[];
|
||||
|
||||
tagmsg(target: string, tags: Record<string, string> = {}): string[];
|
||||
|
||||
join(channel: string, key?: string): void;
|
||||
|
||||
|
|
2
server/types/socket-events.d.ts
vendored
2
server/types/socket-events.d.ts
vendored
|
@ -1,5 +1,6 @@
|
|||
import {ClientMessage, ClientNetwork, InitClientChan} from "../../client/js/types";
|
||||
import {Mention} from "../client";
|
||||
import {ServerTypingNotification} from "../client/js/socket-events/typing";
|
||||
import {ChanState} from "../models/chan";
|
||||
import Msg from "../models/msg";
|
||||
import Network from "../models/network";
|
||||
|
@ -28,6 +29,7 @@ interface ServerToClientEvents {
|
|||
"changelog:newversion": () => void;
|
||||
|
||||
"channel:state": (data: {chan: number; state: ChanState}) => void;
|
||||
"channel:isTyping": (data: ServerTypingNotification) => void;
|
||||
|
||||
"change-password": ({success, error}: {success: boolean; error?: any}) => void;
|
||||
|
||||
|
|
|
@ -57,8 +57,8 @@ describe("Chan", function () {
|
|||
|
||||
it("should update user object", function () {
|
||||
const chan = new Chan();
|
||||
chan.setUser(new User({nick: "TestUser"}, prefixLookup));
|
||||
chan.setUser(new User({nick: "TestUseR", modes: ["o"]}, prefixLookup));
|
||||
chan.setUser(User.withPrefixLookup({nick: "TestUser"}, prefixLookup));
|
||||
chan.setUser(User.withPrefixLookup({nick: "TestUseR", modes: ["o"]}, prefixLookup));
|
||||
const user = chan.getUser("TestUSER");
|
||||
|
||||
expect(user.mode).to.equal("@");
|
||||
|
@ -68,13 +68,13 @@ describe("Chan", function () {
|
|||
describe("#getUser(nick)", function () {
|
||||
it("should returning existing object", function () {
|
||||
const chan = new Chan();
|
||||
chan.setUser(new User({nick: "TestUseR", modes: ["o"]}, prefixLookup));
|
||||
chan.setUser(User.withPrefixLookup({nick: "TestUseR", modes: ["o"]}, prefixLookup));
|
||||
const user = chan.getUser("TestUSER");
|
||||
|
||||
expect(user.mode).to.equal("@");
|
||||
});
|
||||
|
||||
it("should make new User object if not found", function () {
|
||||
it("should make User.withPrefixLookup object if not found", function () {
|
||||
const chan = new Chan();
|
||||
const user = chan.getUser("very-testy-user");
|
||||
|
||||
|
@ -105,7 +105,7 @@ describe("Chan", function () {
|
|||
it("should sort a simple user list", function () {
|
||||
const chan = new Chan();
|
||||
["JocelynD", "YaManicKill", "astorije", "xPaw", "Max-P"].forEach((nick) =>
|
||||
chan.setUser(new User({nick}, prefixLookup))
|
||||
chan.setUser(User.withPrefixLookup({nick}, prefixLookup))
|
||||
);
|
||||
|
||||
expect(getUserNames(chan)).to.deep.equal([
|
||||
|
@ -119,11 +119,13 @@ describe("Chan", function () {
|
|||
|
||||
it("should group users by modes", function () {
|
||||
const chan = new Chan();
|
||||
chan.setUser(new User({nick: "JocelynD", modes: ["a", "o"]}, prefixLookup));
|
||||
chan.setUser(new User({nick: "YaManicKill", modes: ["v"]}, prefixLookup));
|
||||
chan.setUser(new User({nick: "astorije", modes: ["h"]}, prefixLookup));
|
||||
chan.setUser(new User({nick: "xPaw", modes: ["q"]}, prefixLookup));
|
||||
chan.setUser(new User({nick: "Max-P", modes: ["o"]}, prefixLookup));
|
||||
chan.setUser(
|
||||
User.withPrefixLookup({nick: "JocelynD", modes: ["a", "o"]}, prefixLookup)
|
||||
);
|
||||
chan.setUser(User.withPrefixLookup({nick: "YaManicKill", modes: ["v"]}, prefixLookup));
|
||||
chan.setUser(User.withPrefixLookup({nick: "astorije", modes: ["h"]}, prefixLookup));
|
||||
chan.setUser(User.withPrefixLookup({nick: "xPaw", modes: ["q"]}, prefixLookup));
|
||||
chan.setUser(User.withPrefixLookup({nick: "Max-P", modes: ["o"]}, prefixLookup));
|
||||
|
||||
expect(getUserNames(chan)).to.deep.equal([
|
||||
"xPaw",
|
||||
|
@ -136,11 +138,11 @@ describe("Chan", function () {
|
|||
|
||||
it("should sort a mix of users and modes", function () {
|
||||
const chan = new Chan();
|
||||
chan.setUser(new User({nick: "JocelynD"}, prefixLookup));
|
||||
chan.setUser(new User({nick: "YaManicKill", modes: ["o"]}, prefixLookup));
|
||||
chan.setUser(new User({nick: "astorije"}, prefixLookup));
|
||||
chan.setUser(new User({nick: "xPaw"}, prefixLookup));
|
||||
chan.setUser(new User({nick: "Max-P", modes: ["o"]}, prefixLookup));
|
||||
chan.setUser(User.withPrefixLookup({nick: "JocelynD"}, prefixLookup));
|
||||
chan.setUser(User.withPrefixLookup({nick: "YaManicKill", modes: ["o"]}, prefixLookup));
|
||||
chan.setUser(User.withPrefixLookup({nick: "astorije"}, prefixLookup));
|
||||
chan.setUser(User.withPrefixLookup({nick: "xPaw"}, prefixLookup));
|
||||
chan.setUser(User.withPrefixLookup({nick: "Max-P", modes: ["o"]}, prefixLookup));
|
||||
|
||||
expect(getUserNames(chan)).to.deep.equal([
|
||||
"Max-P",
|
||||
|
@ -154,7 +156,7 @@ describe("Chan", function () {
|
|||
it("should be case-insensitive", function () {
|
||||
const chan = new Chan();
|
||||
["aB", "Ad", "AA", "ac"].forEach((nick) =>
|
||||
chan.setUser(new User({nick}, prefixLookup))
|
||||
chan.setUser(User.withPrefixLookup({nick}, prefixLookup))
|
||||
);
|
||||
|
||||
expect(getUserNames(chan)).to.deep.equal(["AA", "aB", "ac", "Ad"]);
|
||||
|
@ -175,7 +177,7 @@ describe("Chan", function () {
|
|||
"!foo",
|
||||
"+foo",
|
||||
"Foo",
|
||||
].forEach((nick) => chan.setUser(new User({nick}, prefixLookup)));
|
||||
].forEach((nick) => chan.setUser(User.withPrefixLookup({nick}, prefixLookup)));
|
||||
|
||||
expect(getUserNames(chan)).to.deep.equal([
|
||||
"!foo",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {expect} from "chai";
|
||||
|
||||
import Prefix from "../../server/models/prefix";
|
||||
import Msg from "../../server/models/msg";
|
||||
import User from "../../server/models/user";
|
||||
import {LinkPreview} from "../../server/plugins/irc-events/link";
|
||||
|
@ -8,17 +9,17 @@ describe("Msg", function () {
|
|||
["from", "target"].forEach((prop) => {
|
||||
it(`should keep a copy of the original user in the \`${prop}\` property`, function () {
|
||||
const prefixLookup = {modeToSymbol: {a: "&", o: "@"}};
|
||||
const user = new User(
|
||||
const user = User.withPrefixLookup(
|
||||
{
|
||||
modes: ["o"],
|
||||
nick: "foo",
|
||||
},
|
||||
prefixLookup as any
|
||||
prefixLookup as unknown as Prefix
|
||||
);
|
||||
const msg = new Msg({[prop]: user});
|
||||
|
||||
// Mutating the user
|
||||
user.setModes(["a"], prefixLookup as any);
|
||||
user.setModesForServer(["a"], prefixLookup as any);
|
||||
user.nick = "bar";
|
||||
|
||||
// Message's `.from`/etc. should still refer to the original user
|
||||
|
|
|
@ -58,7 +58,11 @@ const config: webpack.Configuration = {
|
|||
},
|
||||
{
|
||||
test: /\.ts$/i,
|
||||
include: [path.resolve(__dirname, "client"), path.resolve(__dirname, "shared")],
|
||||
include: [
|
||||
path.resolve(__dirname, "client"),
|
||||
path.resolve(__dirname, "server"),
|
||||
path.resolve(__dirname, "shared"),
|
||||
],
|
||||
exclude: path.resolve(__dirname, "node_modules"),
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
|
|
Loading…
Reference in a new issue