fix sidebar buttons, channel loading, parting in ctxt menu

This commit is contained in:
Max Leiter 2022-05-23 23:35:28 -07:00
parent f189e9766c
commit 4740d1d574
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
61 changed files with 383 additions and 276 deletions

View file

@ -43,7 +43,7 @@ import {
import {useStore} from "../js/store";
import type {DebouncedFunc} from "lodash";
const imageViewerKey = Symbol() as InjectionKey<Ref<typeof ImageViewer | null>>;
export const imageViewerKey = Symbol() as InjectionKey<Ref<typeof ImageViewer | null>>;
const contextMenuKey = Symbol() as InjectionKey<Ref<typeof ContextMenu | null>>;
const confirmDialogKey = Symbol() as InjectionKey<Ref<typeof ConfirmDialog | null>>;
@ -51,14 +51,6 @@ export const useImageViewer = () => {
return inject(imageViewerKey) as Ref<typeof ImageViewer | null>;
};
export const useContextMenu = () => {
return inject(contextMenuKey) as Ref<typeof ContextMenu | null>;
};
export const useConfirmDialog = () => {
return inject(confirmDialogKey) as Ref<typeof ConfirmDialog | null>;
};
export default defineComponent({
name: "App",
components: {
@ -70,7 +62,6 @@ export default defineComponent({
},
setup() {
const store = useStore();
const overlay = ref(null);
const loungeWindow = ref(null);
const imageViewer = ref(null);

View file

@ -17,12 +17,12 @@
<span class="parted-channel-icon" />
</span>
<span class="close-tooltip tooltipped tooltipped-w" aria-label="Leave">
<button class="close" aria-label="Leave" @click.stop="close" />
<button class="close" aria-label="Leave" @click.stop="close()" />
</span>
</template>
<template v-else>
<span class="close-tooltip tooltipped tooltipped-w" aria-label="Close">
<button class="close" aria-label="Close" @click.stop="close" />
<button class="close" aria-label="Close" @click.stop="close()" />
</span>
</template>
</ChannelWrapper>

View file

@ -152,7 +152,8 @@ export default defineComponent({
channel: {type: Object as PropType<ClientChan>, required: true},
focused: String,
},
setup(props) {
emits: ["channel-changed"],
setup(props, {emit}) {
const store = useStore();
const messageList = ref<typeof MessageList>();
@ -175,8 +176,9 @@ export default defineComponent({
const channelChanged = () => {
// Triggered when active channel is set or changed
props.channel.highlight = 0;
props.channel.unread = 0;
// props.channel.highlight = 0;
// props.channel.unread = 0;
emit("channel-changed", props.channel);
socket.emit("open", props.channel.id);
@ -229,9 +231,12 @@ export default defineComponent({
});
};
watch(props.channel, () => {
channelChanged();
});
watch(
() => props.channel,
() => {
channelChanged();
}
);
const editTopicRef = ref(props.channel.editTopic);
watch(editTopicRef, (newTopic) => {

View file

@ -222,17 +222,21 @@ export default defineComponent({
}
};
const channelId = ref(props.channel.id);
watch(channelId, () => {
if (autocompletionRef.value) {
autocompletionRef.value.hide();
watch(
() => props.channel.id,
() => {
if (autocompletionRef.value) {
autocompletionRef.value.hide();
}
}
});
);
const pendingMessage = ref(props.channel.pendingMessage);
watch(pendingMessage, () => {
setInputSize();
});
watch(
() => props.channel.pendingMessage,
() => {
setInputSize();
}
);
onMounted(() => {
eventbus.on("escapekey", blurInput);
@ -347,8 +351,6 @@ export default defineComponent({
openFileUpload,
blurInput,
onBlur,
channelId,
pendingMessage,
setInputSize,
upload,
getInputPlaceholder,

View file

@ -83,7 +83,6 @@ export default defineComponent({
const userSearchInput = ref("");
const activeUser = ref<UserInMessage | null>();
const userlist = ref<HTMLDivElement>();
const filteredUsers = computed(() => {
if (!userSearchInput.value) {
return;

View file

@ -9,7 +9,7 @@
<script lang="ts">
import dayjs from "dayjs";
import calendar from "dayjs/plugin/calendar";
import Vue, {computed, defineComponent, onBeforeUnmount, onMounted, PropType} from "vue";
import {computed, defineComponent, onBeforeUnmount, onMounted, PropType} from "vue";
import eventbus from "../js/eventbus";
import {ClientMessage} from "../js/types";
@ -33,8 +33,8 @@ export default defineComponent({
const dayChange = () => {
// TODO: this is nasty. and maybe doesnt work?
const instance = Vue.getCurrentInstance();
instance?.proxy?.$forceUpdate();
// const instance = Vue.getCurrentInstance();
// instance?.proxy?.$forceUpdate();
if (hoursPassed() >= 48) {
eventbus.off("daychange", dayChange);

View file

@ -130,24 +130,23 @@
</template>
<script lang="ts">
import Vue, {
import {
computed,
defineComponent,
inject,
nextTick,
onBeforeUnmount,
onMounted,
onUnmounted,
PropType,
ref,
inject,
Ref,
watch,
} from "vue";
import eventbus from "../js/eventbus";
import friendlysize from "../js/helpers/friendlysize";
import {useStore} from "../js/store";
import type {ClientChan, ClientLinkPreview} from "../js/types";
import {useImageViewer} from "./App.vue";
import {imageViewerKey, useImageViewer} from "./App.vue";
export default defineComponent({
name: "LinkPreview",
@ -235,7 +234,7 @@ export default defineComponent({
const onThumbnailClick = (e: MouseEvent) => {
e.preventDefault();
const imageViewer = useImageViewer();
const imageViewer = inject(imageViewerKey);
if (!imageViewer.value) {
return;
@ -280,11 +279,13 @@ export default defineComponent({
updateShownState();
const linkTypeRef = ref(props.link.type);
watch(linkTypeRef, () => {
updateShownState();
onPreviewUpdate();
});
watch(
() => props.link.type,
() => {
updateShownState();
onPreviewUpdate();
}
);
onMounted(() => {
eventbus.on("resize", handleResize);
@ -301,6 +302,20 @@ export default defineComponent({
// Otherwise the browser can cause a resize on video elements
props.link.sourceLoaded = false;
});
return {
moreButtonLabel,
imageMaxSize,
onThumbnailClick,
onThumbnailError,
onMoreClick,
onPreviewReady,
onPreviewUpdate,
showMoreButton,
isContentShown,
content,
container,
};
},
});
</script>

View file

@ -33,7 +33,7 @@
<template v-else> in unknown channel </template>
</span>
<span :title="message.localetime" class="time">
{{ messageTime(message.time) }}
{{ messageTime(message.time.toString()) }}
</span>
</div>
<div>
@ -50,7 +50,7 @@
</div>
</div>
<div class="content" dir="auto">
<ParsedMessage :network="null" :message="message" />
<ParsedMessage message="message" />
</div>
</div>
</template>
@ -179,9 +179,12 @@ export default defineComponent({
return messages.filter((message) => !message.channel?.channel.muted);
});
watch(store.state.mentions, () => {
isLoading.value = false;
});
watch(
() => store.state.mentions,
() => {
isLoading.value = false;
}
);
const messageTime = (time: string) => {
return dayjs(time).fromNow();

View file

@ -89,6 +89,9 @@ type CondensedMessageContainer = {
id: number;
};
// TODO; move into component
let unreadMarkerShown = false;
export default defineComponent({
name: "MessageList",
components: {
@ -108,7 +111,6 @@ export default defineComponent({
const loadMoreButton = ref<HTMLButtonElement | null>(null);
const historyObserver = ref<IntersectionObserver | null>(null);
const skipNextScrollEvent = ref(false);
const unreadMarkerShown = ref(false);
const isWaitingForNextTick = ref(false);
@ -265,8 +267,8 @@ export default defineComponent({
};
const shouldDisplayUnreadMarker = (id: number) => {
if (!unreadMarkerShown.value && id > props.channel.firstUnread) {
unreadMarkerShown.value = true;
if (!unreadMarkerShown && id > props.channel.firstUnread) {
unreadMarkerShown = true;
return true;
}
@ -380,35 +382,41 @@ export default defineComponent({
});
});
const channelId = ref(props.channel.id);
watch(channelId, () => {
props.channel.scrolledToBottom = true;
watch(
() => props.channel.id,
() => {
props.channel.scrolledToBottom = true;
// Re-add the intersection observer to trigger the check again on channel switch
// Otherwise if last channel had the button visible, switching to a new channel won't trigger the history
if (historyObserver.value && loadMoreButton.value) {
historyObserver.value.unobserve(loadMoreButton.value);
historyObserver.value.observe(loadMoreButton.value);
// Re-add the intersection observer to trigger the check again on channel switch
// Otherwise if last channel had the button visible, switching to a new channel won't trigger the history
if (historyObserver.value && loadMoreButton.value) {
historyObserver.value.unobserve(loadMoreButton.value);
historyObserver.value.observe(loadMoreButton.value);
}
}
});
);
const channelMessages = ref(props.channel.messages);
watch(channelMessages, () => {
keepScrollPosition();
});
const pendingMessage = ref(props.channel.pendingMessage);
watch(pendingMessage, () => {
nextTick(() => {
// Keep the scroll stuck when input gets resized while typing
watch(
() => props.channel.messages,
() => {
keepScrollPosition();
}).catch(() => {
// no-op
});
});
}
);
watch(
() => props.channel.pendingMessage,
() => {
nextTick(() => {
// Keep the scroll stuck when input gets resized while typing
keepScrollPosition();
}).catch(() => {
// no-op
});
}
);
onBeforeUpdate(() => {
unreadMarkerShown.value = false;
unreadMarkerShown = false;
});
onBeforeUnmount(() => {

View file

@ -499,27 +499,30 @@ export default defineComponent({
)}px`;
};
const commands = ref(props.defaults?.commands);
watch(commands, () => {
nextTick(() => {
resizeCommandsInput();
}).catch((e) => {
// no-op
});
});
const tls = ref(props.defaults?.tls);
watch(tls, (isSecureChecked) => {
const ports = [6667, 6697];
const newPort = isSecureChecked ? 0 : 1;
// If you disable TLS and current port is 6697,
// set it to 6667, and vice versa
if (props.defaults?.port === ports[newPort]) {
props.defaults.port = ports[1 - newPort];
watch(
() => props.defaults?.commands,
() => {
nextTick(() => {
resizeCommandsInput();
}).catch((e) => {
// no-op
});
}
});
);
watch(
() => props.defaults?.tls,
(isSecureChecked) => {
const ports = [6667, 6697];
const newPort = isSecureChecked ? 0 : 1;
// If you disable TLS and current port is 6697,
// set it to 6667, and vice versa
if (props.defaults?.port === ports[newPort]) {
props.defaults.port = ports[1 - newPort];
}
}
);
const setSaslAuth = (type: string) => {
if (props.defaults) {
@ -559,10 +562,8 @@ export default defineComponent({
config,
displayPasswordField,
publicPassword,
commands,
commandsInput,
resizeCommandsInput,
tls,
setSaslAuth,
usernameInput,
onNickChanged,

View file

@ -8,12 +8,9 @@ export default defineComponent({
functional: true,
props: {
text: String,
message: {type: Object as PropType<ClientMessage>, required: false},
message: {type: Object as PropType<ClientMessage | string>, required: false},
network: {type: Object as PropType<ClientNetwork>, required: false},
},
setup(props) {
//
},
render(context) {
return parse(
typeof context.text !== "undefined" ? context.text : context.message.text,

View file

@ -4,6 +4,7 @@
:network="activeChannel.network"
:channel="activeChannel.channel"
:focused="(route.query.focused as string)"
@channel-changed="channelChanged"
/>
</template>
@ -11,6 +12,7 @@
import {watch, computed, defineComponent, onMounted} from "vue";
import {useRoute} from "vue-router";
import {useStore} from "../js/store";
import {ClientChan} from "../js/types";
// Temporary component for routing channels and lobbies
import Chat from "./Chat.vue";
@ -44,9 +46,20 @@ export default defineComponent({
setActiveChannel();
});
const channelChanged = (channel: ClientChan) => {
const chanId = channel.id;
const chanInStore = store.getters.findChannel(chanId);
if (chanInStore?.channel) {
chanInStore.channel.unread = 0;
chanInStore.channel.highlight = 0;
}
};
return {
route,
activeChannel,
channelChanged,
};
},
});

View file

@ -103,7 +103,7 @@ import socket from "../../js/socket";
import RevealPassword from "../RevealPassword.vue";
import Session from "../Session.vue";
import {computed, defineComponent, onMounted, ref} from "vue";
import store from "../../js/store";
import {useStore} from "../../js/store";
export default defineComponent({
name: "UserSettings",
@ -112,6 +112,7 @@ export default defineComponent({
Session,
},
setup() {
const store = useStore();
const settingsForm = ref<HTMLFormElement>();
const passwordErrors = {
missing_fields: "Please enter a new password",

View file

@ -161,7 +161,7 @@ export default defineComponent({
name: "NotificationSettings",
setup() {
const store = useStore();
console.log(store);
const isIOS = computed(
() =>
[
@ -176,10 +176,12 @@ export default defineComponent({
(navigator.userAgent.includes("Mac") && "ontouchend" in document)
);
const playNotification = async () => {
const playNotification = () => {
const pop = new Audio();
pop.src = "audio/pop.wav";
await pop.play();
// eslint-disable-next-line
pop.play();
};
const onPushButtonClick = () => {
@ -188,9 +190,9 @@ export default defineComponent({
return {
isIOS,
store,
playNotification,
onPushButtonClick,
store,
};
},
});

View file

@ -1,16 +1,11 @@
<template>
<li :aria-label="name">
<router-link
v-slot:default="{navigate, isExactActive}"
:to="'/settings/' + to"
:class="['icon', className]"
:aria-label="name"
role="tab"
aria-controls="settings"
:aria-selected="route.name === name"
custom
>
<button :class="{active: isExactActive}" @click="navigate" @keypress.enter="navigate">
<li :aria-label="name" role="tab" :aria-selected="route.name === name" aria-controls="settings">
<router-link v-slot:default="{navigate, isExactActive}" :to="'/settings/' + to" custom>
<button
:class="['icon', className, {active: isExactActive}]"
@click="navigate"
@keypress.enter="navigate"
>
{{ name }}
</button>
</router-link>
@ -18,6 +13,14 @@
</template>
<script lang="ts">
// v-slot:default="{navigate, isExactActive}"
// :to="'/settings/' + to"
// :class="['icon', className]"
// :aria-label="name"
// role="tab"
// aria-controls="settings"
// :aria-selected="route.name === name"
// custom
import {defineComponent} from "vue";
import {useRoute} from "vue-router";

View file

@ -34,26 +34,32 @@
class="tooltipped tooltipped-n tooltipped-no-touch"
aria-label="Connect to network"
><router-link
v-slot:default="{navigate, isActive}"
to="/connect"
tag="button"
active-class="active"
:class="['icon', 'connect']"
aria-label="Connect to network"
role="tab"
aria-controls="connect"
:aria-selected="route.name === 'Connect'"
/></span>
>
<button
:class="['icon', 'connect', {active: isActive}]"
:aria-selected="isActive"
@click="navigate"
@keypress.enter="navigate"
/> </router-link
></span>
<span class="tooltipped tooltipped-n tooltipped-no-touch" aria-label="Settings"
><router-link
v-slot:default="{navigate, isActive}"
to="/settings"
tag="button"
active-class="active"
:class="['icon', 'settings']"
aria-label="Settings"
role="tab"
aria-controls="settings"
:aria-selected="route.name === 'General'"
/></span>
>
<button
:class="['icon', 'settings', {active: isActive}]"
:aria-selected="isActive"
@click="navigate"
@keypress.enter="navigate"
></button> </router-link
></span>
<span
class="tooltipped tooltipped-n tooltipped-no-touch"
:aria-label="
@ -62,19 +68,23 @@
: 'Help'
"
><router-link
v-slot:default="{navigate, isActive}"
to="/help"
tag="button"
active-class="active"
:class="[
'icon',
'help',
{notified: store.state.serverConfiguration?.isUpdateAvailable},
]"
aria-label="Help"
role="tab"
aria-controls="help"
:aria-selected="route.name === 'Help'"
/></span>
>
<button
:aria-selected="route.name === 'Help'"
:class="[
'icon',
'help',
{notified: store.state.serverConfiguration?.isUpdateAvailable},
{active: isActive},
]"
@click="navigate"
@keypress.enter="navigate"
></button> </router-link
></span>
</footer>
</aside>
</template>

View file

@ -48,7 +48,7 @@ export default defineComponent({
return props.user.mode;
});
const nickColor = computed(() => colorClass(props.user.nick));
const nickColor = computed(() => colorClass(props.user.nick!));
const hover = () => {
if (props.onHover) {

View file

@ -50,11 +50,12 @@ export default defineComponent({
// this.setNetworkData();
// },
// },
watch(route.params, (newValue) => {
if (newValue.uuid) {
watch(
() => route.params.uuid,
(newValue) => {
setNetworkData();
}
});
);
onMounted(() => {
setNetworkData();

View file

@ -68,7 +68,7 @@
<!-- TODO: this was message.date -->
<DateMarker
v-if="shouldDisplayDateMarker(message, id)"
:key="message.when.toString()"
:key="message.time"
:message="message"
/>
<!-- todo channel and network ! -->
@ -257,18 +257,21 @@ export default defineComponent({
});
};
const routeIdRef = ref(route.params.id);
const routeQueryRef = ref(route.query);
watch(
() => route.params.id,
() => {
doSearch();
setActiveChannel();
}
);
watch(routeIdRef, () => {
doSearch();
setActiveChannel();
});
watch(routeQueryRef, () => {
doSearch();
setActiveChannel();
});
watch(
() => route.query,
() => {
doSearch();
setActiveChannel();
}
);
watch(messages, () => {
moreResultsAvailable.value = !!(

View file

@ -7,7 +7,7 @@ import {TextareaEditor} from "@textcomplete/textarea/dist/TextareaEditor";
import fuzzy from "fuzzy";
import emojiMap from "./helpers/simplemap.json";
import store from "./store";
import {store} from "./store";
export default enableAutocomplete;

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
function input() {
const messageIds = [];

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
function input() {
const messageIds = [];

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
import {switchToChannel} from "../router";
function input(args) {

View file

@ -1,20 +1,25 @@
import store from "../store";
import {store} from "../store";
import {router} from "../router";
function input(args) {
function input(args: string[]) {
if (!store.state.settings.searchEnabled) {
return false;
}
router.push({
name: "SearchResults",
params: {
id: store.state.activeChannel.channel.id,
},
query: {
q: args.join(" "),
},
});
router
.push({
name: "SearchResults",
params: {
id: store.state.activeChannel.channel.id,
},
query: {
q: args.join(" "),
},
})
.catch((e: Error) => {
// eslint-disable-next-line no-console
console.error(`Failed to push SearchResults route: ${e.message}`);
});
return true;
}

View file

@ -1,5 +1,9 @@
// Generates a string from "color-1" to "color-32" based on an input string
export default (str: string) => {
if (!str) {
return "";
}
let hash = 0;
for (let i = 0; i < str.length; i++) {

View file

@ -3,7 +3,7 @@ import eventbus from "../eventbus";
import type {ClientChan, ClientNetwork, ClientUser} from "../types";
import {switchToChannel} from "../router";
import {TypedStore} from "../store";
import closeChannel from "../hooks/use-close-channel";
import useCloseChannel from "../hooks/use-close-channel";
type BaseContextMenuItem = {
label: string;
@ -32,6 +32,8 @@ export function generateChannelContextMenu(
channel: ClientChan,
network: ClientNetwork
): ContextMenuItem[] {
const closeChannel = useCloseChannel(channel);
const typeMap = {
lobby: "network",
channel: "chan",
@ -229,7 +231,7 @@ export function generateChannelContextMenu(
type: "item",
class: "close",
action() {
closeChannel(channel);
closeChannel();
},
});

View file

@ -1,4 +1,4 @@
import store from "../store";
import {store} from "../store";
export default (network, channel) => {
if (!network.isCollapsed || channel.highlight || channel.type === "lobby") {

View file

@ -112,13 +112,11 @@ function parse(text: string, message?: ClientMessage, network?: ClientNetwork) {
const link = createElement(
"a",
{
attrs: {
// @ts-ignore
href: textPart.link,
dir: preview ? null : "auto",
target: "_blank",
rel: "noopener",
},
// @ts-ignore
"^href": textPart.link,
"^dir": preview ? null : "auto",
"^target": "_blank",
"^rel": "noopener",
},
() => fragments
);

View file

@ -1,6 +1,6 @@
import Mousetrap from "mousetrap";
import store from "./store";
import {store} from "./store";
import {switchToChannel, router, navigate} from "./router";
import isChannelCollapsed from "./helpers/isChannelCollapsed";
import isIgnoredKeybind from "./helpers/isIgnoredKeybind";

View file

@ -9,7 +9,7 @@ import Changelog from "../components/Windows/Changelog.vue";
import NetworkEdit from "../components/Windows/NetworkEdit.vue";
import SearchResults from "../components/Windows/SearchResults.vue";
import RoutedChat from "../components/RoutedChat.vue";
import store from "./store";
import {store} from "./store";
import AppearanceSettings from "../components/Settings/Appearance.vue";
import GeneralSettings from "../components/Settings/General.vue";

View file

@ -1,5 +1,5 @@
import socket from "./socket";
import {TypedStore} from "./store";
import type {TypedStore} from "./store";
const defaultSettingConfig = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
@ -35,7 +35,14 @@ const defaultConfig = {
default: false,
sync: "never",
apply(store: TypedStore, value: boolean) {
// TODO: investigate ignores
// TODO: investigate
if (!store) {
return;
// throw new Error("store is not defined");
}
// Commit a mutation. options can have root: true that allows to commit root mutations in namespaced modules.
// https://vuex.vuejs.org/api/#store-instance-methods. not typed?
// @ts-ignore
store.commit("refreshDesktopNotificationState", null, {root: true});
@ -87,14 +94,24 @@ const defaultConfig = {
theme: {
default: document.getElementById("theme")?.dataset.serverTheme,
apply(store: TypedStore, value: string) {
const themeEl = document.getElementById("theme") as any;
const themeEl = document.getElementById("theme");
const themeUrl = `themes/${value}.css`;
if (themeEl?.attributes.href.value === themeUrl) {
if (!(themeEl instanceof HTMLLinkElement)) {
throw new Error("theme element is not a link");
}
const hrefAttr = themeEl.attributes.getNamedItem("href");
if (!hrefAttr) {
throw new Error("theme is missing href attribute");
}
if (hrefAttr.value === themeUrl) {
return;
}
themeEl.attributes.href.value = themeUrl;
hrefAttr.value = themeUrl;
if (!store.state.serverConfiguration) {
return;
@ -106,9 +123,13 @@ const defaultConfig = {
const metaSelector = document.querySelector('meta[name="theme-color"]');
if (!(metaSelector instanceof HTMLMetaElement)) {
throw new Error("theme meta element is not a meta element");
}
if (metaSelector) {
const themeColor = newTheme.themeColor || (metaSelector as any).content;
(metaSelector as any).content = themeColor;
const themeColor = newTheme.themeColor || metaSelector.content;
metaSelector.content = themeColor;
}
},
},

View file

@ -1,7 +1,7 @@
import socket from "../socket";
import storage from "../localStorage";
import {router, navigate} from "../router";
import store from "../store";
import {store} from "../store";
import location from "../location";
let lastServerHash = null;

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("changelog", function (data) {
store.commit("versionData", data);

View file

@ -1,13 +1,13 @@
import socket from "../socket";
import upload from "../upload";
import store from "../store";
import {store} from "../store";
socket.once("configuration", function (data) {
store.commit("serverConfiguration", data);
// 'theme' setting depends on serverConfiguration.themes so
// settings cannot be applied before this point
store.dispatch("settings/applyAll");
void store.dispatch("settings/applyAll");
if (data.fileUpload) {
upload.initialize();
@ -18,9 +18,14 @@ socket.once("configuration", function (data) {
const currentTheme = data.themes.find((t) => t.name === store.state.settings.theme);
if (currentTheme === undefined) {
store.dispatch("settings/update", {name: "theme", value: data.defaultTheme, sync: true});
void store.dispatch("settings/update", {
name: "theme",
value: data.defaultTheme,
sync: true,
});
} else if (currentTheme.themeColor) {
document.querySelector('meta[name="theme-color"]').content = currentTheme.themeColor;
(document.querySelector('meta[name="theme-color"]') as HTMLMetaElement).content =
currentTheme.themeColor;
}
if (document.body.classList.contains("public")) {

View file

@ -1,4 +1,4 @@
import store from "../store";
import {store} from "../store";
import socket from "../socket";
socket.on("disconnect", handleDisconnect);
@ -26,7 +26,7 @@ socket.on("connect", function () {
});
function handleDisconnect(data) {
const message = data.message || data;
const message = (data.message || data) as string;
store.commit("isConnected", false);
@ -45,6 +45,7 @@ function handleDisconnect(data) {
// If the server shuts down, socket.io skips reconnection
// and we have to manually call connect to start the process
// However, do not reconnect if TL client manually closed the connection
// @ts-ignore TODO
if (socket.io.skipReconnect && message !== "io client disconnect") {
requestIdleCallback(() => socket.connect(), 2000);
}

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("history:clear", function (data) {
const {channel} = store.getters.findChannel(data.target);

View file

@ -2,7 +2,7 @@ import {nextTick} from "vue";
import socket from "../socket";
import storage from "../localStorage";
import {router, switchToChannel, navigate} from "../router";
import store from "../store";
import {store} from "../store";
import parseIrcUri from "../helpers/parseIrcUri";
import {ClientNetwork, InitClientChan} from "../types";

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
import {switchToChannel} from "../router";
socket.on("join", function (data) {

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("mentions:list", function (data) {
store.commit("mentions", data);

View file

@ -1,7 +1,7 @@
import {nextTick} from "vue";
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("more", function (data) {
const channel = store.getters.findChannel(data.chan)?.channel;

View file

@ -1,6 +1,6 @@
import socket from "../socket";
import cleanIrcMessage from "../helpers/ircmessageparser/cleanIrcMessage";
import store from "../store";
import {store} from "../store";
import {switchToChannel} from "../router";
let pop;

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("msg:preview", function (data) {
const {channel} = store.getters.findChannel(data.chan);

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
import {switchToChannel} from "../router";
socket.on("msg:special", function (data) {

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("mute:changed", (response) => {
const {target, status} = response;

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("names", function (data) {
const channel = store.getters.findChannel(data.id);

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
import {switchToChannel} from "../router";
socket.on("network", function (data) {

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("nick", function (data) {
const network = store.getters.findNetwork(data.network);

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
// Sync unread badge and marker when other clients open a channel
socket.on("open", function (id) {

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
import {switchToChannel} from "../router";
socket.on("part", function (data) {

View file

@ -1,6 +1,6 @@
import socket from "../socket";
import {switchToChannel, navigate} from "../router";
import store from "../store";
import {store} from "../store";
socket.on("quit", function (data) {
// If we're in a channel, and it's on the network that is being removed,

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("search:results", (response) => {
store.commit("messageSearchInProgress", false);

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("sessions:list", function (data) {
data.sort((a, b) => b.lastUse - a.lastUse);

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("setting:new", function (data) {
const name = data.name;

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("sync_sort", function (data) {
const order = data.order;

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("topic", function (data) {
const channel = store.getters.findChannel(data.chan);

View file

@ -1,5 +1,5 @@
import socket from "../socket";
import store from "../store";
import {store} from "../store";
socket.on("users", function (data) {
if (store.state.activeChannel && store.state.activeChannel.channel.id === data.chan) {

View file

@ -4,7 +4,6 @@ import {ActionContext, createStore, Store, useStore as baseUseStore} from "vuex"
import {createSettingsStore} from "./store-settings";
import storage from "./localStorage";
import type {
Mention,
ClientChan,
ClientConfiguration,
ClientNetwork,
@ -13,16 +12,8 @@ import type {
ClientMessage,
ClientMention,
} from "./types";
import type {InjectionKey, WatchOptions} from "vue";
import type {InjectionKey} from "vue";
// import {
// useAccessor,
// getterTree,
// mutationTree,
// actionTree,
// getAccessorType,
// } from 'typed-vuex'
import {VueApp} from "./vue";
import {SettingsState} from "./settings";
const appName = document.title;
@ -397,14 +388,8 @@ const storePattern = {
getters,
};
export const store = createStore(storePattern);
const settingsStore = createSettingsStore(store);
// Settings module is registered dynamically because it benefits
// from a direct reference to the store
store.registerModule("settings", settingsStore);
// https://vuex.vuejs.org/guide/typescript-support.html#typing-usestore-composition-function
export const key: InjectionKey<Store<State>> = Symbol();
@ -417,7 +402,11 @@ export type TypedStore = Omit<Store<State>, "getters" | "commit"> & {
};
};
export default store;
export const store = createStore(storePattern) as TypedStore;
// Settings module is registered dynamically because it benefits
// from a direct reference to the store
store.registerModule("settings", settingsStore);
export function useStore() {
return baseUseStore(key) as TypedStore;

View file

@ -1,14 +1,17 @@
import {update as updateCursor} from "undate";
import socket from "./socket";
import store from "./store";
import {store} from "./store";
class Uploader {
init() {
this.xhr = null;
this.fileQueue = [];
this.tokenKeepAlive = null;
xhr: XMLHttpRequest | null = null;
fileQueue: File[] = [];
tokenKeepAlive: NodeJS.Timeout | null = null;
overlay: HTMLDivElement | null = null;
uploadProgressbar: HTMLSpanElement | null = null;
init() {
document.addEventListener("dragenter", (e) => this.dragEnter(e));
document.addEventListener("dragover", (e) => this.dragOver(e));
document.addEventListener("dragleave", (e) => this.dragLeave(e));
@ -19,45 +22,45 @@ class Uploader {
}
mounted() {
this.overlay = document.getElementById("upload-overlay");
this.uploadProgressbar = document.getElementById("upload-progressbar");
this.overlay = document.getElementById("upload-overlay") as HTMLDivElement;
this.uploadProgressbar = document.getElementById("upload-progressbar") as HTMLSpanElement;
}
dragOver(event) {
if (event.dataTransfer.types.includes("Files")) {
dragOver(event: DragEvent) {
if (event.dataTransfer?.types.includes("Files")) {
// Prevent dragover event completely and do nothing with it
// This stops the browser from trying to guess which cursor to show
event.preventDefault();
}
}
dragEnter(event) {
dragEnter(event: DragEvent) {
// relatedTarget is the target where we entered the drag from
// when dragging from another window, the target is null, otherwise its a DOM element
if (!event.relatedTarget && event.dataTransfer.types.includes("Files")) {
if (!event.relatedTarget && event.dataTransfer?.types.includes("Files")) {
event.preventDefault();
this.overlay.classList.add("is-dragover");
this.overlay?.classList.add("is-dragover");
}
}
dragLeave(event) {
dragLeave(event: DragEvent) {
// If relatedTarget is null, that means we are no longer dragging over the page
if (!event.relatedTarget) {
event.preventDefault();
this.overlay.classList.remove("is-dragover");
this.overlay?.classList.remove("is-dragover");
}
}
drop(event) {
if (!event.dataTransfer.types.includes("Files")) {
drop(event: DragEvent) {
if (!event.dataTransfer?.types.includes("Files")) {
return;
}
event.preventDefault();
this.overlay.classList.remove("is-dragover");
this.overlay?.classList.remove("is-dragover");
let files;
let files: (File | null)[];
if (event.dataTransfer.items) {
files = Array.from(event.dataTransfer.items)
@ -70,13 +73,17 @@ class Uploader {
this.triggerUpload(files);
}
paste(event) {
const items = event.clipboardData.items;
const files = [];
paste(event: ClipboardEvent) {
const items = event.clipboardData?.items;
const files: (File | null)[] = [];
for (const item of items) {
if (item.kind === "file") {
files.push(item.getAsFile());
if (!items) {
return;
}
for (let i = 0; i < items.length; i++) {
if (items[i].kind === "file") {
files.push(items[i].getAsFile());
}
}
@ -88,7 +95,7 @@ class Uploader {
this.triggerUpload(files);
}
triggerUpload(files) {
triggerUpload(files: (File | null)[]) {
if (!files.length) {
return;
}
@ -102,9 +109,13 @@ class Uploader {
}
const wasQueueEmpty = this.fileQueue.length === 0;
const maxFileSize = store.state.serverConfiguration?.fileUploadMaxFileSize;
const maxFileSize = store.state.serverConfiguration?.fileUploadMaxFileSize || 0;
for (const file of files) {
if (!file) {
return;
}
if (maxFileSize > 0 && file.size > maxFileSize) {
this.handleResponse({
error: `File ${file.name} is over the maximum allowed size`,
@ -127,14 +138,22 @@ class Uploader {
socket.emit("upload:auth");
}
setProgress(value) {
setProgress(value: number) {
if (!this.uploadProgressbar) {
return;
}
this.uploadProgressbar.classList.toggle("upload-progressbar-visible", value > 0);
this.uploadProgressbar.style.width = value + "%";
this.uploadProgressbar.style.width = `${value}%`;
}
uploadNextFileInQueue(token) {
uploadNextFileInQueue(token: string) {
const file = this.fileQueue.shift();
if (!file) {
return;
}
// Tell the server that we are still upload to this token
// so it does not become invalidated and fail the upload.
// This issue only happens if The Lounge is proxied through other software
@ -153,7 +172,7 @@ class Uploader {
}
}
renderImage(file, callback) {
renderImage(file: File, callback: (file: File) => void) {
const fileReader = new FileReader();
fileReader.onabort = () => callback(file);
@ -169,20 +188,25 @@ class Uploader {
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Could not get canvas context in upload");
}
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob) => {
callback(new File([blob], file.name));
callback(new File([blob!], file.name));
}, file.type);
};
img.src = fileReader.result;
img.src = fileReader.result as string;
};
fileReader.readAsDataURL(file);
}
performUpload(token, file) {
performUpload(token: string, file: File) {
this.xhr = new XMLHttpRequest();
this.xhr.upload.addEventListener(
@ -195,7 +219,7 @@ class Uploader {
);
this.xhr.onreadystatechange = () => {
if (this.xhr.readyState === XMLHttpRequest.DONE) {
if (this.xhr?.readyState === XMLHttpRequest.DONE) {
let response;
try {
@ -227,7 +251,7 @@ class Uploader {
this.xhr.send(formData);
}
handleResponse(response) {
handleResponse(response: {error?: string; url?: string}) {
this.setProgress(0);
if (this.tokenKeepAlive) {
@ -245,9 +269,14 @@ class Uploader {
}
}
insertUploadUrl(url) {
const fullURL = new URL(url, location).toString();
insertUploadUrl(url: string) {
const fullURL = new URL(url, location.toString()).toString();
const textbox = document.getElementById("input");
if (!textbox) {
throw new Error("Could not find textbox in upload");
}
const initStart = textbox.selectionStart;
// Get the text before the cursor, and add a space if it's not in the beginning

View file

@ -2,7 +2,7 @@ import constants from "./constants";
import "../css/style.css";
import {createApp} from "vue";
import store, {CallableGetters, key, State, TypedStore} from "./store";
import {store, CallableGetters, key} from "./store";
import App from "../components/App.vue";
import storage from "./localStorage";
import {router, navigate} from "./router";
@ -12,7 +12,6 @@ import eventbus from "./eventbus";
import "./socket-events";
import "./webpush";
import "./keybinds";
import {Store} from "vuex";
import {LoungeWindow} from "./types";
const favicon = document.getElementById("favicon");

View file

@ -1,5 +1,5 @@
import socket from "./socket";
import store from "./store";
import {store} from "./store";
export default {togglePushSubscription};

View file

@ -598,7 +598,7 @@ class Client {
}
}
search(query: SearchQuery): Promise<SearchResponse> {
search(query: SearchQuery) {
if (this.messageProvider === undefined) {
return Promise.resolve({
results: [],