mirror of
https://github.com/thelounge/thelounge.git
synced 2024-05-17 05:36:36 +02:00
fix sidebar buttons, channel loading, parting in ctxt menu
This commit is contained in:
parent
f189e9766c
commit
4740d1d574
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -50,11 +50,12 @@ export default defineComponent({
|
|||
// this.setNetworkData();
|
||||
// },
|
||||
// },
|
||||
watch(route.params, (newValue) => {
|
||||
if (newValue.uuid) {
|
||||
watch(
|
||||
() => route.params.uuid,
|
||||
(newValue) => {
|
||||
setNetworkData();
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
setNetworkData();
|
||||
|
|
|
@ -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 = !!(
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import socket from "../socket";
|
||||
import store from "../store";
|
||||
import {store} from "../store";
|
||||
|
||||
function input() {
|
||||
const messageIds = [];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import socket from "../socket";
|
||||
import store from "../store";
|
||||
import {store} from "../store";
|
||||
|
||||
function input() {
|
||||
const messageIds = [];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import socket from "../socket";
|
||||
import store from "../store";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
|
||||
function input(args) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import store from "../store";
|
||||
import {store} from "../store";
|
||||
|
||||
export default (network, channel) => {
|
||||
if (!network.isCollapsed || channel.highlight || channel.type === "lobby") {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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")) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import socket from "./socket";
|
||||
import store from "./store";
|
||||
import {store} from "./store";
|
||||
|
||||
export default {togglePushSubscription};
|
||||
|
||||
|
|
|
@ -598,7 +598,7 @@ class Client {
|
|||
}
|
||||
}
|
||||
|
||||
search(query: SearchQuery): Promise<SearchResponse> {
|
||||
search(query: SearchQuery) {
|
||||
if (this.messageProvider === undefined) {
|
||||
return Promise.resolve({
|
||||
results: [],
|
||||
|
|
Loading…
Reference in a new issue