mirror of
https://github.com/thelounge/thelounge.git
synced 2024-06-08 00:32:19 +02:00
some test fixes
This commit is contained in:
parent
b798cfdc64
commit
4c98b81e35
|
@ -1,3 +1,4 @@
|
|||
public/
|
||||
coverage/
|
||||
src/dist/
|
||||
dist/
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,3 +8,4 @@ coverage/
|
|||
public/
|
||||
client/dist
|
||||
src/dist
|
||||
dist/
|
||||
|
|
|
@ -4,6 +4,7 @@ test/fixtures/.thelounge/logs/
|
|||
test/fixtures/.thelounge/certificates/
|
||||
test/fixtures/.thelounge/storage/
|
||||
src/dist/
|
||||
dist/
|
||||
*.log
|
||||
*.png
|
||||
*.svg
|
||||
|
|
|
@ -195,7 +195,7 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
<script>
|
||||
import Mousetrap from "mousetrap";
|
||||
import Draggable from "vuedraggable";
|
||||
import {filter as fuzzyFilter} from "fuzzy";
|
||||
|
@ -209,10 +209,7 @@ import isIgnoredKeybind from "../js/helpers/isIgnoredKeybind";
|
|||
import distance from "../js/helpers/distance";
|
||||
import eventbus from "../js/eventbus";
|
||||
|
||||
import NetworkModel from "../../src/models/network";
|
||||
import ChannelMode from "../../src/models/chan";
|
||||
|
||||
export default {
|
||||
export default Vue.extend({
|
||||
name: "NetworkList",
|
||||
components: {
|
||||
JoinChannel,
|
||||
|
@ -484,5 +481,5 @@ export default {
|
|||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -274,10 +274,6 @@ function fuzzyGrep<T>(term: string, array: Array<T>) {
|
|||
}
|
||||
|
||||
function rawNicks() {
|
||||
if (!store.state.activeChannel) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (store.state.activeChannel.channel.users.length > 0) {
|
||||
const users = store.state.activeChannel.channel.users.slice();
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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<Record<string, unknown>>((acc, path) => {
|
||||
const command = path.substring(2, path.length - 3);
|
||||
|
||||
if (command === "index") {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) ||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<ParsedStyle, "text">[], textSegment) => {
|
||||
const result = existingEntries.reduce<Part[]>((acc, textSegment) => {
|
||||
if (textSegment.start > position) {
|
||||
acc.push({
|
||||
start: position,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<MergedPart[]>((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));
|
||||
|
||||
|
|
|
@ -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<any, string>, 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 &&
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export default (stringUri) => {
|
||||
export default (stringUri: string) => {
|
||||
const data = {};
|
||||
|
||||
try {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export default (count) => {
|
||||
export default (count: number) => {
|
||||
if (count < 1000) {
|
||||
return count.toString();
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
8
client/index.ts → client/js/types.d.ts
vendored
8
client/index.ts → client/js/types.d.ts
vendored
|
@ -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;
|
||||
};
|
|
@ -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<Data, Methods, Computed, Props>({
|
||||
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",
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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<SearchResponse> {
|
||||
if (this.messageProvider === undefined) {
|
||||
return Promise.resolve([]);
|
||||
return Promise.resolve({
|
||||
results: [],
|
||||
target: "",
|
||||
networkUuid: "",
|
||||
offset: 0,
|
||||
searchTerm: query?.searchTerm,
|
||||
});
|
||||
}
|
||||
|
||||
return this.messageProvider.search(query);
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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<Message[]>;
|
||||
}
|
||||
|
||||
search(query: {searchTerm: string; networkUuid: string; channelName: string; offset: string}) {
|
||||
search(query: SearchQuery): Promise<SearchResponse> {
|
||||
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) {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -22,6 +22,22 @@ interface MessageStorage {
|
|||
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 {
|
||||
database: Database;
|
||||
search: SearchFunction;
|
||||
}
|
|
@ -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[] = [];
|
||||
|
|
|
@ -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<Chan>) {
|
||||
return this.client.createChannel(attributes);
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
1
src/types/index.d.ts
vendored
1
src/types/index.d.ts
vendored
|
@ -1,2 +1 @@
|
|||
import "./modules";
|
||||
import "./plugins";
|
||||
|
|
1
src/types/plugins/index.d.ts
vendored
1
src/types/plugins/index.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
import "./messageStorage";
|
|
@ -5,4 +5,4 @@ reporter: dot
|
|||
interactive: false
|
||||
spec: "test/**/*.ts"
|
||||
ignore: "test/client/**"
|
||||
require: "test/fixtures/env"
|
||||
require: "test/fixtures/env.ts"
|
||||
|
|
6
test/fixtures/.thelounge/config.js
vendored
6
test/fixtures/.thelounge/config.js
vendored
|
@ -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;
|
||||
|
|
2
test/fixtures/env.ts
vendored
2
test/fixtures/env.ts
vendored
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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/,
|
||||
|
|
Loading…
Reference in a new issue