some test fixes

This commit is contained in:
Max Leiter 2022-05-14 15:18:06 -07:00
parent b798cfdc64
commit 4c98b81e35
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
44 changed files with 215 additions and 90 deletions

View file

@ -1,3 +1,4 @@
public/ public/
coverage/ coverage/
src/dist/ src/dist/
dist/

1
.gitignore vendored
View file

@ -8,3 +8,4 @@ coverage/
public/ public/
client/dist client/dist
src/dist src/dist
dist/

View file

@ -4,6 +4,7 @@ test/fixtures/.thelounge/logs/
test/fixtures/.thelounge/certificates/ test/fixtures/.thelounge/certificates/
test/fixtures/.thelounge/storage/ test/fixtures/.thelounge/storage/
src/dist/ src/dist/
dist/
*.log *.log
*.png *.png
*.svg *.svg

View file

@ -195,7 +195,7 @@
} }
</style> </style>
<script lang="ts"> <script>
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import Draggable from "vuedraggable"; import Draggable from "vuedraggable";
import {filter as fuzzyFilter} from "fuzzy"; import {filter as fuzzyFilter} from "fuzzy";
@ -209,10 +209,7 @@ import isIgnoredKeybind from "../js/helpers/isIgnoredKeybind";
import distance from "../js/helpers/distance"; import distance from "../js/helpers/distance";
import eventbus from "../js/eventbus"; import eventbus from "../js/eventbus";
import NetworkModel from "../../src/models/network"; export default Vue.extend({
import ChannelMode from "../../src/models/chan";
export default {
name: "NetworkList", name: "NetworkList",
components: { components: {
JoinChannel, JoinChannel,
@ -484,5 +481,5 @@ export default {
}); });
}, },
}, },
}; });
</script> </script>

View file

@ -274,10 +274,6 @@ function fuzzyGrep<T>(term: string, array: Array<T>) {
} }
function rawNicks() { function rawNicks() {
if (!store.state.activeChannel) {
return [];
}
if (store.state.activeChannel.channel.users.length > 0) { if (store.state.activeChannel.channel.users.length > 0) {
const users = store.state.activeChannel.channel.users.slice(); const users = store.state.activeChannel.channel.users.slice();

View file

@ -4,7 +4,7 @@ import store from "../store";
function input() { function input() {
const messageIds = []; const messageIds = [];
for (const message of store.state.activeChannel.channel.messages) { for (const message of store.state.activeChannel?.channel.messages) {
let toggled = false; let toggled = false;
for (const preview of message.previews) { for (const preview of message.previews) {
@ -22,7 +22,7 @@ function input() {
// Tell the server we're toggling so it remembers at page reload // Tell the server we're toggling so it remembers at page reload
if (!document.body.classList.contains("public") && messageIds.length > 0) { if (!document.body.classList.contains("public") && messageIds.length > 0) {
socket.emit("msg:preview:toggle", { socket.emit("msg:preview:toggle", {
target: store.state.activeChannel.channel.id, target: store.state.activeChannel?.channel.id,
messageIds: messageIds, messageIds: messageIds,
shown: false, shown: false,
}); });

View file

@ -22,7 +22,7 @@ function input() {
// Tell the server we're toggling so it remembers at page reload // Tell the server we're toggling so it remembers at page reload
if (!document.body.classList.contains("public") && messageIds.length > 0) { if (!document.body.classList.contains("public") && messageIds.length > 0) {
socket.emit("msg:preview:toggle", { socket.emit("msg:preview:toggle", {
target: store.state.activeChannel.channel.id, target: store.state.activeChannel?.channel.id,
messageIds: messageIds, messageIds: messageIds,
shown: true, shown: true,
}); });

View file

@ -4,9 +4,9 @@
// directory, so we iterate over its content, which is a map statically built by // directory, so we iterate over its content, which is a map statically built by
// Webpack. // Webpack.
// Second argument says it's recursive, third makes sure we only load javascript. // Second argument says it's recursive, third makes sure we only load javascript.
const commands = require.context("./", true, /\.js$/); const commands = require.context("./", true, /\.ts$/);
export default commands.keys().reduce((acc, path) => { export default commands.keys().reduce<Record<string, unknown>>((acc, path) => {
const command = path.substring(2, path.length - 3); const command = path.substring(2, path.length - 3);
if (command === "index") { if (command === "index") {

View file

@ -1,6 +1,7 @@
import socket from "../socket"; import socket from "../socket";
import eventbus from "../eventbus"; import eventbus from "../eventbus";
import type {ClientChan, ClientNetwork} from "../types";
import type {Methods} from "../vue";
type ContextMenuItem = type ContextMenuItem =
| ({ | ({
label: string; label: string;
@ -18,7 +19,11 @@ type ContextMenuItem =
type: "divider"; type: "divider";
}; };
export function generateChannelContextMenu($root, channel, network) { export function generateChannelContextMenu(
$root: Methods,
channel: ClientChan,
network: ClientNetwork
) {
const typeMap = { const typeMap = {
lobby: "network", lobby: "network",
channel: "chan", channel: "chan",

View file

@ -1,8 +1,9 @@
import {ParsedStyle} from "./parseStyle";
// Return true if any section of "a" or "b" parts (defined by their start/end // Return true if any section of "a" or "b" parts (defined by their start/end
import {Part} from "./merge";
// markers) intersect each other, false otherwise. // markers) intersect each other, false otherwise.
function anyIntersection(a: ParsedStyle, b: ParsedStyle) { function anyIntersection(a: Part, b: Part) {
return ( return (
(a.start <= b.start && b.start < a.end) || (a.start <= b.start && b.start < a.end) ||
(a.start < b.end && b.end <= a.end) || (a.start < b.end && b.end <= a.end) ||

View file

@ -1,4 +1,5 @@
const matchFormatting = const matchFormatting =
// eslint-disable-next-line no-control-regex
/\x02|\x1D|\x1F|\x16|\x0F|\x11|\x1E|\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?|\x04(?:[0-9a-f]{6}(?:,[0-9a-f]{6})?)?/gi; /\x02|\x1D|\x1F|\x16|\x0F|\x11|\x1E|\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?|\x04(?:[0-9a-f]{6}(?:,[0-9a-f]{6})?)?/gi;
export default (message: string) => message.replace(matchFormatting, "").trim(); export default (message: string) => message.replace(matchFormatting, "").trim();

View file

@ -1,15 +1,16 @@
import {ParsedStyle} from "./parseStyle";
// Create plain text entries corresponding to areas of the text that match no // Create plain text entries corresponding to areas of the text that match no
// existing entries. Returns an empty array if all parts of the text have been // existing entries. Returns an empty array if all parts of the text have been
import {Part} from "./merge";
// parsed into recognizable entries already. // parsed into recognizable entries already.
function fill(existingEntries: ParsedStyle[], text: string) { function fill(existingEntries: Part[], text: string) {
let position = 0; let position = 0;
// Fill inner parts of the text. For example, if text is `foobarbaz` and both // Fill inner parts of the text. For example, if text is `foobarbaz` and both
// `foo` and `baz` have matched into an entry, this will return a dummy entry // `foo` and `baz` have matched into an entry, this will return a dummy entry
// corresponding to `bar`. // corresponding to `bar`.
const result = existingEntries.reduce((acc: Omit<ParsedStyle, "text">[], textSegment) => { const result = existingEntries.reduce<Part[]>((acc, textSegment) => {
if (textSegment.start > position) { if (textSegment.start > position) {
acc.push({ acc.push({
start: position, start: position,

View file

@ -2,13 +2,14 @@
// ")", "[", "]", "{", "}", and "|" in string. // ")", "[", "]", "{", "}", and "|" in string.
// See https://lodash.com/docs/#escapeRegExp // See https://lodash.com/docs/#escapeRegExp
import escapeRegExp from "lodash/escapeRegExp"; import escapeRegExp from "lodash/escapeRegExp";
import {Part} from "./merge";
// Given an array of channel prefixes (such as "#" and "&") and an array of user // Given an array of channel prefixes (such as "#" and "&") and an array of user
// modes (such as "@" and "+"), this function extracts channels and nicks from a // modes (such as "@" and "+"), this function extracts channels and nicks from a
// text. // text.
// It returns an array of objects for each channel found with their start index, // It returns an array of objects for each channel found with their start index,
// end index and channel name. // end index and channel name.
function findChannels(text, channelPrefixes, userModes) { function findChannels(text: string, channelPrefixes: string[], userModes: string[]) {
// `userModePattern` is necessary to ignore user modes in /whois responses. // `userModePattern` is necessary to ignore user modes in /whois responses.
// For example, a voiced user in #thelounge will have a /whois response of: // For example, a voiced user in #thelounge will have a /whois response of:
// > foo is on the following channels: +#thelounge // > foo is on the following channels: +#thelounge
@ -18,7 +19,7 @@ function findChannels(text, channelPrefixes, userModes) {
const channelPattern = `(?:^|\\s)[${userModePattern}]*([${channelPrefixPattern}][^ \u0007]+)`; const channelPattern = `(?:^|\\s)[${userModePattern}]*([${channelPrefixPattern}][^ \u0007]+)`;
const channelRegExp = new RegExp(channelPattern, "g"); const channelRegExp = new RegExp(channelPattern, "g");
const result = []; const result: ChannelPart[] = [];
let match; let match;
do { do {
@ -38,4 +39,8 @@ function findChannels(text, channelPrefixes, userModes) {
return result; return result;
} }
export type ChannelPart = Part & {
channel: string;
};
export default findChannels; export default findChannels;

View file

@ -1,10 +1,13 @@
const emojiRegExp = require("emoji-regex")(); import emojiRegExp from "emoji-regex";
import {Part} from "./merge";
function findEmoji(text) { const regExp = emojiRegExp();
const result = [];
function findEmoji(text: string) {
const result: EmojiPart[] = [];
let match; let match;
while ((match = emojiRegExp.exec(text))) { while ((match = regExp.exec(text))) {
result.push({ result.push({
start: match.index, start: match.index,
end: match.index + match[0].length, end: match.index + match[0].length,
@ -15,4 +18,8 @@ function findEmoji(text) {
return result; return result;
} }
export type EmojiPart = Part & {
emoji: string;
};
export default findEmoji; export default findEmoji;

View file

@ -1,4 +1,5 @@
import LinkifyIt, {Match} from "linkify-it"; import LinkifyIt, {Match} from "linkify-it";
import {Part} from "./merge";
type OurMatch = Match & { type OurMatch = Match & {
noschema?: boolean; noschema?: boolean;
@ -24,7 +25,8 @@ LinkifyIt.prototype.normalize = function normalize(match: OurMatch) {
} }
}; };
const linkify = LinkifyIt().tlds(require("tlds")).tlds("onion", true); import tlds from "tlds";
const linkify = LinkifyIt().tlds(tlds).tlds("onion", true);
// Known schemes to detect in text // Known schemes to detect in text
const commonSchemes = [ const commonSchemes = [
@ -68,7 +70,7 @@ function findLinksWithSchema(text: string) {
return matches.filter((url) => !url.noschema).map(returnUrl); return matches.filter((url) => !url.noschema).map(returnUrl);
} }
function returnUrl(url: OurMatch) { function returnUrl(url: OurMatch): LinkPart {
return { return {
start: url.index, start: url.index,
end: url.lastIndex, end: url.lastIndex,
@ -76,4 +78,8 @@ function returnUrl(url: OurMatch) {
}; };
} }
export type LinkPart = Part & {
link: string;
};
export {findLinks, findLinksWithSchema}; export {findLinks, findLinksWithSchema};

View file

@ -1,17 +1,23 @@
import {Part} from "./merge";
const nickRegExp = /([\w[\]\\`^{|}-]+)/g; const nickRegExp = /([\w[\]\\`^{|}-]+)/g;
function findNames(text, users) { export type NamePart = Part & {
const result = []; nick: string;
};
function findNames(text: string, nicks: string[]): NamePart[] {
const result: NamePart[] = [];
// Return early if we don't have any nicknames to find // Return early if we don't have any nicknames to find
if (users.length === 0) { if (nicks.length === 0) {
return result; return result;
} }
let match; let match;
while ((match = nickRegExp.exec(text))) { while ((match = nickRegExp.exec(text))) {
if (users.indexOf(match[1]) > -1) { if (nicks.indexOf(match[1]) > -1) {
result.push({ result.push({
start: match.index, start: match.index,
end: match.index + match[1].length, end: match.index + match[1].length,

View file

@ -1,8 +1,17 @@
import anyIntersection from "./anyIntersection"; import anyIntersection from "./anyIntersection";
import fill from "./fill"; import fill from "./fill";
import {ChannelPart} from "./findChannels";
import {EmojiPart} from "./findEmoji";
import {NamePart} from "./findNames";
type TextPart = Part & {
text: string;
};
type Fragment = TextPart;
// Merge text part information within a styling fragment // Merge text part information within a styling fragment
function assign(textPart, fragment) { function assign(textPart: Part, fragment: Fragment) {
const fragStart = fragment.start; const fragStart = fragment.start;
const start = Math.max(fragment.start, textPart.start); const start = Math.max(fragment.start, textPart.start);
const end = Math.min(fragment.end, textPart.end); const end = Math.min(fragment.end, textPart.end);
@ -11,10 +20,20 @@ function assign(textPart, fragment) {
return Object.assign({}, fragment, {start, end, text}); return Object.assign({}, fragment, {start, end, text});
} }
function sortParts(a, b) { function sortParts(a: Part, b: Part) {
return a.start - b.start || b.end - a.end; return a.start - b.start || b.end - a.end;
} }
export type Part = {
start: number;
end: number;
fragments?: Fragment;
};
type MergedPart = TextPart | NamePart | EmojiPart | ChannelPart;
type MergedPartWithFragments = MergedPart & {fragments: Fragment[]};
// Merge the style fragments within the text parts, taking into account // Merge the style fragments within the text parts, taking into account
// boundaries and text sections that have not matched to links or channels. // boundaries and text sections that have not matched to links or channels.
// For example, given a string "foobar" where "foo" and "bar" have been // For example, given a string "foobar" where "foo" and "bar" have been
@ -22,9 +41,13 @@ function sortParts(a, b) {
// different styles, the first resulting part will contain fragments "fo" and // different styles, the first resulting part will contain fragments "fo" and
// "o", and the second resulting part will contain "b" and "ar". "o" and "b" // "o", and the second resulting part will contain "b" and "ar". "o" and "b"
// fragments will contain duplicate styling attributes. // fragments will contain duplicate styling attributes.
function merge(textParts, styleFragments, cleanText) { function merge(
textParts: MergedPart[],
styleFragments: Fragment[],
cleanText: string
): MergedPart[] {
// Remove overlapping parts // Remove overlapping parts
textParts = textParts.sort(sortParts).reduce((prev, curr) => { textParts = textParts.sort(sortParts).reduce<MergedPart[]>((prev, curr) => {
const intersection = prev.some((p) => anyIntersection(p, curr)); const intersection = prev.some((p) => anyIntersection(p, curr));
if (intersection) { if (intersection) {
@ -37,11 +60,14 @@ function merge(textParts, styleFragments, cleanText) {
// Every section of the original text that has not been captured in a "part" // Every section of the original text that has not been captured in a "part"
// is filled with "text" parts, dummy objects with start/end but no extra // is filled with "text" parts, dummy objects with start/end but no extra
// metadata. // metadata.
const allParts = textParts.concat(fill(textParts, cleanText)).sort(sortParts); // Sort all parts identified based on their position in the original text
const filled = fill(textParts, cleanText) as TextPart[];
const allParts: MergedPart[] = [...textParts, ...filled].sort(sortParts); // Sort all parts identified based on their position in the original text
// Distribute the style fragments within the text parts // Distribute the style fragments within the text parts
return allParts.map((textPart) => { return allParts.map((textPart) => {
textPart.fragments = styleFragments // TODO: remove any type casting.
(textPart as any).fragments = styleFragments
.filter((fragment) => anyIntersection(textPart, fragment)) .filter((fragment) => anyIntersection(textPart, fragment))
.map((fragment) => assign(textPart, fragment)); .map((fragment) => assign(textPart, fragment));

View file

@ -9,11 +9,17 @@ import LinkPreviewToggle from "../../components/LinkPreviewToggle.vue";
import LinkPreviewFileSize from "../../components/LinkPreviewFileSize.vue"; import LinkPreviewFileSize from "../../components/LinkPreviewFileSize.vue";
import InlineChannel from "../../components/InlineChannel.vue"; import InlineChannel from "../../components/InlineChannel.vue";
import Username from "../../components/Username.vue"; import Username from "../../components/Username.vue";
import {VNode} from "vue";
import Network from "src/models/network";
import {Message} from "src/models/msg";
const emojiModifiersRegex = /[\u{1f3fb}-\u{1f3ff}]|\u{fe0f}/gu; const emojiModifiersRegex = /[\u{1f3fb}-\u{1f3ff}]|\u{fe0f}/gu;
type createElement = (tag: string, props: any, children: any) => VNode;
// Create an HTML `span` with styling information for a given fragment // Create an HTML `span` with styling information for a given fragment
function createFragment(fragment, createElement) { // TODO: remove any
function createFragment(fragment: Record<any, string>, createElement: createElement) {
const classes = []; const classes = [];
if (fragment.bold) { if (fragment.bold) {
@ -44,7 +50,7 @@ function createFragment(fragment, createElement) {
classes.push("irc-monospace"); classes.push("irc-monospace");
} }
const data = {}; const data = {} as any;
let hasData = false; let hasData = false;
if (classes.length > 0) { if (classes.length > 0) {
@ -68,7 +74,7 @@ function createFragment(fragment, createElement) {
// Transform an IRC message potentially filled with styling control codes, URLs, // Transform an IRC message potentially filled with styling control codes, URLs,
// nicknames, and channels into a string of HTML elements to display on the client. // nicknames, and channels into a string of HTML elements to display on the client.
function parse(createElement, text, message = undefined, network = undefined) { function parse(createElement: createElement, text: string, message?: Message, network?: Network) {
// Extract the styling information and get the plain text version from it // Extract the styling information and get the plain text version from it
const styleFragments = parseStyle(text); const styleFragments = parseStyle(text);
const cleanText = styleFragments.map((fragment) => fragment.text).join(""); const cleanText = styleFragments.map((fragment) => fragment.text).join("");
@ -76,14 +82,15 @@ function parse(createElement, text, message = undefined, network = undefined) {
// On the plain text, find channels and URLs, returned as "parts". Parts are // On the plain text, find channels and URLs, returned as "parts". Parts are
// arrays of objects containing start and end markers, as well as metadata // arrays of objects containing start and end markers, as well as metadata
// depending on what was found (channel or link). // depending on what was found (channel or link).
const channelPrefixes = network ? network.serverOptions.CHANTYPES : ["#", "&"]; const channelPrefixes = network?.serverOptions?.CHANTYPES || ["#", "&"];
const userModes = network?.serverOptions?.PREFIX.symbols || ["!", "@", "%", "+"]; const userModes = network?.serverOptions?.PREFIX.symbols || ["!", "@", "%", "+"];
const channelParts = findChannels(cleanText, channelPrefixes, userModes); const channelParts = findChannels(cleanText, channelPrefixes, userModes);
const linkParts = findLinks(cleanText); const linkParts = findLinks(cleanText);
const emojiParts = findEmoji(cleanText); const emojiParts = findEmoji(cleanText);
const nameParts = findNames(cleanText, message ? message.users || [] : []); // TODO: remove type casting.
const nameParts = findNames(cleanText, message ? (message.users as string[]) || [] : []);
const parts = channelParts.concat(linkParts).concat(emojiParts).concat(nameParts); const parts = [...channelParts, ...linkParts, ...emojiParts, ...nameParts];
// The channel the message belongs to might not exist if the user isn't joined to it. // The channel the message belongs to might not exist if the user isn't joined to it.
const messageChannel = message ? message.channel : null; const messageChannel = message ? message.channel : null;
@ -91,12 +98,12 @@ function parse(createElement, text, message = undefined, network = undefined) {
// Merge the styling information with the channels / URLs / nicks / text objects and // Merge the styling information with the channels / URLs / nicks / text objects and
// generate HTML strings with the resulting fragments // generate HTML strings with the resulting fragments
return merge(parts, styleFragments, cleanText).map((textPart) => { return merge(parts, styleFragments, cleanText).map((textPart) => {
const fragments = textPart.fragments.map((fragment) => const fragments = textPart.fragments?.map((fragment) =>
createFragment(fragment, createElement) createFragment(fragment, createElement)
); );
// Wrap these potentially styled fragments with links and channel buttons // Wrap these potentially styled fragments with links and channel buttons
if (textPart.link) { if ("link" in textPart) {
const preview = const preview =
message && message &&
message.previews && message.previews &&

View file

@ -1,4 +1,4 @@
export default (stringUri) => { export default (stringUri: string) => {
const data = {}; const data = {};
try { try {

View file

@ -1,4 +1,4 @@
export default (count) => { export default (count: number) => {
if (count < 1000) { if (count < 1000) {
return count.toString(); return count.toString();
} }

View file

@ -5,6 +5,12 @@ import store from "../store";
import location from "../location"; import location from "../location";
let lastServerHash = null; let lastServerHash = null;
declare global {
interface Window {
g_TheLoungeRemoveLoading: () => void;
}
}
socket.on("auth:success", function () { socket.on("auth:success", function () {
store.commit("currentUserVisibleError", "Loading messages…"); store.commit("currentUserVisibleError", "Loading messages…");
updateLoadingMessage(); updateLoadingMessage();
@ -83,10 +89,10 @@ function showSignIn() {
} }
} }
function reloadPage(message) { function reloadPage(message: string) {
socket.disconnect(); socket.disconnect();
store.commit("currentUserVisibleError", message); store.commit("currentUserVisibleError", message);
location.reload(true); location.reload();
} }
function updateLoadingMessage() { function updateLoadingMessage() {

View file

@ -1,6 +1,6 @@
import io, {Socket} from "socket.io-client"; import io, {Socket} from "socket.io-client";
const socket = io({ const socket: Socket = io({
transports: JSON.parse(document.body.dataset.transports || "['polling', 'websocket']"), transports: JSON.parse(document.body.dataset.transports || "['polling', 'websocket']"),
path: window.location.pathname + "socket.io/", path: window.location.pathname + "socket.io/",
autoConnect: false, autoConnect: false,

View file

@ -2,6 +2,7 @@ import Vue from "vue";
import Vuex from "vuex"; import Vuex from "vuex";
import {createSettingsStore} from "./store-settings"; import {createSettingsStore} from "./store-settings";
import storage from "./localStorage"; import storage from "./localStorage";
import {ClientChan, ClientNetwork} from "./types";
const appName = document.title; const appName = document.title;
@ -21,15 +22,15 @@ function detectDesktopNotificationState() {
export type State = { export type State = {
appLoaded: boolean; appLoaded: boolean;
activeChannel?: { activeChannel: {
network: Network; network: ClientNetwork;
channel: ClientChan; channel: ClientChan;
}; };
currentUserVisibleError: string | null; currentUserVisibleError: string | null;
desktopNotificationState: "granted" | "blocked" | "nohttps" | "unsupported"; desktopNotificationState: "granted" | "blocked" | "nohttps" | "unsupported";
isAutoCompleting: boolean; isAutoCompleting: boolean;
isConnected: boolean; isConnected: boolean;
networks: Network[]; networks: ClientNetwork[];
// TODO: type // TODO: type
mentions: any[]; mentions: any[];
hasServiceWorker: boolean; hasServiceWorker: boolean;

View file

@ -1,4 +1,5 @@
import Chan from "../src/models/chan"; import Chan from "../../src/models/chan";
import Network from "../../src/models/network";
declare module "*.vue" { declare module "*.vue" {
import Vue from "vue"; import Vue from "vue";
@ -10,4 +11,9 @@ interface LoungeWindow extends Window {
type ClientChan = Chan & { type ClientChan = Chan & {
moreHistoryAvailable: boolean; moreHistoryAvailable: boolean;
editTopic: boolean;
};
type ClientNetwork = Network & {
isJoinChannelShown: boolean;
}; };

View file

@ -12,22 +12,31 @@ import eventbus from "./eventbus";
import "./socket-events"; import "./socket-events";
import "./webpush"; import "./webpush";
import "./keybinds"; import "./keybinds";
import {ClientChan} from "./types";
const favicon = document.getElementById("favicon"); const favicon = document.getElementById("favicon");
const faviconNormal = favicon?.getAttribute("href") || ""; const faviconNormal = favicon?.getAttribute("href") || "";
const faviconAlerted = favicon?.dataset.other || ""; const faviconAlerted = favicon?.dataset.other || "";
new Vue({ type Data = {};
export type Methods = {
switchToChannel: (channel: ClientChan) => void;
closeChannel: (channel: ClientChan) => void;
};
type Computed = {};
type Props = {};
new Vue<Data, Methods, Computed, Props>({
el: "#viewport", el: "#viewport",
router, router,
mounted() { mounted() {
socket.open(); socket.open();
}, },
methods: { methods: {
switchToChannel(channel: Channel) { switchToChannel(channel: ClientChan) {
navigate("RoutedChat", {id: channel.id}); navigate("RoutedChat", {id: channel.id});
}, },
closeChannel(channel: Channel) { closeChannel(channel: ClientChan) {
if (channel.type === "lobby") { if (channel.type === "lobby") {
eventbus.emit( eventbus.emit(
"confirm-dialog", "confirm-dialog",

View file

@ -6,6 +6,7 @@
"files": [ "files": [
"../package.json" "../package.json"
] /* If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. When a 'files' property is specified, only those files and those specified by 'include' are included. */, ] /* If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. When a 'files' property is specified, only those files and those specified by 'include' are included. */,
// "exclude": [],
"compilerOptions": { "compilerOptions": {
"sourceMap": false /*Create source map files for emitted JavaScript files. See more: https://www.typescriptlang.org/tsconfig#sourceMap */, "sourceMap": false /*Create source map files for emitted JavaScript files. See more: https://www.typescriptlang.org/tsconfig#sourceMap */,
"jsx": "preserve" /* Specify what JSX code is generated. */, "jsx": "preserve" /* Specify what JSX code is generated. */,
@ -19,6 +20,8 @@
"module": "es2015", "module": "es2015",
"moduleResolution": "node", "moduleResolution": "node",
// TODO: Remove eventually, this is due to typescript checking vue files that don't have lang="ts".
"checkJs": false,
// TODO: Remove eventually // TODO: Remove eventually
"noImplicitAny": false /*Enable error reporting for expressions and declarations with an implied any type. See more: https://www.typescriptlang.org/tsconfig#noImplicitAny */ "noImplicitAny": false /*Enable error reporting for expressions and declarations with an implied any type. See more: https://www.typescriptlang.org/tsconfig#noImplicitAny */
} /* Instructs the TypeScript compiler how to compile .ts files. */ } /* Instructs the TypeScript compiler how to compile .ts files. */

View file

@ -25,7 +25,7 @@
"lint:tsc": "tsc --noEmit", "lint:tsc": "tsc --noEmit",
"start": "node src/dist/src/index start", "start": "node src/dist/src/index start",
"test": "run-p --aggregate-output --continue-on-error lint:* test:*", "test": "run-p --aggregate-output --continue-on-error lint:* test:*",
"test:mocha": "NODE_ENV=test webpack --mode=development && nyc --nycrc-path=test/.nycrc-mocha.json mocha --colors --config=test/.mocharc.yml", "test:mocha": "NODE_ENV=test webpack --mode=development && nyc --nycrc-path=test/.nycrc-mocha.json mocha --require ts-node/register --colors --config=test/.mocharc.yml",
"watch": "webpack --watch" "watch": "webpack --watch"
}, },
"keywords": [ "keywords": [

View file

@ -17,7 +17,7 @@ import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
import TextFileMessageStorage from "./plugins/messageStorage/text"; import TextFileMessageStorage from "./plugins/messageStorage/text";
import Network, {NetworkWithIrcFramework} from "./models/network"; import Network, {NetworkWithIrcFramework} from "./models/network";
import ClientManager from "./clientManager"; import ClientManager from "./clientManager";
import {MessageStorage} from "./types/plugins/messageStorage"; import {MessageStorage, SearchQuery, SearchResponse} from "./plugins/messageStorage/types";
const events = [ const events = [
"away", "away",
@ -598,9 +598,15 @@ class Client {
} }
} }
search(query: string) { search(query: SearchQuery): Promise<SearchResponse> {
if (this.messageProvider === undefined) { if (this.messageProvider === undefined) {
return Promise.resolve([]); return Promise.resolve({
results: [],
target: "",
networkUuid: "",
offset: 0,
searchTerm: query?.searchTerm,
});
} }
return this.messageProvider.search(query); return this.messageProvider.search(query);

View file

@ -1,6 +1,7 @@
import Client from "../../client"; import Client from "../../client";
import Chan, {Channel} from "../../models/chan"; import Chan, {Channel} from "../../models/chan";
import Network, {NetworkWithIrcFramework} from "../../models/network"; import Network, {NetworkWithIrcFramework} from "../../models/network";
import {PackageInfo} from "../packages";
export type PluginInputHandler = ( export type PluginInputHandler = (
this: Client, this: Client,
@ -97,7 +98,7 @@ const getCommands = () =>
.concat(passThroughCommands) .concat(passThroughCommands)
.sort(); .sort();
const addPluginCommand = (packageInfo, command, func) => { const addPluginCommand = (packageInfo: PackageInfo, command, func) => {
func.packageInfo = packageInfo; func.packageInfo = packageInfo;
pluginCommands.set(command, func); pluginCommands.set(command, func);
}; };

View file

@ -7,8 +7,9 @@ import Config from "../../config";
import Msg, {Message} from "../../models/msg"; import Msg, {Message} from "../../models/msg";
import Client from "../../client"; import Client from "../../client";
import Chan, {Channel} from "../../models/chan"; import Chan, {Channel} from "../../models/chan";
import type {SqliteMessageStorage as ISqliteMessageStorage} from "../../types/plugins/messageStorage"; import type {SearchResponse, SqliteMessageStorage as ISqliteMessageStorage} from "./types";
import Network from "../../models/network"; import Network from "../../models/network";
import {SearchQuery} from "./types";
// TODO; type // TODO; type
let sqlite3: any; let sqlite3: any;
@ -209,9 +210,15 @@ class SqliteMessageStorage implements ISqliteMessageStorage {
}) as Promise<Message[]>; }) as Promise<Message[]>;
} }
search(query: {searchTerm: string; networkUuid: string; channelName: string; offset: string}) { search(query: SearchQuery): Promise<SearchResponse> {
if (!this.isEnabled) { if (!this.isEnabled) {
return Promise.resolve([]); return Promise.resolve({
results: [],
target: "",
networkUuid: "",
offset: 0,
searchTerm: query?.searchTerm,
});
} }
// Using the '@' character to escape '%' and '_' in patterns. // Using the '@' character to escape '%' and '_' in patterns.
@ -243,7 +250,7 @@ class SqliteMessageStorage implements ISqliteMessageStorage {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
const response = { const response: SearchResponse = {
searchTerm: query.searchTerm, searchTerm: query.searchTerm,
target: query.channelName, target: query.channelName,
networkUuid: query.networkUuid, networkUuid: query.networkUuid,
@ -263,7 +270,8 @@ class SqliteMessageStorage implements ISqliteMessageStorage {
export default SqliteMessageStorage; export default SqliteMessageStorage;
function parseSearchRowsToMessages(id, rows) { // TODO: type any
function parseSearchRowsToMessages(id: string, rows: any[]) {
const messages: Msg[] = []; const messages: Msg[] = [];
for (const row of rows) { for (const row of rows) {

View file

@ -4,7 +4,7 @@ import filenamify from "filenamify";
import log from "../../log"; import log from "../../log";
import Config from "../../config"; import Config from "../../config";
import {MessageStorage} from "../../types/plugins/messageStorage"; import {MessageStorage} from "./types";
import Client from "../../client"; import Client from "../../client";
import Channel from "../../models/chan"; import Channel from "../../models/chan";
import {Message, MessageType} from "../../models/msg"; import {Message, MessageType} from "../../models/msg";

View file

@ -22,6 +22,22 @@ interface MessageStorage {
canProvideMessages(): boolean; canProvideMessages(): boolean;
} }
export type SearchQuery = {
searchTerm: string;
networkUuid: string;
channelName: string;
offset: string;
};
export type SearchResponse = Omit<SearchQuery, "channelName" | "offset"> & {
results: Message[];
target: string;
offset: number;
};
type SearchFunction = (query: SearchQuery) => Promise<SearchResponse>;
export interface SqliteMessageStorage extends MessageStorage { export interface SqliteMessageStorage extends MessageStorage {
database: Database; database: Database;
search: SearchFunction;
} }

View file

@ -12,12 +12,14 @@ import fs from "fs";
import Utils from "../../command-line/utils"; import Utils from "../../command-line/utils";
import Client from "../../client"; import Client from "../../client";
type PackageInfo = { export type PackageInfo = {
packageName: string; packageName: string;
thelounge?: {supports: any}; thelounge?: {supports: any};
version: string; version: string;
type?: string; type?: string;
files?: string[]; files?: string[];
// Legacy support
name?: string;
}; };
const stylesheets: string[] = []; const stylesheets: string[] = [];

View file

@ -1,12 +1,13 @@
import {PackageInfo} from "./index";
import Client from "../../client"; import Client from "../../client";
import Chan from "../../models/chan"; import Chan from "../../models/chan";
import Msg, {MessageType, UserInMessage} from "../../models/msg"; import Msg, {MessageType, UserInMessage} from "../../models/msg";
export default class PublicClient { export default class PublicClient {
private client: Client; private client: Client;
private packageInfo: any; private packageInfo: PackageInfo;
constructor(client, packageInfo) { constructor(client: Client, packageInfo: PackageInfo) {
this.client = client; this.client = client;
this.packageInfo = packageInfo; this.packageInfo = packageInfo;
} }
@ -24,7 +25,7 @@ export default class PublicClient {
* *
* @param {Object} attributes * @param {Object} attributes
*/ */
createChannel(attributes) { createChannel(attributes: Partial<Chan>) {
return this.client.createChannel(attributes); return this.client.createChannel(attributes);
} }

View file

@ -1,13 +1,13 @@
{ {
"extends": "../tsconfig.base.json" /* Path to base configuration file to inherit from. Requires TypeScript version 2.1 or later. */, "extends": "../tsconfig.base.json" /* Path to base configuration file to inherit from. Requires TypeScript version 2.1 or later. */,
"include": [ "include": [
"**/*" "**/*",
"../client/js/helpers/ircmessageparser/*.ts"
] /* Specifies a list of glob patterns that match files to be included in compilation. If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. Requires TypeScript version 2.0 or later. */, ] /* Specifies a list of glob patterns that match files to be included in compilation. If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. Requires TypeScript version 2.0 or later. */,
"files": [ "files": [
"../babel.config.cjs",
"../client/js/constants.ts", "../client/js/constants.ts",
"../client/js/helpers/ircmessageparser/cleanIrcMessage.ts",
"../client/js/helpers/ircmessageparser/findLinks.ts", "../babel.config.cjs",
"../defaults/config.js", "../defaults/config.js",
"../package.json", "../package.json",
"../webpack.config.ts" "../webpack.config.ts"

View file

@ -1,2 +1 @@
import "./modules"; import "./modules";
import "./plugins";

View file

@ -1 +0,0 @@
import "./messageStorage";

View file

@ -5,4 +5,4 @@ reporter: dot
interactive: false interactive: false
spec: "test/**/*.ts" spec: "test/**/*.ts"
ignore: "test/client/**" ignore: "test/client/**"
require: "test/fixtures/env" require: "test/fixtures/env.ts"

View file

@ -1,13 +1,13 @@
"use strict"; "use strict";
var config = require("../../../defaults/config.js"); import config from "../../../defaults/config.js";
config.defaults.name = "Example IRC Server"; config.defaults.name = "Example IRC Server";
config.defaults.host = "irc.example.com"; config.defaults.host = "irc.example.com";
config.public = true; config.public = true;
config.prefetch = true; config.prefetch = true;
config.host = config.bind = "127.0.0.1"; config.host = bind = "127.0.0.1";
config.port = 61337; config.port = 61337;
config.transports = ["websocket"]; config.transports = ["websocket"];
module.exports = config; export default config;

View file

@ -10,7 +10,7 @@ config.setHome(home);
import STSPolicies from "../../src/plugins/sts"; // Must be imported *after* setHome import STSPolicies from "../../src/plugins/sts"; // Must be imported *after* setHome
const mochaGlobalTeardown = async function () { const mochaGlobalTeardown = function () {
STSPolicies.refresh.cancel(); // Cancel debounced function, so it does not write later STSPolicies.refresh.cancel(); // Cancel debounced function, so it does not write later
fs.unlinkSync(STSPolicies.stsFile); fs.unlinkSync(STSPolicies.stsFile);
}; };

View file

@ -6,7 +6,8 @@
"../src" "../src"
] /* Specifies a list of glob patterns that match files to be included in compilation. If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. Requires TypeScript version 2.0 or later. */, ] /* Specifies a list of glob patterns that match files to be included in compilation. If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. Requires TypeScript version 2.0 or later. */,
"files": [ "files": [
"../babel.config.cjs" "../babel.config.cjs",
"../src/helper.ts"
] /* If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. When a 'files' property is specified, only those files and those specified by 'include' are included. */, ] /* If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. When a 'files' property is specified, only those files and those specified by 'include' are included. */,
"ts-node": { "ts-node": {
"files": true "files": true

View file

@ -47,7 +47,8 @@
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */ /* outDir is necessary because otherwise the built output for files like babel.config.cjs would overwrite the input. */
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */ // "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */ // "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */

View file

@ -5,6 +5,7 @@
"./babel.config.cjs", "./babel.config.cjs",
"./src/helper.ts" "./src/helper.ts"
] /* If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. When a 'files' property is specified, only those files and those specified by 'include' are included. */, ] /* If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. When a 'files' property is specified, only those files and those specified by 'include' are included. */,
// "exclude": [],
"references": [ "references": [
{ {
"path": "./client" /* Path to referenced tsconfig or to folder containing tsconfig. */ "path": "./client" /* Path to referenced tsconfig or to folder containing tsconfig. */

View file

@ -187,6 +187,10 @@ export default (env: any, argv: any) => {
filename: "css/style.css", filename: "css/style.css",
}), }),
new MiniCssExtractPlugin({
filename: "css/style.css",
}),
// Client tests that require Vue may end up requireing socket.io // Client tests that require Vue may end up requireing socket.io
new webpack.NormalModuleReplacementPlugin( new webpack.NormalModuleReplacementPlugin(
/js(\/|\\)socket\.js/, /js(\/|\\)socket\.js/,