diff --git a/.eslintignore b/.eslintignore
index 1f2ae53f..219bb10b 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,2 +1,3 @@
public/
coverage/
+dist/
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index ca48b1ad..4b567364 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -1,14 +1,18 @@
-module.exports = {
- root: true,
+// @ts-check
+const {defineConfig} = require("eslint-define-config");
+
+const projects = defineConfig({
parserOptions: {
- ecmaVersion: 2022,
- },
- env: {
- es6: true,
- browser: true,
- mocha: true,
- node: true,
+ project: [
+ "./tsconfig.json",
+ "./client/tsconfig.json",
+ "./server/tsconfig.json",
+ "./test/tsconfig.json",
+ ],
},
+}).parserOptions.project;
+
+const baseRules = defineConfig({
rules: {
"block-scoped-var": "error",
curly: ["error", "all"],
@@ -23,7 +27,6 @@ module.exports = {
"no-else-return": "error",
"no-implicit-globals": "error",
"no-restricted-globals": ["error", "event", "fdescribe"],
- "no-shadow": "error",
"no-template-curly-in-string": "error",
"no-unsafe-negation": "error",
"no-useless-computed-key": "error",
@@ -62,18 +65,127 @@ module.exports = {
"spaced-comment": ["error", "always"],
strict: "off",
yoda: "error",
+ },
+}).rules;
+
+const vueRules = defineConfig({
+ rules: {
+ "import/no-default-export": 0,
+ "import/unambiguous": 0, // vue SFC can miss script tags
+ "@typescript-eslint/prefer-readonly": 0, // can be used in template
"vue/component-tags-order": [
"error",
{
order: ["template", "style", "script"],
},
],
+ "vue/multi-word-component-names": "off",
"vue/no-mutating-props": "off",
"vue/no-v-html": "off",
"vue/require-default-prop": "off",
"vue/v-slot-style": ["error", "longform"],
- "vue/multi-word-component-names": "off",
},
- plugins: ["vue"],
- extends: ["eslint:recommended", "plugin:vue/recommended", "prettier"],
-};
+}).rules;
+
+const tsRules = defineConfig({
+ rules: {
+ // note you must disable the base rule as it can report incorrect errors
+ "no-shadow": "off",
+ "@typescript-eslint/no-shadow": ["error"],
+ },
+}).rules;
+
+const tsRulesTemp = defineConfig({
+ rules: {
+ // TODO: eventually remove these
+ "@typescript-eslint/ban-ts-comment": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/no-this-alias": "off",
+ "@typescript-eslint/no-unnecessary-type-assertion": "off",
+ "@typescript-eslint/no-unsafe-argument": "off",
+ "@typescript-eslint/no-unsafe-assignment": "off",
+ "@typescript-eslint/no-unsafe-call": "off",
+ "@typescript-eslint/no-unsafe-member-access": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ },
+}).rules;
+
+const tsTestRulesTemp = defineConfig({
+ rules: {
+ // TODO: remove these
+ "@typescript-eslint/no-unsafe-return": "off",
+ "@typescript-eslint/no-empty-function": "off",
+ "@typescript-eslint/restrict-plus-operands": "off",
+ },
+}).rules;
+
+module.exports = defineConfig({
+ root: true,
+ parserOptions: {
+ ecmaVersion: 2022,
+ },
+ overrides: [
+ {
+ files: ["**/*.ts", "**/*.vue"],
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: projects,
+ extraFileExtensions: [".vue"],
+ },
+ plugins: ["@typescript-eslint"],
+ extends: [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:@typescript-eslint/recommended-requiring-type-checking",
+ "prettier",
+ ],
+ rules: {
+ ...baseRules,
+ ...tsRules,
+ ...tsRulesTemp,
+ },
+ },
+ {
+ files: ["**/*.vue"],
+ parser: "vue-eslint-parser",
+ parserOptions: {
+ ecmaVersion: 2022,
+ ecmaFeatures: {
+ jsx: true,
+ },
+ parser: "@typescript-eslint/parser",
+ tsconfigRootDir: __dirname,
+ project: projects,
+ },
+ plugins: ["vue"],
+ extends: [
+ "eslint:recommended",
+ "plugin:vue/vue3-recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:@typescript-eslint/recommended-requiring-type-checking",
+ "prettier",
+ ],
+ rules: {...baseRules, ...tsRules, ...tsRulesTemp, ...vueRules},
+ },
+ {
+ files: ["./tests/**/*.ts"],
+ parser: "@typescript-eslint/parser",
+ rules: {
+ ...baseRules,
+ ...tsRules,
+ ...tsRulesTemp,
+ ...tsTestRulesTemp,
+ },
+ },
+ ],
+ env: {
+ es6: true,
+ browser: true,
+ mocha: true,
+ node: true,
+ },
+ extends: ["eslint:recommended", "prettier"],
+ rules: baseRules,
+});
diff --git a/.gitignore b/.gitignore
index 348de645..2f36fb37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ package-lock.json
coverage/
public/
+dist/
diff --git a/.npmignore b/.npmignore
index e902f20a..8df5322f 100644
--- a/.npmignore
+++ b/.npmignore
@@ -9,9 +9,9 @@
# Ignore client folder as it's being built into public/ folder
# except for the specified files which are used by the server
client/**
-!client/js/constants.js
-!client/js/helpers/ircmessageparser/findLinks.js
-!client/js/helpers/ircmessageparser/cleanIrcMessage.js
+!client/js/constants.ts
+!client/js/helpers/ircmessageparser/findLinks.ts
+!client/js/helpers/ircmessageparser/cleanIrcMessage.ts
!client/index.html.tpl
public/js/bundle.vendor.js.map
@@ -22,3 +22,4 @@ appveyor.yml
webpack.config*.js
postcss.config.js
renovate.json
+
diff --git a/.prettierignore b/.prettierignore
index aa316e17..5dabdfae 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,9 +1,10 @@
coverage/
public/
+dist/
test/fixtures/.thelounge/logs/
test/fixtures/.thelounge/certificates/
test/fixtures/.thelounge/storage/
-
+test/fixtures/.thelounge/sts-policies.json
*.log
*.png
*.svg
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index d401e0e3..6e4bf808 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -3,7 +3,8 @@
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
- "octref.vetur"
+ "Vue.volar",
+ "Vue.vscode-typescript-vue-plugin"
],
"unwantedRecommendations": []
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index d80731df..a7e07398 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,10 +1,10 @@
{
"editor.formatOnSave": true,
- "vetur.format.enable": false,
"prettier.useEditorConfig": true,
"prettier.requireConfig": true,
"prettier.disableLanguages": [],
- "prettier.packageManager": "yarn",
"eslint.packageManager": "yarn",
- "eslint.codeActionsOnSave.mode": "all"
+ "eslint.codeActionsOnSave.mode": "all",
+ "[typescript]": {"editor.defaultFormatter": "esbenp.prettier-vscode"},
+ "[vue]": {"editor.defaultFormatter": "esbenp.prettier-vscode"}
}
diff --git a/README.md b/README.md
index 8fb2fb62..31665ab7 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ The Lounge is the official and community-managed fork of [Shout](https://github.
## Installation and usage
The Lounge requires latest [Node.js](https://nodejs.org/) LTS version or more recent.
-[Yarn package manager](https://yarnpkg.com/) is also recommended.
+The [Yarn package manager](https://yarnpkg.com/) is also recommended.
If you want to install with npm, `--unsafe-perm` is required for a correct install.
### Running stable releases
@@ -85,5 +85,8 @@ Before submitting any change, make sure to:
- Read the [Contributing instructions](https://github.com/thelounge/thelounge/blob/master/.github/CONTRIBUTING.md#contributing)
- Run `yarn test` to execute linters and the test suite
- Run `yarn format:prettier` if linting fails
-- Run `yarn build` if you change or add anything in `client/js` or `client/components`
+- Run `yarn build:client` if you change or add anything in `client/js` or `client/components`
+ - The built files will be output to `public/` by webpack
+- Run `yarn build:server` if you change anything in `server/`
+ - The built files will be output to `dist/` by tsc
- `yarn dev` can be used to start The Lounge with hot module reloading
diff --git a/babel.config.cjs b/babel.config.cjs
index 059d55ab..8554cbc5 100644
--- a/babel.config.cjs
+++ b/babel.config.cjs
@@ -1,3 +1,4 @@
module.exports = {
- presets: [["@babel/env"]],
+ presets: [["@babel/preset-env", {bugfixes: true}], "babel-preset-typescript-vue3"],
+ plugins: ["@babel/plugin-transform-runtime"],
};
diff --git a/client/components/App.vue b/client/components/App.vue
index e02a10e7..61bbb3b7 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -1,13 +1,13 @@
-
+
-
+
@@ -16,10 +16,10 @@
-
diff --git a/client/components/Channel.vue b/client/components/Channel.vue
index 72551f9b..c21e55d2 100644
--- a/client/components/Channel.vue
+++ b/client/components/Channel.vue
@@ -1,4 +1,5 @@
+
{{ channel.name }}
-
diff --git a/client/components/ChannelWrapper.vue b/client/components/ChannelWrapper.vue
index 572881bc..380b52af 100644
--- a/client/components/ChannelWrapper.vue
+++ b/client/components/ChannelWrapper.vue
@@ -23,72 +23,90 @@
:data-type="channel.type"
:aria-controls="'#chan-' + channel.id"
:aria-selected="active"
- :style="channel.closed ? {transition: 'none', opacity: 0.4} : null"
+ :style="channel.closed ? {transition: 'none', opacity: 0.4} : undefined"
role="tab"
@click="click"
@contextmenu.prevent="openContextMenu"
>
-
+
-
diff --git a/client/components/Chat.vue b/client/components/Chat.vue
index 9838d1b6..f1dfc51b 100644
--- a/client/components/Chat.vue
+++ b/client/components/Chat.vue
@@ -3,10 +3,10 @@
@@ -95,7 +95,7 @@
{'scroll-down-shown': !channel.scrolledToBottom},
]"
aria-label="Jump to recent messages"
- @click="$refs.messageList.jumpToBottom()"
+ @click="messageList?.jumpToBottom()"
>
@@ -110,17 +110,17 @@
- {{ $store.state.currentUserVisibleError }}
+ {{ store.state.currentUserVisibleError }}
-
diff --git a/client/components/ChatInput.vue b/client/components/ChatInput.vue
index 401ee7ef..e89ef276 100644
--- a/client/components/ChatInput.vue
+++ b/client/components/ChatInput.vue
@@ -16,7 +16,7 @@
@blur="onBlur"
/>
-
diff --git a/client/components/ChatUserList.vue b/client/components/ChatUserList.vue
index 7eaa7c8b..ec3c971b 100644
--- a/client/components/ChatUserList.vue
+++ b/client/components/ChatUserList.vue
@@ -28,9 +28,10 @@
+
+
-
diff --git a/client/components/ConfirmDialog.vue b/client/components/ConfirmDialog.vue
index af92e368..17691cad 100644
--- a/client/components/ConfirmDialog.vue
+++ b/client/components/ConfirmDialog.vue
@@ -1,13 +1,13 @@
-
+
-
{{ data.title }}
-
{{ data.text }}
+
{{ data?.title }}
+
{{ data?.text }}
-
+
@@ -50,37 +50,53 @@
}
-
diff --git a/client/components/ContextMenu.vue b/client/components/ContextMenu.vue
index 60ff51be..8a51fc5c 100644
--- a/client/components/ContextMenu.vue
+++ b/client/components/ContextMenu.vue
@@ -14,14 +14,17 @@
id="context-menu"
ref="contextMenu"
role="menu"
- :style="style"
+ :style="{
+ top: style.top + 'px',
+ left: style.left + 'px',
+ }"
tabindex="-1"
@mouseleave="activeItem = -1"
@keydown.enter.prevent="clickActiveItem"
>
-
+
+
import {
generateUserContextMenu,
generateChannelContextMenu,
generateInlineChannelContextMenu,
-} from "../js/helpers/contextMenu.js";
+ ContextMenuItem,
+} from "../js/helpers/contextMenu";
import eventbus from "../js/eventbus";
+import {defineComponent, nextTick, onMounted, onUnmounted, PropType, ref} from "vue";
+import {ClientChan, ClientMessage, ClientNetwork, ClientUser} from "../js/types";
+import {useStore} from "../js/store";
+import {useRouter} from "vue-router";
-export default {
+export default defineComponent({
name: "ContextMenu",
props: {
- message: Object,
+ message: {
+ required: false,
+ type: Object as PropType,
+ },
},
- data() {
- return {
- isOpen: false,
- passthrough: false,
- previousActiveElement: null,
- items: [],
- activeItem: -1,
- style: {
- left: 0,
- top: 0,
- },
- };
- },
- mounted() {
- eventbus.on("escapekey", this.close);
- eventbus.on("contextmenu:cancel", this.close);
- eventbus.on("contextmenu:user", this.openUserContextMenu);
- eventbus.on("contextmenu:channel", this.openChannelContextMenu);
- eventbus.on("contextmenu:inline-channel", this.openInlineChannelContextMenu);
- },
- destroyed() {
- eventbus.off("escapekey", this.close);
- eventbus.off("contextmenu:cancel", this.close);
- eventbus.off("contextmenu:user", this.openUserContextMenu);
- eventbus.off("contextmenu:channel", this.openChannelContextMenu);
- eventbus.off("contextmenu:inline-channel", this.openInlineChannelContextMenu);
+ setup() {
+ const store = useStore();
+ const router = useRouter();
- this.close();
- },
- methods: {
- enablePointerEvents() {
- this.passthrough = false;
- document.body.removeEventListener("pointerup", this.enablePointerEvents, {
- passive: true,
- });
- },
- openChannelContextMenu(data) {
- if (data.event.type === "contextmenu") {
- // Pass through all pointer events to allow the network list's
- // dragging events to continue triggering.
- this.passthrough = true;
- document.body.addEventListener("pointerup", this.enablePointerEvents, {
- passive: true,
- });
- }
+ const isOpen = ref(false);
+ const passthrough = ref(false);
- const items = generateChannelContextMenu(this.$root, data.channel, data.network);
- this.open(data.event, items);
- },
- openInlineChannelContextMenu(data) {
- const {network} = this.$store.state.activeChannel;
- const items = generateInlineChannelContextMenu(this.$root, data.channel, network);
- this.open(data.event, items);
- },
- openUserContextMenu(data) {
- const {network, channel} = this.$store.state.activeChannel;
+ const contextMenu = ref();
+ const previousActiveElement = ref();
+ const items = ref([]);
+ const activeItem = ref(-1);
+ const style = ref({
+ top: 0,
+ left: 0,
+ });
- const items = generateUserContextMenu(
- this.$root,
- channel,
- network,
- channel.users.find((u) => u.nick === data.user.nick) || {
- nick: data.user.nick,
- modes: [],
- }
- );
- this.open(data.event, items);
- },
- open(event, items) {
- event.preventDefault();
-
- this.previousActiveElement = document.activeElement;
- this.items = items;
- this.activeItem = 0;
- this.isOpen = true;
-
- // Position the menu and set the focus on the first item after it's size has updated
- this.$nextTick(() => {
- const pos = this.positionContextMenu(event);
- this.style.left = pos.left + "px";
- this.style.top = pos.top + "px";
- this.$refs.contextMenu.focus();
- });
- },
- close() {
- if (!this.isOpen) {
+ const close = () => {
+ if (!isOpen.value) {
return;
}
- this.isOpen = false;
- this.items = [];
+ isOpen.value = false;
+ items.value = [];
- if (this.previousActiveElement) {
- this.previousActiveElement.focus();
- this.previousActiveElement = null;
+ if (previousActiveElement.value) {
+ previousActiveElement.value.focus();
+ previousActiveElement.value = null;
}
- },
- hoverItem(id) {
- this.activeItem = id;
- },
- clickItem(item) {
- this.close();
+ };
- if (item.action) {
- item.action();
- } else if (item.link) {
- this.$router.push(item.link);
- }
- },
- clickActiveItem() {
- if (this.items[this.activeItem]) {
- this.clickItem(this.items[this.activeItem]);
- }
- },
- navigateMenu(direction) {
- let currentIndex = this.activeItem;
+ const enablePointerEvents = () => {
+ passthrough.value = false;
+ document.body.removeEventListener("pointerup", enablePointerEvents);
+ };
- currentIndex += direction;
-
- const nextItem = this.items[currentIndex];
-
- // If the next item we would select is a divider, skip over it
- if (nextItem && nextItem.type === "divider") {
- currentIndex += direction;
- }
-
- if (currentIndex < 0) {
- currentIndex += this.items.length;
- }
-
- if (currentIndex > this.items.length - 1) {
- currentIndex -= this.items.length;
- }
-
- this.activeItem = currentIndex;
- },
- containerClick(event) {
+ const containerClick = (event: MouseEvent) => {
if (event.currentTarget === event.target) {
- this.close();
+ close();
}
- },
- positionContextMenu(event) {
- const element = event.target;
- const menuWidth = this.$refs.contextMenu.offsetWidth;
- const menuHeight = this.$refs.contextMenu.offsetHeight;
+ };
+
+ const positionContextMenu = (event: MouseEvent) => {
+ const element = event.target as HTMLElement;
+
+ if (!contextMenu.value) {
+ return;
+ }
+
+ const menuWidth = contextMenu.value?.offsetWidth;
+ const menuHeight = contextMenu.value?.offsetHeight;
if (element && element.classList.contains("menu")) {
return {
@@ -215,7 +131,154 @@ export default {
}
return offset;
- },
+ };
+
+ const hoverItem = (id: number) => {
+ activeItem.value = id;
+ };
+
+ const clickItem = (item: ContextMenuItem) => {
+ close();
+
+ if ("action" in item && item.action) {
+ item.action();
+ } else if ("link" in item && item.link) {
+ router.push(item.link).catch(() => {
+ // eslint-disable-next-line no-console
+ console.error("Failed to navigate to", item.link);
+ });
+ }
+ };
+
+ const clickActiveItem = () => {
+ if (items.value[activeItem.value]) {
+ clickItem(items.value[activeItem.value]);
+ }
+ };
+
+ const open = (event: MouseEvent, newItems: ContextMenuItem[]) => {
+ event.preventDefault();
+
+ previousActiveElement.value = document.activeElement as HTMLElement;
+ items.value = newItems;
+ activeItem.value = 0;
+ isOpen.value = true;
+
+ // Position the menu and set the focus on the first item after it's size has updated
+ nextTick(() => {
+ const pos = positionContextMenu(event);
+
+ if (!pos) {
+ return;
+ }
+
+ style.value.left = pos.left;
+ style.value.top = pos.top;
+ contextMenu.value?.focus();
+ }).catch((e) => {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ });
+ };
+
+ const openChannelContextMenu = (data: {
+ event: MouseEvent;
+ channel: ClientChan;
+ network: ClientNetwork;
+ }) => {
+ if (data.event.type === "contextmenu") {
+ // Pass through all pointer events to allow the network list's
+ // dragging events to continue triggering.
+ passthrough.value = true;
+ document.body.addEventListener("pointerup", enablePointerEvents, {
+ passive: true,
+ });
+ }
+
+ const newItems = generateChannelContextMenu(data.channel, data.network);
+ open(data.event, newItems);
+ };
+
+ const openInlineChannelContextMenu = (data: {channel: string; event: MouseEvent}) => {
+ const {network} = store.state.activeChannel;
+ const newItems = generateInlineChannelContextMenu(store, data.channel, network);
+
+ open(data.event, newItems);
+ };
+
+ const openUserContextMenu = (data: {
+ user: Pick;
+ event: MouseEvent;
+ }) => {
+ const {network, channel} = store.state.activeChannel;
+
+ const newItems = generateUserContextMenu(
+ store,
+ channel,
+ network,
+ channel.users.find((u) => u.nick === data.user.nick) || {
+ nick: data.user.nick,
+ modes: [],
+ }
+ );
+ open(data.event, newItems);
+ };
+
+ const navigateMenu = (direction: number) => {
+ let currentIndex = activeItem.value;
+
+ currentIndex += direction;
+
+ const nextItem = items.value[currentIndex];
+
+ // If the next item we would select is a divider, skip over it
+ if (nextItem && "type" in nextItem && nextItem.type === "divider") {
+ currentIndex += direction;
+ }
+
+ if (currentIndex < 0) {
+ currentIndex += items.value.length;
+ }
+
+ if (currentIndex > items.value.length - 1) {
+ currentIndex -= items.value.length;
+ }
+
+ activeItem.value = currentIndex;
+ };
+
+ onMounted(() => {
+ eventbus.on("escapekey", close);
+ eventbus.on("contextmenu:cancel", close);
+ eventbus.on("contextmenu:user", openUserContextMenu);
+ eventbus.on("contextmenu:channel", openChannelContextMenu);
+ eventbus.on("contextmenu:inline-channel", openInlineChannelContextMenu);
+ });
+
+ onUnmounted(() => {
+ eventbus.off("escapekey", close);
+ eventbus.off("contextmenu:cancel", close);
+ eventbus.off("contextmenu:user", openUserContextMenu);
+ eventbus.off("contextmenu:channel", openChannelContextMenu);
+ eventbus.off("contextmenu:inline-channel", openInlineChannelContextMenu);
+
+ close();
+ });
+
+ return {
+ isOpen,
+ items,
+ activeItem,
+ style,
+ contextMenu,
+ passthrough,
+ close,
+ containerClick,
+ navigateMenu,
+ hoverItem,
+ clickItem,
+ clickActiveItem,
+ };
},
-};
+});
diff --git a/client/components/DateMarker.vue b/client/components/DateMarker.vue
index 4b6fa37c..4125465c 100644
--- a/client/components/DateMarker.vue
+++ b/client/components/DateMarker.vue
@@ -6,52 +6,61 @@
-
diff --git a/client/components/Draggable.vue b/client/components/Draggable.vue
new file mode 100644
index 00000000..065662fd
--- /dev/null
+++ b/client/components/Draggable.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
diff --git a/client/components/ImageViewer.vue b/client/components/ImageViewer.vue
index 8ad19773..31b8d750 100644
--- a/client/components/ImageViewer.vue
+++ b/client/components/ImageViewer.vue
@@ -38,121 +38,125 @@
-
diff --git a/client/components/InlineChannel.vue b/client/components/InlineChannel.vue
index 5262561d..784a93ad 100644
--- a/client/components/InlineChannel.vue
+++ b/client/components/InlineChannel.vue
@@ -10,21 +10,26 @@
>
-
diff --git a/client/components/JoinChannel.vue b/client/components/JoinChannel.vue
index 129d5383..fa5eebc4 100644
--- a/client/components/JoinChannel.vue
+++ b/client/components/JoinChannel.vue
@@ -35,54 +35,59 @@
-
diff --git a/client/components/LinkPreview.vue b/client/components/LinkPreview.vue
index 9ae10b74..9a124ab8 100644
--- a/client/components/LinkPreview.vue
+++ b/client/components/LinkPreview.vue
@@ -129,137 +129,201 @@
-
diff --git a/client/components/LinkPreviewFileSize.vue b/client/components/LinkPreviewFileSize.vue
index 883df53d..5c577c1b 100644
--- a/client/components/LinkPreviewFileSize.vue
+++ b/client/components/LinkPreviewFileSize.vue
@@ -2,18 +2,21 @@
({{ previewSize }})
-
diff --git a/client/components/LinkPreviewToggle.vue b/client/components/LinkPreviewToggle.vue
index 2b461066..24351ab7 100644
--- a/client/components/LinkPreviewToggle.vue
+++ b/client/components/LinkPreviewToggle.vue
@@ -7,23 +7,31 @@
/>
-
diff --git a/client/components/Mentions.vue b/client/components/Mentions.vue
index 54759bff..63144948 100644
--- a/client/components/Mentions.vue
+++ b/client/components/Mentions.vue
@@ -20,20 +20,20 @@
Loading…
You have no recent mentions.
-
-
+
+
-
+
in {{ message.channel.channel.name }} on
{{ message.channel.network.name }}
- in unknown channel
-
+ in unknown channel {{ ` ` }}
- {{ messageTime(message.time) }}
+ {{ messageTime(message.time.toString()) }}
@@ -50,7 +50,7 @@
@@ -144,7 +144,7 @@
}
-
diff --git a/client/components/Message.vue b/client/components/Message.vue
index 5526badd..65167784 100644
--- a/client/components/Message.vue
+++ b/client/components/Message.vue
@@ -17,12 +17,14 @@
aria-hidden="true"
:aria-label="messageTimeLocale"
class="time tooltipped tooltipped-e"
- >{{ messageTime }}
+ >{{ `${messageTime} ` }}
[{{ message.command }}]
- {{ param }}
+ {{
+ ` ${param} `
+ }}
@@ -95,56 +97,73 @@
-
diff --git a/client/components/MessageCondensed.vue b/client/components/MessageCondensed.vue
index d167a86d..6cf92b67 100644
--- a/client/components/MessageCondensed.vue
+++ b/client/components/MessageCondensed.vue
@@ -17,35 +17,45 @@
-
diff --git a/client/components/MessageList.vue b/client/components/MessageList.vue
index cd298a6c..1c5ce9b5 100644
--- a/client/components/MessageList.vue
+++ b/client/components/MessageList.vue
@@ -3,7 +3,7 @@