progress before vue 3

This commit is contained in:
Max Leiter 2022-05-21 17:27:51 -07:00
parent 4c98b81e35
commit aace97056b
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
49 changed files with 183 additions and 303 deletions

View file

@ -84,6 +84,8 @@ const vueRules = defineConfig({
"vue/no-v-html": "off", "vue/no-v-html": "off",
"vue/require-default-prop": "off", "vue/require-default-prop": "off",
"vue/v-slot-style": ["error", "longform"], "vue/v-slot-style": ["error", "longform"],
// Should be fixable in Vue 3 / when components use Vue.extend()
"@typescript-eslint/unbound-method": "off",
}, },
}).rules; }).rules;
@ -113,6 +115,9 @@ const tsRulesTemp = defineConfig({
module.exports = defineConfig({ module.exports = defineConfig({
root: true, root: true,
parserOptions: {
ecmaVersion: 2022,
},
overrides: [ overrides: [
{ {
files: [ files: [
@ -132,7 +137,11 @@ module.exports = defineConfig({
"plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier", "prettier",
], ],
rules: {...baseRules, ...tsRules, ...tsRulesTemp}, rules: {
...baseRules,
...tsRules,
...tsRulesTemp,
},
}, },
// TODO: verify // TODO: verify
{ {

View file

@ -31,15 +31,6 @@ import ConfirmDialog from "./ConfirmDialog.vue";
import Mentions from "./Mentions.vue"; import Mentions from "./Mentions.vue";
import VueApp from "vue"; import VueApp from "vue";
// This stops Vue from complaining about adding objects to the component context
declare module "vue/types/vue" {
interface Vue {
debouncedResize: () => void;
// TODO; type as Timeout
dayChangeTimeout: any;
}
}
export default VueApp.extend({ export default VueApp.extend({
name: "App", name: "App",
components: { components: {

View file

@ -27,30 +27,32 @@
</ChannelWrapper> </ChannelWrapper>
</template> </template>
<script> <script lang="ts">
import Vue, {PropType} from "vue";
import roundBadgeNumber from "../js/helpers/roundBadgeNumber"; import roundBadgeNumber from "../js/helpers/roundBadgeNumber";
import {ClientChan, ClientNetwork} from "../js/types";
import ChannelWrapper from "./ChannelWrapper.vue"; import ChannelWrapper from "./ChannelWrapper.vue";
export default { export default Vue.extend({
name: "Channel", name: "Channel",
components: { components: {
ChannelWrapper, ChannelWrapper,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
channel: Object, channel: Object as PropType<ClientChan>,
active: Boolean, active: Boolean,
isFiltering: Boolean, isFiltering: Boolean,
}, },
computed: { computed: {
unreadCount() { unreadCount(): string {
return roundBadgeNumber(this.channel.unread); return roundBadgeNumber(this.channel.unread);
}, },
}, },
methods: { methods: {
close() { close(): void {
this.$root.closeChannel(this.channel); this.$root.closeChannel(this.channel);
}, },
}, },
}; });
</script> </script>

View file

@ -32,15 +32,17 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import Vue, {PropType} from "vue";
import eventbus from "../js/eventbus"; import eventbus from "../js/eventbus";
import isChannelCollapsed from "../js/helpers/isChannelCollapsed"; import isChannelCollapsed from "../js/helpers/isChannelCollapsed";
import {ClientNetwork, ClientChan} from "../js/types";
export default { export default Vue.extend({
name: "ChannelWrapper", name: "ChannelWrapper",
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
channel: Object, channel: Object as PropType<ClientChan>,
active: Boolean, active: Boolean,
isFiltering: Boolean, isFiltering: Boolean,
}, },
@ -53,7 +55,7 @@ export default {
}, },
}, },
methods: { methods: {
getAriaLabel() { getAriaLabel(): string {
const extra = []; const extra = [];
const type = this.channel.type; const type = this.channel.type;
@ -75,14 +77,14 @@ export default {
return `${type}: ${this.channel.name} ${extra.length ? `(${extra.join(", ")})` : ""}`; return `${type}: ${this.channel.name} ${extra.length ? `(${extra.join(", ")})` : ""}`;
}, },
click() { click(): void {
if (this.isFiltering) { if (this.isFiltering) {
return; return;
} }
this.$root.switchToChannel(this.channel); this.$root.switchToChannel(this.channel);
}, },
openContextMenu(event) { openContextMenu(event): void {
eventbus.emit("contextmenu:channel", { eventbus.emit("contextmenu:channel", {
event: event, event: event,
channel: this.channel, channel: this.channel,
@ -90,5 +92,5 @@ export default {
}); });
}, },
}, },
}; });
</script> </script>

View file

@ -145,8 +145,8 @@ export default {
MessageSearchForm, MessageSearchForm,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
channel: Object, channel: Object as PropType<ClientChan>,
focused: String, focused: String,
}, },
computed: { computed: {

View file

@ -91,8 +91,8 @@ let autocompletionRef = null;
export default { export default {
name: "ChatInput", name: "ChatInput",
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
channel: Object, channel: Object as PropType<ClientChan>,
}, },
watch: { watch: {
"channel.id"() { "channel.id"() {

View file

@ -74,7 +74,7 @@ export default {
Username, Username,
}, },
props: { props: {
channel: Object, channel: Object as PropType<ClientChan>,
}, },
data() { data() {
return { return {

View file

@ -48,8 +48,8 @@ export default {
}, },
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
channel: Object, channel: Object as PropType<ClientChan>,
}, },
data() { data() {
return { return {

View file

@ -138,7 +138,7 @@ export default {
props: { props: {
link: Object, link: Object,
keepScrollPosition: Function, keepScrollPosition: Function,
channel: Object, channel: Object as PropType<ClientChan>,
}, },
data() { data() {
return { return {

View file

@ -113,8 +113,8 @@ export default {
components: MessageTypes, components: MessageTypes,
props: { props: {
message: Object, message: Object,
channel: Object, channel: Object as PropType<ClientChan>,
network: Object, network: Object as PropType<ClientNetwork>,
keepScrollPosition: Function, keepScrollPosition: Function,
isPreviousSource: Boolean, isPreviousSource: Boolean,
focused: Boolean, focused: Boolean,

View file

@ -27,7 +27,7 @@ export default {
Message, Message,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
messages: Array, messages: Array,
keepScrollPosition: Function, keepScrollPosition: Function,
focused: Boolean, focused: Boolean,

View file

@ -84,8 +84,8 @@ form.message-search.opened .input-wrapper {
export default { export default {
name: "MessageSearchForm", name: "MessageSearchForm",
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
channel: Object, channel: Object as PropType<ClientChan>,
}, },
data() { data() {
return { return {

View file

@ -20,7 +20,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -19,7 +19,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -23,7 +23,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -16,7 +16,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -17,7 +17,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -13,7 +13,7 @@ export default {
ParsedMessage, ParsedMessage,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
computed: { computed: {

View file

@ -19,7 +19,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -23,7 +23,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -20,7 +20,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -17,7 +17,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -8,7 +8,7 @@
export default { export default {
name: "MessageChannelMode", name: "MessageChannelMode",
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -8,7 +8,7 @@
export default { export default {
name: "MessageChannelMode", name: "MessageChannelMode",
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -13,7 +13,7 @@ export default {
ParsedMessage, ParsedMessage,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
computed: { computed: {

View file

@ -15,7 +15,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -20,7 +20,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -20,7 +20,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -6,7 +6,7 @@
export default { export default {
name: "MessageTypeRaw", name: "MessageTypeRaw",
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -21,7 +21,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
}; };

View file

@ -16,7 +16,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
computed: { computed: {

View file

@ -123,7 +123,7 @@ export default {
Username, Username,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
message: Object, message: Object,
}, },
methods: { methods: {

View file

@ -209,7 +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";
export default Vue.extend({ export default {
name: "NetworkList", name: "NetworkList",
components: { components: {
JoinChannel, JoinChannel,
@ -481,5 +481,5 @@ export default Vue.extend({
}); });
}, },
}, },
}); };
</script> </script>

View file

@ -56,7 +56,7 @@ export default {
ChannelWrapper, ChannelWrapper,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
isJoinChannelShown: Boolean, isJoinChannelShown: Boolean,
active: Boolean, active: Boolean,
isFiltering: Boolean, isFiltering: Boolean,

View file

@ -7,7 +7,7 @@ export default {
props: { props: {
text: String, text: String,
message: Object, message: Object,
network: Object, network: Object as PropType<ClientNetwork>,
}, },
render(createElement, context) { render(createElement, context) {
return parse( return parse(

View file

@ -27,8 +27,8 @@ export default {
ParsedMessage, ParsedMessage,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
channel: Object, channel: Object as PropType<ClientChan>,
}, },
methods: { methods: {
localetime(date) { localetime(date) {

View file

@ -27,8 +27,8 @@ export default {
ParsedMessage, ParsedMessage,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
channel: Object, channel: Object as PropType<ClientChan>,
}, },
}; };
</script> </script>

View file

@ -25,8 +25,8 @@ export default {
ParsedMessage, ParsedMessage,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
channel: Object, channel: Object as PropType<ClientChan>,
}, },
methods: { methods: {
localetime(date) { localetime(date) {

View file

@ -29,8 +29,8 @@ export default {
ParsedMessage, ParsedMessage,
}, },
props: { props: {
network: Object, network: Object as PropType<ClientNetwork>,
channel: Object, channel: Object as PropType<ClientChan>,
}, },
methods: { methods: {
localetime(date) { localetime(date) {

View file

@ -20,8 +20,8 @@ export default {
user: Object, user: Object,
active: Boolean, active: Boolean,
onHover: Function, onHover: Function,
channel: Object, channel: Object as PropType<ClientChan>,
network: Object, network: Object as PropType<ClientNetwork>,
}, },
computed: { computed: {
mode() { mode() {

View file

@ -4,6 +4,7 @@ import storage from "../localStorage";
import {router, switchToChannel, navigate} from "../router"; import {router, switchToChannel, navigate} from "../router";
import store from "../store"; import store from "../store";
import parseIrcUri from "../helpers/parseIrcUri"; import parseIrcUri from "../helpers/parseIrcUri";
import {ClientChan, ClientNetwork, InitClientChan} from "../types";
socket.on("init", function (data) { socket.on("init", function (data) {
store.commit("networks", mergeNetworkData(data.networks)); store.commit("networks", mergeNetworkData(data.networks));
@ -47,8 +48,9 @@ socket.on("init", function (data) {
} }
}); });
function mergeNetworkData(newNetworks) { function mergeNetworkData(newNetworks: ClientNetwork[]) {
const collapsedNetworks = new Set(JSON.parse(storage.get("thelounge.networks.collapsed"))); const stored = storage.get("thelounge.networks.collapsed");
const collapsedNetworks = stored ? new Set(JSON.parse(stored)) : new Set();
for (let n = 0; n < newNetworks.length; n++) { for (let n = 0; n < newNetworks.length; n++) {
const network = newNetworks[n]; const network = newNetworks[n];
@ -74,7 +76,7 @@ function mergeNetworkData(newNetworks) {
if (key === "channels") { if (key === "channels") {
currentNetwork.channels = mergeChannelData( currentNetwork.channels = mergeChannelData(
currentNetwork.channels, currentNetwork.channels,
network.channels network.channels as InitClientChan[]
); );
} else { } else {
currentNetwork[key] = network[key]; currentNetwork[key] = network[key];
@ -87,7 +89,7 @@ function mergeNetworkData(newNetworks) {
return newNetworks; return newNetworks;
} }
function mergeChannelData(oldChannels, newChannels) { function mergeChannelData(oldChannels: InitClientChan[], newChannels: InitClientChan[]) {
for (let c = 0; c < newChannels.length; c++) { for (let c = 0; c < newChannels.length; c++) {
const channel = newChannels[c]; const channel = newChannels[c];
const currentChannel = oldChannels.find((chan) => chan.id === channel.id); const currentChannel = oldChannels.find((chan) => chan.id === channel.id);
@ -131,7 +133,7 @@ function mergeChannelData(oldChannels, newChannels) {
// on the client, and decide whether theres more messages to load from server // on the client, and decide whether theres more messages to load from server
if (key === "totalMessages") { if (key === "totalMessages") {
currentChannel.moreHistoryAvailable = currentChannel.moreHistoryAvailable =
channel.totalMessages > currentChannel.messages.length; channel.totalMessages! > currentChannel.messages.length;
continue; continue;
} }
@ -167,10 +169,12 @@ function handleQueryParams() {
if (params.has("uri")) { if (params.has("uri")) {
// Set default connection settings from IRC protocol links // Set default connection settings from IRC protocol links
const uri = params.get("uri"); const uri = params.get("uri");
const queryParams = parseIrcUri(uri); const queryParams = parseIrcUri(uri as string);
cleanParams(); cleanParams();
router.push({name: "Connect", query: queryParams}); router.push({name: "Connect", query: queryParams}).catch(() => {
// Ignore errors
});
return true; return true;
} else if (document.body.classList.contains("public") && document.location.search) { } else if (document.body.classList.contains("public") && document.location.search) {
@ -178,7 +182,9 @@ function handleQueryParams() {
const queryParams = Object.fromEntries(params.entries()); const queryParams = Object.fromEntries(params.entries());
cleanParams(); cleanParams();
router.push({name: "Connect", query: queryParams}); router.push({name: "Connect", query: queryParams}).catch(() => {
// Ignore errors
});
return true; return true;
} }

View file

@ -1,8 +1,8 @@
import Vue from "vue"; import Vue from "vue";
import Vuex from "vuex"; import Vuex, {GetterTree, Store} 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"; import type {ClientChan, ClientNetwork, InitClientChan} from "./types";
const appName = document.title; const appName = document.title;
@ -20,7 +20,7 @@ function detectDesktopNotificationState() {
return "blocked"; return "blocked";
} }
export type State = { export interface State {
appLoaded: boolean; appLoaded: boolean;
activeChannel: { activeChannel: {
network: ClientNetwork; network: ClientNetwork;
@ -54,13 +54,15 @@ export type State = {
} | null; } | null;
messageSearchInProgress: boolean; messageSearchInProgress: boolean;
searchEnabled: boolean; searchEnabled: boolean;
}; }
export type SettingsState = {}; const store = new Store<State>({
const store = new Vuex.Store<Omit<State, "settings">>({
state: { state: {
appLoaded: false, appLoaded: false,
activeChannel: null, activeChannel: {
network: {} as ClientNetwork,
channel: {} as ClientChan,
},
currentUserVisibleError: null, currentUserVisibleError: null,
desktopNotificationState: detectDesktopNotificationState(), desktopNotificationState: detectDesktopNotificationState(),
isAutoCompleting: false, isAutoCompleting: false,
@ -162,14 +164,16 @@ const store = new Vuex.Store<Omit<State, "settings">>({
state.messageSearchResults = value; state.messageSearchResults = value;
}, },
addMessageSearchResults(state, value) { addMessageSearchResults(state, value) {
if (state.messageSearchResults!.results) { // Append the search results and add networks and channels to new messages
// Append the search results and add networks and channels to new messages if (!state.messageSearchResults) {
value.results = [...state.messageSearchResults!.results, ...value.results]; state.messageSearchResults = {results: []};
} else {
value.results = value.results;
} }
state.messageSearchResults = value; const results = [...state.messageSearchResults.results, ...value.results];
state.messageSearchResults = {
results,
};
}, },
}, },
actions: { actions: {
@ -179,11 +183,11 @@ const store = new Vuex.Store<Omit<State, "settings">>({
}, },
}, },
getters: { getters: {
findChannelOnCurrentNetwork: (state) => (name) => { findChannelOnCurrentNetwork: (state) => (name: string) => {
name = name.toLowerCase(); name = name.toLowerCase();
return state.activeChannel?.network.channels.find((c) => c.name.toLowerCase() === name); return state.activeChannel?.network.channels.find((c) => c.name.toLowerCase() === name);
}, },
findChannelOnNetwork: (state) => (networkUuid, channelName) => { findChannelOnNetwork: (state) => (networkUuid: string, channelName: string) => {
for (const network of state.networks) { for (const network of state.networks) {
if (network.uuid !== networkUuid) { if (network.uuid !== networkUuid) {
continue; continue;
@ -198,7 +202,7 @@ const store = new Vuex.Store<Omit<State, "settings">>({
return null; return null;
}, },
findChannel: (state) => (id) => { findChannel: (state) => (id: number) => {
for (const network of state.networks) { for (const network of state.networks) {
for (const channel of network.channels) { for (const channel of network.channels) {
if (channel.id === id) { if (channel.id === id) {
@ -209,7 +213,7 @@ const store = new Vuex.Store<Omit<State, "settings">>({
return null; return null;
}, },
findNetwork: (state) => (uuid) => { findNetwork: (state) => (uuid: string) => {
for (const network of state.networks) { for (const network of state.networks) {
if (network.uuid === uuid) { if (network.uuid === uuid) {
return network; return network;
@ -233,14 +237,16 @@ const store = new Vuex.Store<Omit<State, "settings">>({
return highlightCount; return highlightCount;
}, },
// TODO: type
title(state, getters) { title(state, getters) {
const alertEventCount = getters.highlightCount ? `(${getters.highlightCount}) ` : ""; // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
const alertEventCount = getters?.highlightCount ? `(${getters.highlightCount}) ` : "";
const channelname = state.activeChannel ? `${state.activeChannel.channel.name}` : ""; const channelname = state.activeChannel ? `${state.activeChannel.channel.name}` : "";
return alertEventCount + channelname + appName; return alertEventCount + channelname + appName;
}, },
initChannel: () => (channel) => { initChannel: () => (channel: InitClientChan) => {
// TODO: This should be a mutation // TODO: This should be a mutation
channel.pendingMessage = ""; channel.pendingMessage = "";
channel.inputHistoryPosition = 0; channel.inputHistoryPosition = 0;
@ -250,20 +256,20 @@ const store = new Vuex.Store<Omit<State, "settings">>({
.filter((m) => m.self && m.text && m.type === "message") .filter((m) => m.self && m.text && m.type === "message")
.map((m) => m.text) .map((m) => m.text)
.reverse() .reverse()
.slice(null, 99) .slice(0, 99)
); );
channel.historyLoading = false; channel.historyLoading = false;
channel.scrolledToBottom = true; channel.scrolledToBottom = true;
channel.editTopic = false; channel.editTopic = false;
channel.moreHistoryAvailable = channel.totalMessages > channel.messages.length; channel.moreHistoryAvailable = channel.totalMessages! > channel.messages.length;
delete channel.totalMessages; delete channel.totalMessages;
if (channel.type === "channel") { if (channel.type === "channel") {
channel.usersOutdated = true; channel.usersOutdated = true;
} }
return channel; return channel as ClientChan;
}, },
}, },
}); });

15
client/js/types.d.ts vendored
View file

@ -5,6 +5,7 @@ declare module "*.vue" {
import Vue from "vue"; import Vue from "vue";
export default Vue; export default Vue;
} }
interface LoungeWindow extends Window { interface LoungeWindow extends Window {
g_TheLoungeRemoveLoading?: () => void; g_TheLoungeRemoveLoading?: () => void;
} }
@ -12,8 +13,22 @@ interface LoungeWindow extends Window {
type ClientChan = Chan & { type ClientChan = Chan & {
moreHistoryAvailable: boolean; moreHistoryAvailable: boolean;
editTopic: boolean; editTopic: boolean;
// these are added in store/initChannel
pendingMessage: string;
inputHistoryPosition: number;
inputHistory: string[];
historyLoading: boolean;
scrolledToBottom: boolean;
usersOutdated: boolean;
};
type InitClientChan = ClientChan & {
// total messages is deleted after its use when init event is sent/handled
totalMessages?: number;
}; };
type ClientNetwork = Network & { type ClientNetwork = Network & {
isJoinChannelShown: boolean; isJoinChannelShown: boolean;
isCollapsed: boolean;
}; };

View file

@ -2,7 +2,7 @@ import constants from "./constants";
import "../css/style.css"; import "../css/style.css";
import Vue from "vue"; import Vue from "vue";
import store from "./store"; import store, {State} from "./store";
import App from "../components/App.vue"; import App from "../components/App.vue";
import storage from "./localStorage"; import storage from "./localStorage";
import {router, navigate} from "./router"; import {router, navigate} from "./router";
@ -13,20 +13,26 @@ import "./socket-events";
import "./webpush"; import "./webpush";
import "./keybinds"; import "./keybinds";
import {ClientChan} from "./types"; import {ClientChan} from "./types";
import {Store} from "vuex";
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 || "";
type Data = {}; declare module "vue/types/vue" {
export type Methods = { interface Vue {
switchToChannel: (channel: ClientChan) => void; debouncedResize: () => void;
closeChannel: (channel: ClientChan) => void; // TODO; type as Timeout
}; dayChangeTimeout: any;
type Computed = {};
type Props = {};
new Vue<Data, Methods, Computed, Props>({ switchToChannel: (channel: ClientChan) => void;
closeChannel: (channel: ClientChan) => void;
$store: Store<State>;
}
}
new Vue({
el: "#viewport", el: "#viewport",
router, router,
mounted() { mounted() {

View file

@ -111,6 +111,7 @@
"@typescript-eslint/parser": "5.22.0", "@typescript-eslint/parser": "5.22.0",
"@vue/babel-helper-vue-jsx-merge-props": "1.2.1", "@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
"@vue/babel-preset-jsx": "1.2.4", "@vue/babel-preset-jsx": "1.2.4",
"@vue/runtime-core": "3.2.35",
"@vue/runtime-dom": "3.2.33", "@vue/runtime-dom": "3.2.33",
"@vue/server-test-utils": "1.3.0", "@vue/server-test-utils": "1.3.0",
"@vue/test-utils": "1.3.0", "@vue/test-utils": "1.3.0",

View file

@ -4,11 +4,22 @@ import express from "express";
import log from "../log"; import log from "../log";
import webpack from "webpack";
import config from "../../webpack.config";
export default (app: express.Application) => { export default (app: express.Application) => {
log.debug("Starting server in development mode"); log.debug("Starting server in development mode");
const webpack = require("webpack"); const webpackConfig = config(undefined, {mode: "production"});
const webpackConfig = require("../../webpack.config.js")(undefined, { mode: "production" });
if (
!webpackConfig ||
!webpackConfig.plugins?.length ||
!webpackConfig.entry ||
!webpackConfig.entry["js/bundle.js"]
) {
throw new Error("No valid production webpack config found");
}
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.entry["js/bundle.js"].push( webpackConfig.entry["js/bundle.js"].push(

View file

@ -15,7 +15,7 @@ export default {
props: { props: {
text: String, text: String,
message: Object, message: Object,
network: Object, network: Object as PropType<ClientNetwork>,
}, },
}; };
</script> </script>

View file

@ -1,189 +0,0 @@
"use strict";
const webpack = require("webpack");
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
const Helper = require("./src/helper.js");
const babelConfig = require("./babel.config.cjs");
const isProduction = process.env.NODE_ENV === "production";
const config = {
mode: isProduction ? "production" : "development",
entry: {
"js/bundle.js": [path.resolve(__dirname, "client/js/vue.js")],
},
devtool: "source-map",
output: {
clean: true, // Clean the output directory before emit.
path: path.resolve(__dirname, "public"),
filename: "[name]",
publicPath: "/",
},
performance: {
hints: false,
},
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: "vue-loader",
options: {
compilerOptions: {
preserveWhitespace: false,
},
},
},
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
esModule: false,
},
},
{
loader: "css-loader",
options: {
url: false,
importLoaders: 1,
sourceMap: true,
},
},
{
loader: "postcss-loader",
options: {
sourceMap: true,
},
},
],
},
{
test: /\.js$/,
include: [path.resolve(__dirname, "client")],
use: {
loader: "babel-loader",
options: babelConfig,
},
},
],
},
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: "js/bundle.vendor.js",
chunks: "all",
},
},
},
},
externals: {
json3: "JSON", // socket.io uses json3.js, but we do not target any browsers that need it
},
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: "css/style.css",
}),
new CopyPlugin({
patterns: [
{
from: "./node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff*",
to: "fonts/[name][ext]",
},
{
from: "./client/js/loading-error-handlers.js",
to: "js/[name][ext]",
},
{
from: "./client/*",
to: "[name][ext]",
globOptions: {
ignore: ["**/index.html.tpl", "**/service-worker.js"],
},
},
{
from: "./client/service-worker.js",
to: "[name][ext]",
transform(content) {
return content
.toString()
.replace(
"__HASH__",
isProduction ? Helper.getVersionCacheBust() : "dev"
);
},
},
{
from: "./client/audio/*",
to: "audio/[name][ext]",
},
{
from: "./client/img/*",
to: "img/[name][ext]",
},
{
from: "./client/themes/*",
to: "themes/[name][ext]",
},
],
}),
// socket.io uses debug, we don't need it
new webpack.NormalModuleReplacementPlugin(
/debug/,
path.resolve(__dirname, "scripts/noop.js")
),
],
};
module.exports = (env, argv) => {
if (argv.mode === "development") {
config.target = "node";
config.devtool = "eval";
config.stats = "errors-only";
config.output.path = path.resolve(__dirname, "test/public");
config.entry = {
"testclient.js": [path.resolve(__dirname, "test/client/index.js")],
};
// Add the istanbul plugin to babel-loader options
for (const rule of config.module.rules) {
if (rule.use.loader === "babel-loader") {
rule.use.options.plugins = ["istanbul"];
}
}
// `optimization.splitChunks` is incompatible with a `target` of `node`. See:
// - https://github.com/zinserjan/mocha-webpack/issues/84
// - https://github.com/webpack/webpack/issues/6727#issuecomment-372589122
config.optimization.splitChunks = false;
// Disable plugins like copy files, it is not required
config.plugins = [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: "css/style.css",
}),
// Client tests that require Vue may end up requireing socket.io
new webpack.NormalModuleReplacementPlugin(
/js(\/|\\)socket\.js/,
path.resolve(__dirname, "scripts/noop.js")
),
// "Fixes" Critical dependency: the request of a dependency is an expression
new webpack.ContextReplacementPlugin(/vue-server-renderer$/),
];
}
if (argv.mode === "production") {
// ...
}
return config;
};

View file

@ -1874,6 +1874,13 @@
dependencies: dependencies:
"@vue/shared" "3.2.33" "@vue/shared" "3.2.33"
"@vue/reactivity@3.2.35":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.35.tgz#c66af289f3beda6aba63c264db9c6acd607d1c73"
integrity sha512-6j9N9R1SwHVcJas4YqAzwdRS/cgmj3Z9aUert5Mv1jk5B9H9ivN/zot/fgMUbseWXigkkmX60OsfRbz49o8kCw==
dependencies:
"@vue/shared" "3.2.35"
"@vue/runtime-core@3.2.33": "@vue/runtime-core@3.2.33":
version "3.2.33" version "3.2.33"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.33.tgz#2df8907c85c37c3419fbd1bdf1a2df097fa40df2" resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.33.tgz#2df8907c85c37c3419fbd1bdf1a2df097fa40df2"
@ -1882,6 +1889,14 @@
"@vue/reactivity" "3.2.33" "@vue/reactivity" "3.2.33"
"@vue/shared" "3.2.33" "@vue/shared" "3.2.33"
"@vue/runtime-core@3.2.35":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.35.tgz#a87bd5214ff31f9dc6542f5c498d4f3543c6ea8f"
integrity sha512-P8AeGPRGyIiYdOdvLc/7KR8VSdbUGG8Jxdx6Xlj5okEjyV9IYxeHRIQIoye85K0lZXBH4zuh1syD1mX+oZ0KqQ==
dependencies:
"@vue/reactivity" "3.2.35"
"@vue/shared" "3.2.35"
"@vue/runtime-dom@3.2.33": "@vue/runtime-dom@3.2.33":
version "3.2.33" version "3.2.33"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.33.tgz#123b8969247029ea0d9c1983676d4706a962d848" resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.33.tgz#123b8969247029ea0d9c1983676d4706a962d848"
@ -1904,6 +1919,11 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.33.tgz#69a8c99ceb37c1b031d5cc4aec2ff1dc77e1161e" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.33.tgz#69a8c99ceb37c1b031d5cc4aec2ff1dc77e1161e"
integrity sha512-UBc1Pg1T3yZ97vsA2ueER0F6GbJebLHYlEi4ou1H5YL4KWvMOOWwpYo9/QpWq93wxKG6Wo13IY74Hcn/f7c7Bg== integrity sha512-UBc1Pg1T3yZ97vsA2ueER0F6GbJebLHYlEi4ou1H5YL4KWvMOOWwpYo9/QpWq93wxKG6Wo13IY74Hcn/f7c7Bg==
"@vue/shared@3.2.35":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.35.tgz#fb60530fa009dc21473386a7639eed833877cb0f"
integrity sha512-/sxDqMcy0MsfQ3LQixKYDxIinDYNy1dXTsF2Am0pv0toImWabymFQ8cFmPJnPt+gh5ElKwwn7KzQcDbLHar60A==
"@vue/test-utils@1.3.0": "@vue/test-utils@1.3.0":
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.3.0.tgz#d563decdcd9c68a7bca151d4179a2bfd6d5c3e15" resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.3.0.tgz#d563decdcd9c68a7bca151d4179a2bfd6d5c3e15"