diff --git a/.eslintignore b/.eslintignore index f47503b7..d4e5515c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ public/ coverage/ src/dist/ +dist/ diff --git a/.gitignore b/.gitignore index 8a787eea..6371eb74 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ coverage/ public/ client/dist src/dist +dist/ diff --git a/.prettierignore b/.prettierignore index 3f173ad3..47a38736 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ test/fixtures/.thelounge/logs/ test/fixtures/.thelounge/certificates/ test/fixtures/.thelounge/storage/ src/dist/ +dist/ *.log *.png *.svg diff --git a/client/components/NetworkList.vue b/client/components/NetworkList.vue index 67efc352..915cda1b 100644 --- a/client/components/NetworkList.vue +++ b/client/components/NetworkList.vue @@ -195,7 +195,7 @@ } - diff --git a/client/js/autocompletion.ts b/client/js/autocompletion.ts index d67a3743..9a12a62c 100644 --- a/client/js/autocompletion.ts +++ b/client/js/autocompletion.ts @@ -274,10 +274,6 @@ function fuzzyGrep(term: string, array: Array) { } function rawNicks() { - if (!store.state.activeChannel) { - return []; - } - if (store.state.activeChannel.channel.users.length > 0) { const users = store.state.activeChannel.channel.users.slice(); diff --git a/client/js/commands/collapse.ts b/client/js/commands/collapse.ts index 7c8cf6d7..a2a8581c 100644 --- a/client/js/commands/collapse.ts +++ b/client/js/commands/collapse.ts @@ -4,7 +4,7 @@ import store from "../store"; function input() { const messageIds = []; - for (const message of store.state.activeChannel.channel.messages) { + for (const message of store.state.activeChannel?.channel.messages) { let toggled = false; for (const preview of message.previews) { @@ -22,7 +22,7 @@ function input() { // Tell the server we're toggling so it remembers at page reload if (!document.body.classList.contains("public") && messageIds.length > 0) { socket.emit("msg:preview:toggle", { - target: store.state.activeChannel.channel.id, + target: store.state.activeChannel?.channel.id, messageIds: messageIds, shown: false, }); diff --git a/client/js/commands/expand.ts b/client/js/commands/expand.ts index 6942d795..d8829594 100644 --- a/client/js/commands/expand.ts +++ b/client/js/commands/expand.ts @@ -22,7 +22,7 @@ function input() { // Tell the server we're toggling so it remembers at page reload if (!document.body.classList.contains("public") && messageIds.length > 0) { socket.emit("msg:preview:toggle", { - target: store.state.activeChannel.channel.id, + target: store.state.activeChannel?.channel.id, messageIds: messageIds, shown: true, }); diff --git a/client/js/commands/index.ts b/client/js/commands/index.ts index 05dbafda..247ed3e3 100644 --- a/client/js/commands/index.ts +++ b/client/js/commands/index.ts @@ -4,9 +4,9 @@ // directory, so we iterate over its content, which is a map statically built by // Webpack. // 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>((acc, path) => { const command = path.substring(2, path.length - 3); if (command === "index") { diff --git a/client/js/helpers/contextMenu.ts b/client/js/helpers/contextMenu.ts index 637267fc..1d75b29b 100644 --- a/client/js/helpers/contextMenu.ts +++ b/client/js/helpers/contextMenu.ts @@ -1,6 +1,7 @@ import socket from "../socket"; import eventbus from "../eventbus"; - +import type {ClientChan, ClientNetwork} from "../types"; +import type {Methods} from "../vue"; type ContextMenuItem = | ({ label: string; @@ -18,7 +19,11 @@ type ContextMenuItem = type: "divider"; }; -export function generateChannelContextMenu($root, channel, network) { +export function generateChannelContextMenu( + $root: Methods, + channel: ClientChan, + network: ClientNetwork +) { const typeMap = { lobby: "network", channel: "chan", diff --git a/client/js/helpers/ircmessageparser/anyIntersection.ts b/client/js/helpers/ircmessageparser/anyIntersection.ts index 8d773ae7..cf2fd156 100644 --- a/client/js/helpers/ircmessageparser/anyIntersection.ts +++ b/client/js/helpers/ircmessageparser/anyIntersection.ts @@ -1,8 +1,9 @@ -import {ParsedStyle} from "./parseStyle"; - // 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. -function anyIntersection(a: ParsedStyle, b: ParsedStyle) { +function anyIntersection(a: Part, b: Part) { return ( (a.start <= b.start && b.start < a.end) || (a.start < b.end && b.end <= a.end) || diff --git a/client/js/helpers/ircmessageparser/cleanIrcMessage.ts b/client/js/helpers/ircmessageparser/cleanIrcMessage.ts index 7e4fdf35..3b79d2b8 100644 --- a/client/js/helpers/ircmessageparser/cleanIrcMessage.ts +++ b/client/js/helpers/ircmessageparser/cleanIrcMessage.ts @@ -1,4 +1,5 @@ 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; export default (message: string) => message.replace(matchFormatting, "").trim(); diff --git a/client/js/helpers/ircmessageparser/fill.ts b/client/js/helpers/ircmessageparser/fill.ts index a36262ac..c9e613f1 100644 --- a/client/js/helpers/ircmessageparser/fill.ts +++ b/client/js/helpers/ircmessageparser/fill.ts @@ -1,15 +1,16 @@ -import {ParsedStyle} from "./parseStyle"; - // 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 + +import {Part} from "./merge"; + // parsed into recognizable entries already. -function fill(existingEntries: ParsedStyle[], text: string) { +function fill(existingEntries: Part[], text: string) { let position = 0; // 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 // corresponding to `bar`. - const result = existingEntries.reduce((acc: Omit[], textSegment) => { + const result = existingEntries.reduce((acc, textSegment) => { if (textSegment.start > position) { acc.push({ start: position, diff --git a/client/js/helpers/ircmessageparser/findChannels.ts b/client/js/helpers/ircmessageparser/findChannels.ts index 809863d6..0206929f 100644 --- a/client/js/helpers/ircmessageparser/findChannels.ts +++ b/client/js/helpers/ircmessageparser/findChannels.ts @@ -2,13 +2,14 @@ // ")", "[", "]", "{", "}", and "|" in string. // See https://lodash.com/docs/#escapeRegExp import escapeRegExp from "lodash/escapeRegExp"; +import {Part} from "./merge"; // 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 // text. // It returns an array of objects for each channel found with their start index, // 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. // For example, a voiced user in #thelounge will have a /whois response of: // > foo is on the following channels: +#thelounge @@ -18,7 +19,7 @@ function findChannels(text, channelPrefixes, userModes) { const channelPattern = `(?:^|\\s)[${userModePattern}]*([${channelPrefixPattern}][^ \u0007]+)`; const channelRegExp = new RegExp(channelPattern, "g"); - const result = []; + const result: ChannelPart[] = []; let match; do { @@ -38,4 +39,8 @@ function findChannels(text, channelPrefixes, userModes) { return result; } +export type ChannelPart = Part & { + channel: string; +}; + export default findChannels; diff --git a/client/js/helpers/ircmessageparser/findEmoji.ts b/client/js/helpers/ircmessageparser/findEmoji.ts index 14e8caa9..57e8bdef 100644 --- a/client/js/helpers/ircmessageparser/findEmoji.ts +++ b/client/js/helpers/ircmessageparser/findEmoji.ts @@ -1,10 +1,13 @@ -const emojiRegExp = require("emoji-regex")(); +import emojiRegExp from "emoji-regex"; +import {Part} from "./merge"; -function findEmoji(text) { - const result = []; +const regExp = emojiRegExp(); + +function findEmoji(text: string) { + const result: EmojiPart[] = []; let match; - while ((match = emojiRegExp.exec(text))) { + while ((match = regExp.exec(text))) { result.push({ start: match.index, end: match.index + match[0].length, @@ -15,4 +18,8 @@ function findEmoji(text) { return result; } +export type EmojiPart = Part & { + emoji: string; +}; + export default findEmoji; diff --git a/client/js/helpers/ircmessageparser/findLinks.ts b/client/js/helpers/ircmessageparser/findLinks.ts index 9adb0627..95f94029 100644 --- a/client/js/helpers/ircmessageparser/findLinks.ts +++ b/client/js/helpers/ircmessageparser/findLinks.ts @@ -1,4 +1,5 @@ import LinkifyIt, {Match} from "linkify-it"; +import {Part} from "./merge"; type OurMatch = Match & { 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 const commonSchemes = [ @@ -68,7 +70,7 @@ function findLinksWithSchema(text: string) { return matches.filter((url) => !url.noschema).map(returnUrl); } -function returnUrl(url: OurMatch) { +function returnUrl(url: OurMatch): LinkPart { return { start: url.index, end: url.lastIndex, @@ -76,4 +78,8 @@ function returnUrl(url: OurMatch) { }; } +export type LinkPart = Part & { + link: string; +}; + export {findLinks, findLinksWithSchema}; diff --git a/client/js/helpers/ircmessageparser/findNames.ts b/client/js/helpers/ircmessageparser/findNames.ts index 5c683150..be16f7e5 100644 --- a/client/js/helpers/ircmessageparser/findNames.ts +++ b/client/js/helpers/ircmessageparser/findNames.ts @@ -1,17 +1,23 @@ +import {Part} from "./merge"; + const nickRegExp = /([\w[\]\\`^{|}-]+)/g; -function findNames(text, users) { - const result = []; +export type NamePart = Part & { + nick: string; +}; + +function findNames(text: string, nicks: string[]): NamePart[] { + const result: NamePart[] = []; // Return early if we don't have any nicknames to find - if (users.length === 0) { + if (nicks.length === 0) { return result; } let match; while ((match = nickRegExp.exec(text))) { - if (users.indexOf(match[1]) > -1) { + if (nicks.indexOf(match[1]) > -1) { result.push({ start: match.index, end: match.index + match[1].length, diff --git a/client/js/helpers/ircmessageparser/merge.ts b/client/js/helpers/ircmessageparser/merge.ts index 8f83763a..82873ba5 100644 --- a/client/js/helpers/ircmessageparser/merge.ts +++ b/client/js/helpers/ircmessageparser/merge.ts @@ -1,8 +1,17 @@ import anyIntersection from "./anyIntersection"; 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 -function assign(textPart, fragment) { +function assign(textPart: Part, fragment: Fragment) { const fragStart = fragment.start; const start = Math.max(fragment.start, textPart.start); const end = Math.min(fragment.end, textPart.end); @@ -11,10 +20,20 @@ function assign(textPart, fragment) { 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; } +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 // boundaries and text sections that have not matched to links or channels. // 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 // "o", and the second resulting part will contain "b" and "ar". "o" and "b" // fragments will contain duplicate styling attributes. -function merge(textParts, styleFragments, cleanText) { +function merge( + textParts: MergedPart[], + styleFragments: Fragment[], + cleanText: string +): MergedPart[] { // Remove overlapping parts - textParts = textParts.sort(sortParts).reduce((prev, curr) => { + textParts = textParts.sort(sortParts).reduce((prev, curr) => { const intersection = prev.some((p) => anyIntersection(p, curr)); 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" // is filled with "text" parts, dummy objects with start/end but no extra // 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 return allParts.map((textPart) => { - textPart.fragments = styleFragments + // TODO: remove any type casting. + (textPart as any).fragments = styleFragments .filter((fragment) => anyIntersection(textPart, fragment)) .map((fragment) => assign(textPart, fragment)); diff --git a/client/js/helpers/parse.ts b/client/js/helpers/parse.ts index c604c570..1d876a69 100644 --- a/client/js/helpers/parse.ts +++ b/client/js/helpers/parse.ts @@ -9,11 +9,17 @@ import LinkPreviewToggle from "../../components/LinkPreviewToggle.vue"; import LinkPreviewFileSize from "../../components/LinkPreviewFileSize.vue"; import InlineChannel from "../../components/InlineChannel.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; +type createElement = (tag: string, props: any, children: any) => VNode; + // Create an HTML `span` with styling information for a given fragment -function createFragment(fragment, createElement) { +// TODO: remove any +function createFragment(fragment: Record, createElement: createElement) { const classes = []; if (fragment.bold) { @@ -44,7 +50,7 @@ function createFragment(fragment, createElement) { classes.push("irc-monospace"); } - const data = {}; + const data = {} as any; let hasData = false; if (classes.length > 0) { @@ -68,7 +74,7 @@ function createFragment(fragment, createElement) { // 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. -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 const styleFragments = parseStyle(text); 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 // arrays of objects containing start and end markers, as well as metadata // 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 channelParts = findChannels(cleanText, channelPrefixes, userModes); const linkParts = findLinks(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. 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 // generate HTML strings with the resulting fragments return merge(parts, styleFragments, cleanText).map((textPart) => { - const fragments = textPart.fragments.map((fragment) => + const fragments = textPart.fragments?.map((fragment) => createFragment(fragment, createElement) ); // Wrap these potentially styled fragments with links and channel buttons - if (textPart.link) { + if ("link" in textPart) { const preview = message && message.previews && diff --git a/client/js/helpers/parseIrcUri.ts b/client/js/helpers/parseIrcUri.ts index 8066651c..22ed3c56 100644 --- a/client/js/helpers/parseIrcUri.ts +++ b/client/js/helpers/parseIrcUri.ts @@ -1,4 +1,4 @@ -export default (stringUri) => { +export default (stringUri: string) => { const data = {}; try { diff --git a/client/js/helpers/roundBadgeNumber.ts b/client/js/helpers/roundBadgeNumber.ts index 74a777f7..dd7b6fdc 100644 --- a/client/js/helpers/roundBadgeNumber.ts +++ b/client/js/helpers/roundBadgeNumber.ts @@ -1,4 +1,4 @@ -export default (count) => { +export default (count: number) => { if (count < 1000) { return count.toString(); } diff --git a/client/js/socket-events/auth.ts b/client/js/socket-events/auth.ts index 0d0037dd..248f8a3b 100644 --- a/client/js/socket-events/auth.ts +++ b/client/js/socket-events/auth.ts @@ -5,6 +5,12 @@ import store from "../store"; import location from "../location"; let lastServerHash = null; +declare global { + interface Window { + g_TheLoungeRemoveLoading: () => void; + } +} + socket.on("auth:success", function () { store.commit("currentUserVisibleError", "Loading messages…"); updateLoadingMessage(); @@ -83,10 +89,10 @@ function showSignIn() { } } -function reloadPage(message) { +function reloadPage(message: string) { socket.disconnect(); store.commit("currentUserVisibleError", message); - location.reload(true); + location.reload(); } function updateLoadingMessage() { diff --git a/client/js/socket.ts b/client/js/socket.ts index 7c6068ed..ac08346b 100644 --- a/client/js/socket.ts +++ b/client/js/socket.ts @@ -1,6 +1,6 @@ import io, {Socket} from "socket.io-client"; -const socket = io({ +const socket: Socket = io({ transports: JSON.parse(document.body.dataset.transports || "['polling', 'websocket']"), path: window.location.pathname + "socket.io/", autoConnect: false, diff --git a/client/js/store.ts b/client/js/store.ts index 168992b7..9df4b138 100644 --- a/client/js/store.ts +++ b/client/js/store.ts @@ -2,6 +2,7 @@ import Vue from "vue"; import Vuex from "vuex"; import {createSettingsStore} from "./store-settings"; import storage from "./localStorage"; +import {ClientChan, ClientNetwork} from "./types"; const appName = document.title; @@ -21,15 +22,15 @@ function detectDesktopNotificationState() { export type State = { appLoaded: boolean; - activeChannel?: { - network: Network; + activeChannel: { + network: ClientNetwork; channel: ClientChan; }; currentUserVisibleError: string | null; desktopNotificationState: "granted" | "blocked" | "nohttps" | "unsupported"; isAutoCompleting: boolean; isConnected: boolean; - networks: Network[]; + networks: ClientNetwork[]; // TODO: type mentions: any[]; hasServiceWorker: boolean; diff --git a/client/index.ts b/client/js/types.d.ts similarity index 55% rename from client/index.ts rename to client/js/types.d.ts index 9a61ce27..23947726 100644 --- a/client/index.ts +++ b/client/js/types.d.ts @@ -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" { import Vue from "vue"; @@ -10,4 +11,9 @@ interface LoungeWindow extends Window { type ClientChan = Chan & { moreHistoryAvailable: boolean; + editTopic: boolean; +}; + +type ClientNetwork = Network & { + isJoinChannelShown: boolean; }; diff --git a/client/js/vue.ts b/client/js/vue.ts index 9713ef22..80833412 100644 --- a/client/js/vue.ts +++ b/client/js/vue.ts @@ -12,22 +12,31 @@ import eventbus from "./eventbus"; import "./socket-events"; import "./webpush"; import "./keybinds"; +import {ClientChan} from "./types"; const favicon = document.getElementById("favicon"); const faviconNormal = favicon?.getAttribute("href") || ""; 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({ el: "#viewport", router, mounted() { socket.open(); }, methods: { - switchToChannel(channel: Channel) { + switchToChannel(channel: ClientChan) { navigate("RoutedChat", {id: channel.id}); }, - closeChannel(channel: Channel) { + closeChannel(channel: ClientChan) { if (channel.type === "lobby") { eventbus.emit( "confirm-dialog", diff --git a/client/tsconfig.json b/client/tsconfig.json index 80b40556..d2453859 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -6,6 +6,7 @@ "files": [ "../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. */, + // "exclude": [], "compilerOptions": { "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. */, @@ -19,6 +20,8 @@ "module": "es2015", "moduleResolution": "node", + // TODO: Remove eventually, this is due to typescript checking vue files that don't have lang="ts". + "checkJs": false, // 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 */ } /* Instructs the TypeScript compiler how to compile .ts files. */ diff --git a/package.json b/package.json index a235c2d1..f1fbc3e4 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "lint:tsc": "tsc --noEmit", "start": "node src/dist/src/index start", "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" }, "keywords": [ diff --git a/src/client.ts b/src/client.ts index c45f3c0b..7ebae370 100644 --- a/src/client.ts +++ b/src/client.ts @@ -17,7 +17,7 @@ import SqliteMessageStorage from "./plugins/messageStorage/sqlite"; import TextFileMessageStorage from "./plugins/messageStorage/text"; import Network, {NetworkWithIrcFramework} from "./models/network"; import ClientManager from "./clientManager"; -import {MessageStorage} from "./types/plugins/messageStorage"; +import {MessageStorage, SearchQuery, SearchResponse} from "./plugins/messageStorage/types"; const events = [ "away", @@ -598,9 +598,15 @@ class Client { } } - search(query: string) { + search(query: SearchQuery): Promise { if (this.messageProvider === undefined) { - return Promise.resolve([]); + return Promise.resolve({ + results: [], + target: "", + networkUuid: "", + offset: 0, + searchTerm: query?.searchTerm, + }); } return this.messageProvider.search(query); diff --git a/src/plugins/inputs/index.ts b/src/plugins/inputs/index.ts index 72036491..6d333b6a 100644 --- a/src/plugins/inputs/index.ts +++ b/src/plugins/inputs/index.ts @@ -1,6 +1,7 @@ import Client from "../../client"; import Chan, {Channel} from "../../models/chan"; import Network, {NetworkWithIrcFramework} from "../../models/network"; +import {PackageInfo} from "../packages"; export type PluginInputHandler = ( this: Client, @@ -97,7 +98,7 @@ const getCommands = () => .concat(passThroughCommands) .sort(); -const addPluginCommand = (packageInfo, command, func) => { +const addPluginCommand = (packageInfo: PackageInfo, command, func) => { func.packageInfo = packageInfo; pluginCommands.set(command, func); }; diff --git a/src/plugins/messageStorage/sqlite.ts b/src/plugins/messageStorage/sqlite.ts index cd20cc64..518533d0 100644 --- a/src/plugins/messageStorage/sqlite.ts +++ b/src/plugins/messageStorage/sqlite.ts @@ -7,8 +7,9 @@ import Config from "../../config"; import Msg, {Message} from "../../models/msg"; import Client from "../../client"; 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 {SearchQuery} from "./types"; // TODO; type let sqlite3: any; @@ -209,9 +210,15 @@ class SqliteMessageStorage implements ISqliteMessageStorage { }) as Promise; } - search(query: {searchTerm: string; networkUuid: string; channelName: string; offset: string}) { + search(query: SearchQuery): Promise { 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. @@ -243,7 +250,7 @@ class SqliteMessageStorage implements ISqliteMessageStorage { if (err) { reject(err); } else { - const response = { + const response: SearchResponse = { searchTerm: query.searchTerm, target: query.channelName, networkUuid: query.networkUuid, @@ -263,7 +270,8 @@ class SqliteMessageStorage implements ISqliteMessageStorage { export default SqliteMessageStorage; -function parseSearchRowsToMessages(id, rows) { +// TODO: type any +function parseSearchRowsToMessages(id: string, rows: any[]) { const messages: Msg[] = []; for (const row of rows) { diff --git a/src/plugins/messageStorage/text.ts b/src/plugins/messageStorage/text.ts index 2010aebf..b2199ca9 100644 --- a/src/plugins/messageStorage/text.ts +++ b/src/plugins/messageStorage/text.ts @@ -4,7 +4,7 @@ import filenamify from "filenamify"; import log from "../../log"; import Config from "../../config"; -import {MessageStorage} from "../../types/plugins/messageStorage"; +import {MessageStorage} from "./types"; import Client from "../../client"; import Channel from "../../models/chan"; import {Message, MessageType} from "../../models/msg"; diff --git a/src/types/plugins/messageStorage/index.d.ts b/src/plugins/messageStorage/types.d.ts similarity index 65% rename from src/types/plugins/messageStorage/index.d.ts rename to src/plugins/messageStorage/types.d.ts index 1e0ddea5..3dd32ce6 100644 --- a/src/types/plugins/messageStorage/index.d.ts +++ b/src/plugins/messageStorage/types.d.ts @@ -22,6 +22,22 @@ interface MessageStorage { canProvideMessages(): boolean; } +export type SearchQuery = { + searchTerm: string; + networkUuid: string; + channelName: string; + offset: string; +}; + +export type SearchResponse = Omit & { + results: Message[]; + target: string; + offset: number; +}; + +type SearchFunction = (query: SearchQuery) => Promise; + export interface SqliteMessageStorage extends MessageStorage { database: Database; + search: SearchFunction; } diff --git a/src/plugins/packages/index.ts b/src/plugins/packages/index.ts index d01edc92..195663f9 100644 --- a/src/plugins/packages/index.ts +++ b/src/plugins/packages/index.ts @@ -12,12 +12,14 @@ import fs from "fs"; import Utils from "../../command-line/utils"; import Client from "../../client"; -type PackageInfo = { +export type PackageInfo = { packageName: string; thelounge?: {supports: any}; version: string; type?: string; files?: string[]; + // Legacy support + name?: string; }; const stylesheets: string[] = []; diff --git a/src/plugins/packages/publicClient.ts b/src/plugins/packages/publicClient.ts index b9267f08..109b1c4d 100644 --- a/src/plugins/packages/publicClient.ts +++ b/src/plugins/packages/publicClient.ts @@ -1,12 +1,13 @@ +import {PackageInfo} from "./index"; import Client from "../../client"; import Chan from "../../models/chan"; import Msg, {MessageType, UserInMessage} from "../../models/msg"; export default class PublicClient { private client: Client; - private packageInfo: any; + private packageInfo: PackageInfo; - constructor(client, packageInfo) { + constructor(client: Client, packageInfo: PackageInfo) { this.client = client; this.packageInfo = packageInfo; } @@ -24,7 +25,7 @@ export default class PublicClient { * * @param {Object} attributes */ - createChannel(attributes) { + createChannel(attributes: Partial) { return this.client.createChannel(attributes); } diff --git a/src/tsconfig.json b/src/tsconfig.json index 9b88fea2..771a9e6a 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,13 +1,13 @@ { "extends": "../tsconfig.base.json" /* Path to base configuration file to inherit from. Requires TypeScript version 2.1 or later. */, "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. */, "files": [ - "../babel.config.cjs", "../client/js/constants.ts", - "../client/js/helpers/ircmessageparser/cleanIrcMessage.ts", - "../client/js/helpers/ircmessageparser/findLinks.ts", + + "../babel.config.cjs", "../defaults/config.js", "../package.json", "../webpack.config.ts" diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 18affddd..09595928 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,2 +1 @@ import "./modules"; -import "./plugins"; diff --git a/src/types/plugins/index.d.ts b/src/types/plugins/index.d.ts deleted file mode 100644 index 9020c885..00000000 --- a/src/types/plugins/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -import "./messageStorage"; diff --git a/test/.mocharc.yml b/test/.mocharc.yml index 0c95c50f..06160b3c 100644 --- a/test/.mocharc.yml +++ b/test/.mocharc.yml @@ -5,4 +5,4 @@ reporter: dot interactive: false spec: "test/**/*.ts" ignore: "test/client/**" -require: "test/fixtures/env" +require: "test/fixtures/env.ts" diff --git a/test/fixtures/.thelounge/config.js b/test/fixtures/.thelounge/config.js index 9767acc9..63daf9c3 100644 --- a/test/fixtures/.thelounge/config.js +++ b/test/fixtures/.thelounge/config.js @@ -1,13 +1,13 @@ "use strict"; -var config = require("../../../defaults/config.js"); +import config from "../../../defaults/config.js"; config.defaults.name = "Example IRC Server"; config.defaults.host = "irc.example.com"; config.public = true; config.prefetch = true; -config.host = config.bind = "127.0.0.1"; +config.host = bind = "127.0.0.1"; config.port = 61337; config.transports = ["websocket"]; -module.exports = config; +export default config; diff --git a/test/fixtures/env.ts b/test/fixtures/env.ts index 069c7574..9a3c934a 100644 --- a/test/fixtures/env.ts +++ b/test/fixtures/env.ts @@ -10,7 +10,7 @@ config.setHome(home); 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 fs.unlinkSync(STSPolicies.stsFile); }; diff --git a/test/tsconfig.json b/test/tsconfig.json index ef6a077e..e083fb37 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -6,7 +6,8 @@ "../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. */, "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. */, "ts-node": { "files": true diff --git a/tsconfig.base.json b/tsconfig.base.json index 4fd84902..96492432 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -47,7 +47,8 @@ // "emitDeclarationOnly": true, /* Only output d.ts files and not 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. */ - // "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. */ // "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. */ diff --git a/tsconfig.json b/tsconfig.json index 853611c0..aca37f2f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "./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. */, + // "exclude": [], "references": [ { "path": "./client" /* Path to referenced tsconfig or to folder containing tsconfig. */ diff --git a/webpack.config.ts b/webpack.config.ts index f980a7ac..41a041e4 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -187,6 +187,10 @@ export default (env: any, argv: any) => { filename: "css/style.css", }), + new MiniCssExtractPlugin({ + filename: "css/style.css", + }), + // Client tests that require Vue may end up requireing socket.io new webpack.NormalModuleReplacementPlugin( /js(\/|\\)socket\.js/,