mirror of
https://github.com/thelounge/thelounge.git
synced 2026-04-24 07:51:08 -04:00
Merge branch 'master' into max/04-11-channel-context
This commit is contained in:
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
<span
|
||||
v-else
|
||||
:title="channel.topic"
|
||||
:title="plainTopic"
|
||||
:class="{topic: true, empty: !channel.topic}"
|
||||
@dblclick="editTopic"
|
||||
><ParsedMessage
|
||||
@@ -137,6 +137,7 @@ import {defineComponent, PropType, ref, computed, watch, nextTick, onMounted, Co
|
||||
import type {ClientNetwork, ClientChan} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
import {SpecialChanType, ChanType} from "../../shared/types/chan";
|
||||
import parseStyle from "../js/helpers/ircmessageparser/parseStyle";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Chat",
|
||||
@@ -160,6 +161,18 @@ export default defineComponent({
|
||||
const messageList = ref<typeof MessageList>();
|
||||
const topicInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const plainTopic = computed(() => {
|
||||
const topic = props.channel.topic;
|
||||
|
||||
if (!topic) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return parseStyle(topic)
|
||||
.map((fragment) => fragment.text)
|
||||
.join("");
|
||||
});
|
||||
|
||||
const specialComponent = computed(() => {
|
||||
switch (props.channel.special) {
|
||||
case SpecialChanType.BANLIST:
|
||||
@@ -262,6 +275,7 @@ export default defineComponent({
|
||||
store,
|
||||
messageList,
|
||||
topicInput,
|
||||
plainTopic,
|
||||
specialComponent,
|
||||
hideUserVisibleError,
|
||||
editTopic,
|
||||
|
||||
@@ -154,7 +154,13 @@ import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import {computed, watch, defineComponent, ref, onMounted, onUnmounted} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
import {ClientMention} from "../js/types";
|
||||
import type {SharedMention} from "../../shared/types/mention";
|
||||
import type {NetChan} from "../js/types";
|
||||
|
||||
type MentionWithContext = SharedMention & {
|
||||
localetime: string;
|
||||
channel: NetChan | null;
|
||||
};
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
@@ -169,14 +175,15 @@ export default defineComponent({
|
||||
const isOpen = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const resolvedMessages = computed(() => {
|
||||
const messages = store.state.mentions.slice().reverse();
|
||||
|
||||
for (const message of messages) {
|
||||
message.localetime = localetime(message.time);
|
||||
message.channel = store.getters.findChannel(message.chanId);
|
||||
}
|
||||
|
||||
return messages.filter((message) => !message.channel?.channel.muted);
|
||||
return store.state.mentions
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((message) => ({
|
||||
...message,
|
||||
localetime: localetime(message.time),
|
||||
channel: store.getters.findChannel(message.chanId),
|
||||
}))
|
||||
.filter((message) => !message.channel?.channel.muted);
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -190,7 +197,7 @@ export default defineComponent({
|
||||
return dayjs(time).fromNow();
|
||||
};
|
||||
|
||||
const dismissMention = (message: ClientMention) => {
|
||||
const dismissMention = (message: MentionWithContext) => {
|
||||
store.state.mentions.splice(
|
||||
store.state.mentions.findIndex((m) => m.msgId === message.msgId),
|
||||
1
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import {ClientChan, ClientMessage} from "./types";
|
||||
import {SharedNetworkChan} from "../../shared/types/network";
|
||||
import {SharedMsg, MessageType} from "../../shared/types/msg";
|
||||
import {SharedMsg} from "../../shared/types/msg";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
import {extractInputHistory} from "./helpers/inputHistory";
|
||||
|
||||
export function toClientChan(shared: SharedNetworkChan): ClientChan {
|
||||
const history: string[] = [""].concat(
|
||||
shared.messages
|
||||
.filter((m) => m.self && m.text && m.type === MessageType.MESSAGE)
|
||||
// TS is too stupid to see the nil guard on filter... so we monkey patch it
|
||||
.map((m): string => (m.text ? m.text : ""))
|
||||
.reverse()
|
||||
.slice(0, 99)
|
||||
);
|
||||
const history: string[] = [""].concat(extractInputHistory(shared.messages, 99));
|
||||
// filter the unused vars
|
||||
const {messages, totalMessages: _, ...props} = shared;
|
||||
const channel: ClientChan = {
|
||||
|
||||
13
client/js/helpers/inputHistory.ts
Normal file
13
client/js/helpers/inputHistory.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {MessageType, SharedMsg} from "../../../shared/types/msg";
|
||||
|
||||
export function extractInputHistory(messages: SharedMsg[], limit: number): string[] {
|
||||
return (
|
||||
messages
|
||||
.filter((m) => m.self && m.text && m.type === MessageType.MESSAGE)
|
||||
// TS is too stupid to see the guard in .filter(), so we monkey patch it
|
||||
// to please the compiler
|
||||
.map((m) => m.text!)
|
||||
.reverse()
|
||||
.slice(0, limit)
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,6 @@
|
||||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {ClientMention} from "../types";
|
||||
import {SharedMention} from "../../../shared/types/mention";
|
||||
|
||||
socket.on("mentions:list", function (data) {
|
||||
store.commit("mentions", data.map(sharedToClientMention));
|
||||
store.commit("mentions", data);
|
||||
});
|
||||
|
||||
function sharedToClientMention(shared: SharedMention): ClientMention {
|
||||
const mention: ClientMention = {
|
||||
...shared,
|
||||
localetime: "", // TODO: can't be right
|
||||
channel: null,
|
||||
};
|
||||
return mention;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {nextTick} from "vue";
|
||||
|
||||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {extractInputHistory} from "../helpers/inputHistory";
|
||||
|
||||
socket.on("more", async (data) => {
|
||||
const channel = store.getters.findChannel(data.chan)?.channel;
|
||||
@@ -12,13 +12,7 @@ socket.on("more", async (data) => {
|
||||
}
|
||||
|
||||
channel.inputHistory = channel.inputHistory.concat(
|
||||
data.messages
|
||||
.filter((m) => m.self && m.text && m.type === MessageType.MESSAGE)
|
||||
// TS is too stupid to see the guard in .filter(), so we monkey patch it
|
||||
// to please the compiler
|
||||
.map((m) => (m.text ? m.text : ""))
|
||||
.reverse()
|
||||
.slice(0, 100 - channel.inputHistory.length)
|
||||
extractInputHistory(data.messages, 100 - channel.inputHistory.length)
|
||||
);
|
||||
channel.moreHistoryAvailable =
|
||||
data.totalMessages > channel.messages.length + data.messages.length;
|
||||
|
||||
5
client/js/types.d.ts
vendored
5
client/js/types.d.ts
vendored
@@ -50,10 +50,7 @@ type NetChan = {
|
||||
network: ClientNetwork;
|
||||
};
|
||||
|
||||
type ClientMention = SharedMention & {
|
||||
localetime: string; // TODO: this needs to go the way of the dodo, nothing but a single component uses it
|
||||
channel: NetChan | null;
|
||||
};
|
||||
type ClientMention = SharedMention;
|
||||
|
||||
type ClientLinkPreview = LinkPreview & {
|
||||
sourceLoaded?: boolean;
|
||||
|
||||
@@ -478,6 +478,10 @@ class Network {
|
||||
this.channels.forEach((channel) => channel.destroy());
|
||||
}
|
||||
|
||||
isIgnoredUser(data: Hostmask) {
|
||||
return this.ignoreList.some((entry) => Helper.compareHostmask(entry, data));
|
||||
}
|
||||
|
||||
setNick(this: Network, nick: string) {
|
||||
this.nick = nick;
|
||||
this.highlightRegex = new RegExp(
|
||||
|
||||
@@ -37,11 +37,7 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
network.ignoreList.some(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask);
|
||||
})
|
||||
) {
|
||||
if (network.isIgnoredUser(hostmask)) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import _ from "lodash";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
import Helper from "../../helper";
|
||||
import Msg from "../../models/msg";
|
||||
import User from "../../models/user";
|
||||
import pkg from "../../../package.json";
|
||||
@@ -21,9 +20,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||
const lobby = network.getLobby();
|
||||
|
||||
irc.on("ctcp response", function (data) {
|
||||
const shouldIgnore = network.ignoreList.some(function (entry) {
|
||||
return Helper.compareHostmask(entry, data);
|
||||
});
|
||||
const shouldIgnore = network.isIgnoredUser(data);
|
||||
|
||||
if (shouldIgnore) {
|
||||
return;
|
||||
@@ -59,9 +56,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldIgnore = network.ignoreList.some(function (entry) {
|
||||
return Helper.compareHostmask(entry, data);
|
||||
});
|
||||
const shouldIgnore = network.isIgnoredUser(data);
|
||||
|
||||
if (shouldIgnore) {
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Msg from "../../models/msg";
|
||||
import LinkPrefetch from "./link";
|
||||
import {cleanIrcMessage} from "../../../shared/irc";
|
||||
import Helper from "../../helper";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
import Chan from "../../models/chan";
|
||||
import User from "../../models/user";
|
||||
@@ -70,11 +69,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||
}
|
||||
|
||||
// Check if the sender is in our ignore list
|
||||
const shouldIgnore =
|
||||
!self &&
|
||||
network.ignoreList.some(function (entry) {
|
||||
return Helper.compareHostmask(entry, data);
|
||||
});
|
||||
const shouldIgnore = !self && network.isIgnoredUser(data);
|
||||
|
||||
// Server messages that aren't targeted at a channel go to the server window
|
||||
if (
|
||||
|
||||
@@ -25,8 +25,9 @@ class Storage {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Use `fs.rmdirSync(dir, {recursive: true});` when it's stable (node 13+)
|
||||
items.forEach((item) => deleteFolder(path.join(dir, item)));
|
||||
items.forEach((item) => {
|
||||
fs.rmSync(path.join(dir, item), {recursive: true, force: true});
|
||||
});
|
||||
}
|
||||
|
||||
dereference(url) {
|
||||
@@ -88,17 +89,3 @@ class Storage {
|
||||
}
|
||||
|
||||
export default new Storage();
|
||||
|
||||
function deleteFolder(dir: string) {
|
||||
fs.readdirSync(dir).forEach((item) => {
|
||||
item = path.join(dir, item);
|
||||
|
||||
if (fs.lstatSync(item).isDirectory()) {
|
||||
deleteFolder(item);
|
||||
} else {
|
||||
fs.unlinkSync(item);
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmdirSync(dir);
|
||||
}
|
||||
|
||||
68
test/models/networkIgnoreTest.ts
Normal file
68
test/models/networkIgnoreTest.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {expect} from "chai";
|
||||
import Network from "../../server/models/network";
|
||||
import Helper from "../../server/helper";
|
||||
|
||||
describe("Network#isIgnoredUser", function () {
|
||||
it("should return false when ignore list is empty", function () {
|
||||
const network = new Network();
|
||||
const result = network.isIgnoredUser({nick: "foo", ident: "bar", hostname: "baz"});
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("should match an exact hostmask", function () {
|
||||
const network = new Network();
|
||||
network.ignoreList.push({
|
||||
...Helper.parseHostmask("nick!user@host"),
|
||||
when: Date.now(),
|
||||
});
|
||||
|
||||
expect(network.isIgnoredUser({nick: "nick", ident: "user", hostname: "host"})).to.be.true;
|
||||
});
|
||||
|
||||
it("should match case-insensitively", function () {
|
||||
const network = new Network();
|
||||
network.ignoreList.push({
|
||||
...Helper.parseHostmask("NICK!USER@HOST"),
|
||||
when: Date.now(),
|
||||
});
|
||||
|
||||
expect(network.isIgnoredUser({nick: "nick", ident: "user", hostname: "host"})).to.be.true;
|
||||
});
|
||||
|
||||
it("should match wildcard patterns", function () {
|
||||
const network = new Network();
|
||||
network.ignoreList.push({
|
||||
...Helper.parseHostmask("*!*@spammer.example.com"),
|
||||
when: Date.now(),
|
||||
});
|
||||
|
||||
expect(
|
||||
network.isIgnoredUser({nick: "anyone", ident: "any", hostname: "spammer.example.com"})
|
||||
).to.be.true;
|
||||
expect(network.isIgnoredUser({nick: "anyone", ident: "any", hostname: "good.example.com"}))
|
||||
.to.be.false;
|
||||
});
|
||||
|
||||
it("should return false when no entry matches", function () {
|
||||
const network = new Network();
|
||||
network.ignoreList.push({
|
||||
...Helper.parseHostmask("badnick!*@*"),
|
||||
when: Date.now(),
|
||||
});
|
||||
|
||||
expect(network.isIgnoredUser({nick: "goodnick", ident: "user", hostname: "host"})).to.be
|
||||
.false;
|
||||
});
|
||||
|
||||
it("should match against multiple ignore entries", function () {
|
||||
const network = new Network();
|
||||
network.ignoreList.push(
|
||||
{...Helper.parseHostmask("nick1!*@*"), when: Date.now()},
|
||||
{...Helper.parseHostmask("nick2!*@*"), when: Date.now()}
|
||||
);
|
||||
|
||||
expect(network.isIgnoredUser({nick: "nick1", ident: "a", hostname: "b"})).to.be.true;
|
||||
expect(network.isIgnoredUser({nick: "nick2", ident: "a", hostname: "b"})).to.be.true;
|
||||
expect(network.isIgnoredUser({nick: "nick3", ident: "a", hostname: "b"})).to.be.false;
|
||||
});
|
||||
});
|
||||
68
test/shared/inputHistoryTest.ts
Normal file
68
test/shared/inputHistoryTest.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {expect} from "chai";
|
||||
import {extractInputHistory} from "../../client/js/helpers/inputHistory";
|
||||
import {MessageType} from "../../shared/types/msg";
|
||||
|
||||
describe("extractInputHistory helper", function () {
|
||||
it("should extract self-authored messages", function () {
|
||||
const messages = [
|
||||
{self: true, text: "hello", type: MessageType.MESSAGE},
|
||||
{self: false, text: "world", type: MessageType.MESSAGE},
|
||||
{self: true, text: "foo", type: MessageType.MESSAGE},
|
||||
];
|
||||
|
||||
const result = extractInputHistory(messages as any, 100);
|
||||
expect(result).to.deep.equal(["foo", "hello"]);
|
||||
});
|
||||
|
||||
it("should only include MESSAGE type", function () {
|
||||
const messages = [
|
||||
{self: true, text: "msg", type: MessageType.MESSAGE},
|
||||
{self: true, text: "action", type: MessageType.ACTION},
|
||||
{self: true, text: "notice", type: MessageType.NOTICE},
|
||||
];
|
||||
|
||||
const result = extractInputHistory(messages as any, 100);
|
||||
expect(result).to.deep.equal(["msg"]);
|
||||
});
|
||||
|
||||
it("should skip messages with empty text", function () {
|
||||
const messages = [
|
||||
{self: true, text: "", type: MessageType.MESSAGE},
|
||||
{self: true, text: undefined, type: MessageType.MESSAGE},
|
||||
{self: true, text: "valid", type: MessageType.MESSAGE},
|
||||
];
|
||||
|
||||
const result = extractInputHistory(messages as any, 100);
|
||||
expect(result).to.deep.equal(["valid"]);
|
||||
});
|
||||
|
||||
it("should return most recent first", function () {
|
||||
const messages = [
|
||||
{self: true, text: "first", type: MessageType.MESSAGE},
|
||||
{self: true, text: "second", type: MessageType.MESSAGE},
|
||||
{self: true, text: "third", type: MessageType.MESSAGE},
|
||||
];
|
||||
|
||||
const result = extractInputHistory(messages as any, 100);
|
||||
expect(result[0]).to.equal("third");
|
||||
expect(result[2]).to.equal("first");
|
||||
});
|
||||
|
||||
it("should respect the limit parameter", function () {
|
||||
const messages = [
|
||||
{self: true, text: "a", type: MessageType.MESSAGE},
|
||||
{self: true, text: "b", type: MessageType.MESSAGE},
|
||||
{self: true, text: "c", type: MessageType.MESSAGE},
|
||||
];
|
||||
|
||||
const result = extractInputHistory(messages as any, 2);
|
||||
expect(result).to.have.lengthOf(2);
|
||||
});
|
||||
|
||||
it("should return empty array for no matching messages", function () {
|
||||
const messages = [{self: false, text: "nope", type: MessageType.MESSAGE}];
|
||||
|
||||
const result = extractInputHistory(messages as any, 100);
|
||||
expect(result).to.be.empty;
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user