mirror of
https://github.com/thelounge/thelounge.git
synced 2024-05-17 05:36:36 +02:00
progress
This commit is contained in:
parent
f37d82dd19
commit
52c13f49c1
|
@ -29,10 +29,36 @@ import ImageViewer from "./ImageViewer.vue";
|
|||
import ContextMenu from "./ContextMenu.vue";
|
||||
import ConfirmDialog from "./ConfirmDialog.vue";
|
||||
import Mentions from "./Mentions.vue";
|
||||
import {computed, defineComponent, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
import {
|
||||
computed,
|
||||
provide,
|
||||
defineComponent,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
Ref,
|
||||
InjectionKey,
|
||||
inject,
|
||||
} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
import type {DebouncedFunc} from "lodash";
|
||||
|
||||
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>>;
|
||||
|
||||
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: {
|
||||
|
@ -51,6 +77,10 @@ export default defineComponent({
|
|||
const contextMenu = ref(null);
|
||||
const confirmDialog = ref(null);
|
||||
|
||||
provide(imageViewerKey, imageViewer);
|
||||
provide(contextMenuKey, contextMenu);
|
||||
provide(confirmDialogKey, confirmDialog);
|
||||
|
||||
const viewportClasses = computed(() => {
|
||||
return {
|
||||
notified: store.getters.highlightCount > 0,
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
<div
|
||||
id="chat"
|
||||
:class="{
|
||||
'hide-motd': !$store.state.settings.motd,
|
||||
'colored-nicks': $store.state.settings.coloredNicks,
|
||||
'time-seconds': $store.state.settings.showSeconds,
|
||||
'time-12h': $store.state.settings.use12hClock,
|
||||
'hide-motd': store.state.settings.motd,
|
||||
'colored-nicks': store.state.settings.coloredNicks,
|
||||
'time-seconds': store.state.settings.showSeconds,
|
||||
'time-12h': store.state.settings.use12hClock,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
|
@ -47,7 +47,7 @@
|
|||
/></span>
|
||||
<MessageSearchForm
|
||||
v-if="
|
||||
$store.state.settings.searchEnabled &&
|
||||
store.state.settings.searchEnabled &&
|
||||
['channel', 'query'].includes(channel.type)
|
||||
"
|
||||
:network="network"
|
||||
|
@ -71,7 +71,7 @@
|
|||
<button
|
||||
class="rt"
|
||||
aria-label="Toggle user list"
|
||||
@click="$store.commit('toggleUserlist')"
|
||||
@click="store.commit('toggleUserlist')"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -95,7 +95,7 @@
|
|||
{'scroll-down-shown': !channel.scrolledToBottom},
|
||||
]"
|
||||
aria-label="Jump to recent messages"
|
||||
@click="$refs.messageList.jumpToBottom()"
|
||||
@click="messageList?.jumpToBottom()"
|
||||
>
|
||||
<div class="scroll-down-arrow" />
|
||||
</div>
|
||||
|
@ -110,11 +110,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="$store.state.currentUserVisibleError"
|
||||
v-if="store.state.currentUserVisibleError"
|
||||
id="user-visible-error"
|
||||
@click="hideUserVisibleError"
|
||||
>
|
||||
{{ $store.state.currentUserVisibleError }}
|
||||
{{ store.state.currentUserVisibleError }}
|
||||
</div>
|
||||
<ChatInput :network="network" :channel="channel" />
|
||||
</div>
|
||||
|
@ -133,8 +133,9 @@ import ListBans from "./Special/ListBans.vue";
|
|||
import ListInvites from "./Special/ListInvites.vue";
|
||||
import ListChannels from "./Special/ListChannels.vue";
|
||||
import ListIgnored from "./Special/ListIgnored.vue";
|
||||
import {Component, defineComponent, PropType} from "vue";
|
||||
import {defineComponent, PropType, ref, computed, watch, nextTick, onMounted, Component} from "vue";
|
||||
import type {ClientNetwork, ClientChan} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Chat",
|
||||
|
@ -151,92 +152,123 @@ export default defineComponent({
|
|||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
focused: String,
|
||||
},
|
||||
computed: {
|
||||
specialComponent(): Component {
|
||||
switch (this.channel.special) {
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
|
||||
const messageList = ref<typeof MessageList>();
|
||||
const topicInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const specialComponent = computed(() => {
|
||||
switch (props.channel.special) {
|
||||
case "list_bans":
|
||||
return ListBans;
|
||||
return ListBans as Component;
|
||||
case "list_invites":
|
||||
return ListInvites;
|
||||
return ListInvites as Component;
|
||||
case "list_channels":
|
||||
return ListChannels;
|
||||
return ListChannels as Component;
|
||||
case "list_ignored":
|
||||
return ListIgnored;
|
||||
return ListIgnored as Component;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
channel() {
|
||||
this.channelChanged();
|
||||
},
|
||||
"channel.editTopic"(newValue) {
|
||||
if (newValue) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.topicInput.focus();
|
||||
});
|
||||
|
||||
const channelChanged = () => {
|
||||
// Triggered when active channel is set or changed
|
||||
props.channel.highlight = 0;
|
||||
props.channel.unread = 0;
|
||||
|
||||
socket.emit("open", props.channel.id);
|
||||
|
||||
if (props.channel.usersOutdated) {
|
||||
props.channel.usersOutdated = false;
|
||||
|
||||
socket.emit("names", {
|
||||
target: props.channel.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const hideUserVisibleError = () => {
|
||||
store.commit("currentUserVisibleError", null);
|
||||
};
|
||||
|
||||
const editTopic = () => {
|
||||
if (props.channel.type === "channel") {
|
||||
props.channel.editTopic = true;
|
||||
}
|
||||
};
|
||||
|
||||
const saveTopic = () => {
|
||||
props.channel.editTopic = false;
|
||||
|
||||
if (!topicInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTopic = topicInput.value.value;
|
||||
|
||||
if (props.channel.topic !== newTopic) {
|
||||
const target = props.channel.id;
|
||||
const text = `/raw TOPIC ${props.channel.name} :${newTopic}`;
|
||||
socket.emit("input", {target, text});
|
||||
}
|
||||
};
|
||||
|
||||
const openContextMenu = (event: any) => {
|
||||
eventbus.emit("contextmenu:channel", {
|
||||
event: event,
|
||||
channel: props.channel,
|
||||
network: props.network,
|
||||
});
|
||||
};
|
||||
|
||||
const openMentions = (event: any) => {
|
||||
eventbus.emit("mentions:toggle", {
|
||||
event: event,
|
||||
});
|
||||
};
|
||||
|
||||
watch(props.channel, () => {
|
||||
channelChanged();
|
||||
});
|
||||
|
||||
const editTopicRef = ref(props.channel.editTopic);
|
||||
watch(editTopicRef, (newTopic) => {
|
||||
if (newTopic) {
|
||||
nextTick(() => {
|
||||
topicInput.value?.focus();
|
||||
}).catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.channelChanged();
|
||||
});
|
||||
|
||||
if (this.channel.editTopic) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.topicInput.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
channelChanged() {
|
||||
// Triggered when active channel is set or changed
|
||||
this.channel.highlight = 0;
|
||||
this.channel.unread = 0;
|
||||
onMounted(() => {
|
||||
channelChanged();
|
||||
|
||||
socket.emit("open", this.channel.id);
|
||||
|
||||
if (this.channel.usersOutdated) {
|
||||
this.channel.usersOutdated = false;
|
||||
|
||||
socket.emit("names", {
|
||||
target: this.channel.id,
|
||||
if (props.channel.editTopic) {
|
||||
nextTick(() => {
|
||||
topicInput.value?.focus();
|
||||
}).catch(() => {
|
||||
// no-op
|
||||
});
|
||||
}
|
||||
},
|
||||
hideUserVisibleError() {
|
||||
this.$store.commit("currentUserVisibleError", null);
|
||||
},
|
||||
editTopic() {
|
||||
if (this.channel.type === "channel") {
|
||||
this.channel.editTopic = true;
|
||||
}
|
||||
},
|
||||
saveTopic() {
|
||||
this.channel.editTopic = false;
|
||||
const newTopic = this.$refs.topicInput.value;
|
||||
});
|
||||
|
||||
if (this.channel.topic !== newTopic) {
|
||||
const target = this.channel.id;
|
||||
const text = `/raw TOPIC ${this.channel.name} :${newTopic}`;
|
||||
socket.emit("input", {target, text});
|
||||
}
|
||||
},
|
||||
openContextMenu(event) {
|
||||
eventbus.emit("contextmenu:channel", {
|
||||
event: event,
|
||||
channel: this.channel,
|
||||
network: this.network,
|
||||
});
|
||||
},
|
||||
openMentions(event) {
|
||||
eventbus.emit("mentions:toggle", {
|
||||
event: event,
|
||||
});
|
||||
},
|
||||
return {
|
||||
store,
|
||||
messageList,
|
||||
topicInput,
|
||||
specialComponent,
|
||||
editTopicRef,
|
||||
hideUserVisibleError,
|
||||
editTopic,
|
||||
saveTopic,
|
||||
openContextMenu,
|
||||
openMentions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
@blur="onBlur"
|
||||
/>
|
||||
<span
|
||||
v-if="$store.state.serverConfiguration?.fileUpload"
|
||||
v-if="store.state.serverConfiguration?.fileUpload"
|
||||
id="upload-tooltip"
|
||||
class="tooltipped tooltipped-w tooltipped-no-touch"
|
||||
aria-label="Upload file"
|
||||
|
@ -34,7 +34,7 @@
|
|||
id="upload"
|
||||
type="button"
|
||||
aria-label="Upload file"
|
||||
:disabled="!$store.state.isConnected"
|
||||
:disabled="!store.state.isConnected"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
|
@ -46,7 +46,7 @@
|
|||
id="submit"
|
||||
type="submit"
|
||||
aria-label="Send message"
|
||||
:disabled="!$store.state.isConnected"
|
||||
:disabled="!store.state.isConnected"
|
||||
/>
|
||||
</span>
|
||||
</form>
|
||||
|
@ -60,8 +60,9 @@ import commands from "../js/commands/index";
|
|||
import socket from "../js/socket";
|
||||
import upload from "../js/upload";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {watch, defineComponent, nextTick, onMounted, PropType, ref, onUnmounted} from "vue";
|
||||
import type {ClientNetwork, ClientChan} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
|
||||
const formattingHotkeys = {
|
||||
"mod+k": "\x03",
|
||||
|
@ -88,178 +89,103 @@ const bracketWraps = {
|
|||
_: "_",
|
||||
};
|
||||
|
||||
let autocompletionRef = null;
|
||||
|
||||
export default defineComponent({
|
||||
name: "ChatInput",
|
||||
props: {
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
},
|
||||
watch: {
|
||||
"channel.id"() {
|
||||
if (autocompletionRef) {
|
||||
autocompletionRef.hide();
|
||||
}
|
||||
},
|
||||
"channel.pendingMessage"() {
|
||||
this.setInputSize();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
eventbus.on("escapekey", this.blurInput);
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
const input = ref<HTMLTextAreaElement>();
|
||||
const uploadInput = ref<HTMLInputElement>();
|
||||
const autocompletionRef = ref<ReturnType<typeof autocompletion>>();
|
||||
|
||||
if (this.$accessor.settings.autocomplete) {
|
||||
autocompletionRef = autocompletion(this.$refs.input);
|
||||
}
|
||||
|
||||
const inputTrap = Mousetrap(this.$refs.input);
|
||||
|
||||
inputTrap.bind(Object.keys(formattingHotkeys), function (e, key) {
|
||||
const modifier = formattingHotkeys[key];
|
||||
|
||||
wrapCursor(
|
||||
e.target,
|
||||
modifier,
|
||||
e.target.selectionStart === e.target.selectionEnd ? "" : modifier
|
||||
);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
inputTrap.bind(Object.keys(bracketWraps), function (e, key) {
|
||||
if (e.target?.selectionStart !== e.target.selectionEnd) {
|
||||
wrapCursor(e.target, key, bracketWraps[key]);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
inputTrap.bind(["up", "down"], (e, key) => {
|
||||
if (
|
||||
this.$accessor.isAutoCompleting ||
|
||||
e.target.selectionStart !== e.target.selectionEnd
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onRow = (
|
||||
this.$refs.input.value.slice(null, this.$refs.input.selectionStart).match(/\n/g) ||
|
||||
[]
|
||||
).length;
|
||||
const totalRows = (this.$refs.input.value.match(/\n/g) || []).length;
|
||||
|
||||
const {channel} = this;
|
||||
|
||||
if (channel.inputHistoryPosition === 0) {
|
||||
channel.inputHistory[channel.inputHistoryPosition] = channel.pendingMessage;
|
||||
}
|
||||
|
||||
if (key === "up" && onRow === 0) {
|
||||
if (channel.inputHistoryPosition < channel.inputHistory.length - 1) {
|
||||
channel.inputHistoryPosition++;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (key === "down" && channel.inputHistoryPosition > 0 && onRow === totalRows) {
|
||||
channel.inputHistoryPosition--;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
channel.pendingMessage = channel.inputHistory[channel.inputHistoryPosition];
|
||||
this.$refs.input.value = channel.pendingMessage;
|
||||
this.setInputSize();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (this.$accessor.serverConfiguration.fileUpload) {
|
||||
upload.mounted();
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
eventbus.off("escapekey", this.blurInput);
|
||||
|
||||
if (autocompletionRef) {
|
||||
autocompletionRef.destroy();
|
||||
autocompletionRef = null;
|
||||
}
|
||||
|
||||
upload.abort();
|
||||
},
|
||||
methods: {
|
||||
setPendingMessage(e) {
|
||||
this.channel.pendingMessage = e.target.value;
|
||||
this.channel.inputHistoryPosition = 0;
|
||||
this.setInputSize();
|
||||
},
|
||||
setInputSize() {
|
||||
this.$nextTick(() => {
|
||||
if (!this.$refs.input) {
|
||||
const setInputSize = () => {
|
||||
nextTick(() => {
|
||||
if (!input.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(this.$refs.input);
|
||||
const lineHeight = parseFloat(style.lineHeight, 10) || 1;
|
||||
const style = window.getComputedStyle(input.value);
|
||||
const lineHeight = parseFloat(style.lineHeight) || 1;
|
||||
|
||||
// Start by resetting height before computing as scrollHeight does not
|
||||
// decrease when deleting characters
|
||||
this.$refs.input.style.height = "";
|
||||
input.value.style.height = "";
|
||||
|
||||
// Use scrollHeight to calculate how many lines there are in input, and ceil the value
|
||||
// because some browsers tend to incorrently round the values when using high density
|
||||
// displays or using page zoom feature
|
||||
this.$refs.input.style.height =
|
||||
Math.ceil(this.$refs.input.scrollHeight / lineHeight) * lineHeight + "px";
|
||||
input.value.style.height = `${
|
||||
Math.ceil(input.value.scrollHeight / lineHeight) * lineHeight
|
||||
}px`;
|
||||
}).catch(() => {
|
||||
// no-op
|
||||
});
|
||||
},
|
||||
getInputPlaceholder(channel) {
|
||||
};
|
||||
|
||||
const setPendingMessage = (e: Event) => {
|
||||
props.channel.pendingMessage = (e.target as HTMLInputElement).value;
|
||||
props.channel.inputHistoryPosition = 0;
|
||||
setInputSize();
|
||||
};
|
||||
|
||||
const getInputPlaceholder = (channel: ClientChan) => {
|
||||
if (channel.type === "channel" || channel.type === "query") {
|
||||
return `Write to ${channel.name}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
onSubmit() {
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
if (!input.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Triggering click event opens the virtual keyboard on mobile
|
||||
// This can only be called from another interactive event (e.g. button click)
|
||||
this.$refs.input.click();
|
||||
this.$refs.input.focus();
|
||||
input.value.click();
|
||||
input.value.focus();
|
||||
|
||||
if (!this.$accessor.isConnected) {
|
||||
if (!store.state.isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = this.channel.id;
|
||||
const text = this.channel.pendingMessage;
|
||||
const target = props.channel.id;
|
||||
const text = props.channel.pendingMessage;
|
||||
|
||||
if (text.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (autocompletionRef) {
|
||||
autocompletionRef.hide();
|
||||
if (autocompletionRef.value) {
|
||||
autocompletionRef.value.hide();
|
||||
}
|
||||
|
||||
this.channel.inputHistoryPosition = 0;
|
||||
this.channel.pendingMessage = "";
|
||||
this.$refs.input.value = "";
|
||||
this.setInputSize();
|
||||
props.channel.inputHistoryPosition = 0;
|
||||
props.channel.pendingMessage = "";
|
||||
input.value.value = "";
|
||||
setInputSize();
|
||||
|
||||
// Store new message in history if last message isn't already equal
|
||||
if (this.channel.inputHistory[1] !== text) {
|
||||
this.channel.inputHistory.splice(1, 0, text);
|
||||
if (props.channel.inputHistory[1] !== text) {
|
||||
props.channel.inputHistory.splice(1, 0, text);
|
||||
}
|
||||
|
||||
// Limit input history to a 100 entries
|
||||
if (this.channel.inputHistory.length > 100) {
|
||||
this.channel.inputHistory.pop();
|
||||
if (props.channel.inputHistory.length > 100) {
|
||||
props.channel.inputHistory.pop();
|
||||
}
|
||||
|
||||
if (text[0] === "/") {
|
||||
const args = text.substr(1).split(" ");
|
||||
const cmd = args.shift().toLowerCase();
|
||||
const args = text.substring(1).split(" ");
|
||||
const cmd = args.shift()?.toLowerCase();
|
||||
|
||||
if (!cmd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(commands, cmd) &&
|
||||
|
@ -270,23 +196,165 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
socket.emit("input", {target, text});
|
||||
},
|
||||
onUploadInputChange() {
|
||||
const files = Array.from(this.$refs.uploadInput.files);
|
||||
upload.triggerUpload(files);
|
||||
this.$refs.uploadInput.value = ""; // Reset <input> element so you can upload the same file
|
||||
},
|
||||
openFileUpload() {
|
||||
this.$refs.uploadInput.click();
|
||||
},
|
||||
blurInput() {
|
||||
this.$refs.input.blur();
|
||||
},
|
||||
onBlur() {
|
||||
if (autocompletionRef) {
|
||||
autocompletionRef.hide();
|
||||
};
|
||||
|
||||
const onUploadInputChange = () => {
|
||||
if (!uploadInput.value || !uploadInput.value.files) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
const files = Array.from(uploadInput.value.files);
|
||||
upload.triggerUpload(files);
|
||||
uploadInput.value.value = ""; // Reset <input> element so you can upload the same file
|
||||
};
|
||||
|
||||
const openFileUpload = () => {
|
||||
uploadInput.value?.click();
|
||||
};
|
||||
|
||||
const blurInput = () => {
|
||||
input.value?.blur();
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
if (autocompletionRef.value) {
|
||||
autocompletionRef.value.hide();
|
||||
}
|
||||
};
|
||||
|
||||
const channelId = ref(props.channel.id);
|
||||
watch(channelId, () => {
|
||||
if (autocompletionRef.value) {
|
||||
autocompletionRef.value.hide();
|
||||
}
|
||||
});
|
||||
|
||||
const pendingMessage = ref(props.channel.pendingMessage);
|
||||
watch(pendingMessage, () => {
|
||||
setInputSize();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
eventbus.on("escapekey", blurInput);
|
||||
|
||||
if (store.state.settings.autocomplete) {
|
||||
if (!input.value) {
|
||||
throw new Error("ChatInput autocomplete: input element is not available");
|
||||
}
|
||||
|
||||
autocompletionRef.value = autocompletion(input.value);
|
||||
}
|
||||
|
||||
const inputTrap = Mousetrap(input.value);
|
||||
|
||||
inputTrap.bind(Object.keys(formattingHotkeys), function (e, key) {
|
||||
const modifier = formattingHotkeys[key];
|
||||
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO; investigate types
|
||||
wrapCursor(
|
||||
e.target as HTMLTextAreaElement,
|
||||
modifier,
|
||||
(e.target as HTMLTextAreaElement).selectionStart ===
|
||||
(e.target as HTMLTextAreaElement).selectionEnd
|
||||
? ""
|
||||
: modifier
|
||||
);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
inputTrap.bind(Object.keys(bracketWraps), function (e, key) {
|
||||
if (
|
||||
(e.target as HTMLTextAreaElement)?.selectionStart !==
|
||||
(e.target as HTMLTextAreaElement).selectionEnd
|
||||
) {
|
||||
wrapCursor(e.target as HTMLTextAreaElement, key, bracketWraps[key]);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
inputTrap.bind(["up", "down"], (e, key) => {
|
||||
if (
|
||||
store.state.isAutoCompleting ||
|
||||
(e.target as HTMLTextAreaElement).selectionStart !==
|
||||
(e.target as HTMLTextAreaElement).selectionEnd ||
|
||||
!input.value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onRow = (
|
||||
input.value.value.slice(undefined, input.value.selectionStart).match(/\n/g) ||
|
||||
[]
|
||||
).length;
|
||||
const totalRows = (input.value.value.match(/\n/g) || []).length;
|
||||
|
||||
const {channel} = props;
|
||||
|
||||
if (channel.inputHistoryPosition === 0) {
|
||||
channel.inputHistory[channel.inputHistoryPosition] = channel.pendingMessage;
|
||||
}
|
||||
|
||||
if (key === "up" && onRow === 0) {
|
||||
if (channel.inputHistoryPosition < channel.inputHistory.length - 1) {
|
||||
channel.inputHistoryPosition++;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (
|
||||
key === "down" &&
|
||||
channel.inputHistoryPosition > 0 &&
|
||||
onRow === totalRows
|
||||
) {
|
||||
channel.inputHistoryPosition--;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
channel.pendingMessage = channel.inputHistory[channel.inputHistoryPosition];
|
||||
input.value.value = channel.pendingMessage;
|
||||
setInputSize();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (store.state.serverConfiguration?.fileUpload) {
|
||||
upload.mounted();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
eventbus.off("escapekey", blurInput);
|
||||
|
||||
if (autocompletionRef.value) {
|
||||
autocompletionRef.value.destroy();
|
||||
autocompletionRef.value = undefined;
|
||||
}
|
||||
|
||||
upload.abort();
|
||||
});
|
||||
|
||||
return {
|
||||
store,
|
||||
input,
|
||||
uploadInput,
|
||||
onUploadInputChange,
|
||||
openFileUpload,
|
||||
blurInput,
|
||||
onBlur,
|
||||
channelId,
|
||||
pendingMessage,
|
||||
setInputSize,
|
||||
upload,
|
||||
getInputPlaceholder,
|
||||
onSubmit,
|
||||
setPendingMessage,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -33,7 +33,7 @@ export default defineComponent({
|
|||
const localeDate = computed(() => dayjs(props.message.time).format("D MMMM YYYY"));
|
||||
|
||||
const hoursPassed = () => {
|
||||
return (Date.now() - Date.parse(props.message.time?.toISOString())) / 3600000;
|
||||
return (Date.now() - Date.parse(props.message.time.toString())) / 3600000;
|
||||
};
|
||||
|
||||
const dayChange = () => {
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
import Mousetrap from "mousetrap";
|
||||
import {computed, defineComponent, ref, watch} from "vue";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {ClientChan, ClientMessage, LinkPreview} from "../js/types";
|
||||
import {ClientChan, ClientMessage, ClientLinkPreview} from "../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ImageViewer",
|
||||
|
@ -50,9 +50,9 @@ export default defineComponent({
|
|||
const viewer = ref<HTMLDivElement>();
|
||||
const image = ref<HTMLImageElement>();
|
||||
|
||||
const link = ref<LinkPreview | null>(null);
|
||||
const previousImage = ref<LinkPreview | null>();
|
||||
const nextImage = ref<LinkPreview | null>();
|
||||
const link = ref<ClientLinkPreview | null>(null);
|
||||
const previousImage = ref<ClientLinkPreview | null>();
|
||||
const nextImage = ref<ClientLinkPreview | null>();
|
||||
const channel = ref<ClientChan | null>();
|
||||
|
||||
const position = ref<{
|
||||
|
@ -98,7 +98,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
const setPrevNextImages = () => {
|
||||
if (!channel.value) {
|
||||
if (!channel.value || !link.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -107,7 +107,7 @@ export default defineComponent({
|
|||
.flat()
|
||||
.filter((preview) => preview.thumb);
|
||||
|
||||
const currentIndex = links.indexOf(this.link);
|
||||
const currentIndex = links.indexOf(link.value);
|
||||
|
||||
previousImage.value = links[currentIndex - 1] || null;
|
||||
nextImage.value = links[currentIndex + 1] || null;
|
||||
|
@ -125,10 +125,6 @@ export default defineComponent({
|
|||
}
|
||||
};
|
||||
|
||||
const onImageLoad = () => {
|
||||
prepareImage();
|
||||
};
|
||||
|
||||
const prepareImage = () => {
|
||||
const viewerEl = viewer.value;
|
||||
const imageEl = image.value;
|
||||
|
@ -148,6 +144,10 @@ export default defineComponent({
|
|||
transform.value.y = height / 2;
|
||||
};
|
||||
|
||||
const onImageLoad = () => {
|
||||
prepareImage();
|
||||
};
|
||||
|
||||
const calculateZoomShift = (newScale: number, x: number, y: number, oldScale: number) => {
|
||||
if (!image.value || !viewer.value) {
|
||||
return;
|
||||
|
@ -241,7 +241,7 @@ export default defineComponent({
|
|||
// 1. Move around by dragging it with one finger
|
||||
// 2. Change image scale by using two fingers
|
||||
const onImageTouchStart = (e: TouchEvent) => {
|
||||
const image = this.$refs.image;
|
||||
const img = image.value;
|
||||
let touch = reduceTouches(e.touches);
|
||||
let currentTouches = e.touches;
|
||||
let touchEndFingers = 0;
|
||||
|
@ -313,12 +313,12 @@ export default defineComponent({
|
|||
|
||||
correctPosition();
|
||||
|
||||
image.removeEventListener("touchmove", touchMove, {passive: true});
|
||||
image.removeEventListener("touchend", touchEnd, {passive: true});
|
||||
img?.removeEventListener("touchmove", touchMove);
|
||||
img?.removeEventListener("touchend", touchEnd);
|
||||
};
|
||||
|
||||
image.addEventListener("touchmove", touchMove, {passive: true});
|
||||
image.addEventListener("touchend", touchEnd, {passive: true});
|
||||
img?.addEventListener("touchmove", touchMove, {passive: true});
|
||||
img?.addEventListener("touchend", touchEnd, {passive: true});
|
||||
};
|
||||
|
||||
// Image mouse manipulation:
|
||||
|
|
|
@ -130,144 +130,177 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import Vue, {
|
||||
computed,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
PropType,
|
||||
ref,
|
||||
inject,
|
||||
Ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import eventbus from "../js/eventbus";
|
||||
import friendlysize from "../js/helpers/friendlysize";
|
||||
import type {ClientChan} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
import type {ClientChan, ClientLinkPreview} from "../js/types";
|
||||
import {useImageViewer} from "./App.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "LinkPreview",
|
||||
props: {
|
||||
link: {
|
||||
type: Object,
|
||||
type: Object as PropType<ClientLinkPreview>,
|
||||
required: true,
|
||||
},
|
||||
keepScrollPosition: {
|
||||
type: Function as PropType<() => void>,
|
||||
required: true,
|
||||
},
|
||||
keepScrollPosition: Function,
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMoreButton: false,
|
||||
isContentShown: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
moreButtonLabel(): string {
|
||||
return this.isContentShown ? "Less" : "More";
|
||||
},
|
||||
imageMaxSize(): string | undefined {
|
||||
if (!this.link.maxSize) {
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
|
||||
const showMoreButton = ref(false);
|
||||
const isContentShown = ref(false);
|
||||
|
||||
const content = ref<HTMLDivElement | null>(null);
|
||||
const container = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const moreButtonLabel = computed(() => {
|
||||
return isContentShown.value ? "Less" : "More";
|
||||
});
|
||||
|
||||
// TODO: type
|
||||
const imageMaxSize = computed(() => {
|
||||
// @ts-ignore
|
||||
if (!props.link.maxSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
return friendlysize(this.link.maxSize);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"link.type"() {
|
||||
this.updateShownState();
|
||||
this.onPreviewUpdate();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.updateShownState();
|
||||
},
|
||||
mounted() {
|
||||
eventbus.on("resize", this.handleResize);
|
||||
// @ts-ignore
|
||||
return friendlysize(props.link.maxSize);
|
||||
});
|
||||
|
||||
this.onPreviewUpdate();
|
||||
},
|
||||
beforeUnmount() {
|
||||
eventbus.off("resize", this.handleResize);
|
||||
},
|
||||
unmounted() {
|
||||
// Let this preview go through load/canplay events again,
|
||||
// Otherwise the browser can cause a resize on video elements
|
||||
this.link.sourceLoaded = false;
|
||||
},
|
||||
methods: {
|
||||
onPreviewUpdate() {
|
||||
// Don't display previews while they are loading on the server
|
||||
if (this.link.type === "loading") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Error does not have any media to render
|
||||
if (this.link.type === "error") {
|
||||
this.onPreviewReady();
|
||||
}
|
||||
|
||||
// If link doesn't have a thumbnail, render it
|
||||
if (this.link.type === "link") {
|
||||
this.handleResize();
|
||||
this.keepScrollPosition();
|
||||
}
|
||||
},
|
||||
onPreviewReady() {
|
||||
this.$set(this.link, "sourceLoaded", true);
|
||||
|
||||
this.keepScrollPosition();
|
||||
|
||||
if (this.link.type === "link") {
|
||||
this.handleResize();
|
||||
}
|
||||
},
|
||||
onThumbnailError() {
|
||||
// If thumbnail fails to load, hide it and show the preview without it
|
||||
this.link.thumb = "";
|
||||
this.onPreviewReady();
|
||||
},
|
||||
onThumbnailClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const imageViewer = this.$root.$refs.app.$refs.imageViewer;
|
||||
imageViewer.channel = this.channel;
|
||||
imageViewer.link = this.link;
|
||||
},
|
||||
onMoreClick() {
|
||||
this.isContentShown = !this.isContentShown;
|
||||
this.keepScrollPosition();
|
||||
},
|
||||
handleResize() {
|
||||
this.$nextTick(() => {
|
||||
if (!this.$refs.content) {
|
||||
const handleResize = () => {
|
||||
nextTick(() => {
|
||||
if (!content.value || !container.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showMoreButton =
|
||||
this.$refs.content.offsetWidth >= this.$refs.container.offsetWidth;
|
||||
showMoreButton.value = content.value.offsetWidth >= container.value.offsetWidth;
|
||||
}).catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error in LinkPreview.handleResize", e);
|
||||
});
|
||||
},
|
||||
updateShownState() {
|
||||
};
|
||||
|
||||
const onPreviewReady = () => {
|
||||
props.link.sourceLoaded = true;
|
||||
|
||||
props.keepScrollPosition();
|
||||
|
||||
if (props.link.type === "link") {
|
||||
handleResize();
|
||||
}
|
||||
};
|
||||
|
||||
const onPreviewUpdate = () => {
|
||||
// Don't display previews while they are loading on the server
|
||||
if (props.link.type === "loading") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Error does not have any media to render
|
||||
if (props.link.type === "error") {
|
||||
onPreviewReady();
|
||||
}
|
||||
|
||||
// If link doesn't have a thumbnail, render it
|
||||
if (props.link.type === "link") {
|
||||
handleResize();
|
||||
props.keepScrollPosition();
|
||||
}
|
||||
};
|
||||
|
||||
const onThumbnailError = () => {
|
||||
// If thumbnail fails to load, hide it and show the preview without it
|
||||
props.link.thumb = "";
|
||||
onPreviewReady();
|
||||
};
|
||||
|
||||
const onThumbnailClick = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
if (!imageViewer.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageViewer.value.channel = props.channel;
|
||||
imageViewer.value.link = props.link;
|
||||
};
|
||||
|
||||
const onMoreClick = () => {
|
||||
isContentShown.value = !isContentShown.value;
|
||||
props.keepScrollPosition();
|
||||
};
|
||||
|
||||
const updateShownState = () => {
|
||||
// User has manually toggled the preview, do not apply default
|
||||
if (this.link.shown !== null) {
|
||||
if (props.link.shown !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let defaultState = false;
|
||||
|
||||
switch (this.link.type) {
|
||||
switch (props.link.type) {
|
||||
case "error":
|
||||
// Collapse all errors by default unless its a message about image being too big
|
||||
if (this.link.error === "image-too-big") {
|
||||
defaultState = this.$accessor.settings.media;
|
||||
if (props.link.error === "image-too-big") {
|
||||
defaultState = store.state.settings.media;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "link":
|
||||
defaultState = this.$accessor.settings.links;
|
||||
defaultState = store.state.settings.links;
|
||||
break;
|
||||
|
||||
default:
|
||||
defaultState = this.$accessor.settings.media;
|
||||
defaultState = store.state.settings.media;
|
||||
}
|
||||
|
||||
this.link.shown = defaultState;
|
||||
},
|
||||
props.link.shown = defaultState;
|
||||
};
|
||||
|
||||
updateShownState();
|
||||
|
||||
const linkTypeRef = ref(props.link.type);
|
||||
watch(linkTypeRef, () => {
|
||||
updateShownState();
|
||||
onPreviewUpdate();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
eventbus.on("resize", handleResize);
|
||||
|
||||
onPreviewUpdate();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventbus.off("resize", handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// Let this preview go through load/canplay events again,
|
||||
// Otherwise the browser can cause a resize on video elements
|
||||
props.link.sourceLoaded = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -9,12 +9,12 @@
|
|||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import {ClientMessage, LinkPreview} from "../js/types";
|
||||
import {ClientMessage, ClientLinkPreview} from "../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "LinkPreviewToggle",
|
||||
props: {
|
||||
link: {type: Object as PropType<LinkPreview>, required: true},
|
||||
link: {type: Object as PropType<ClientLinkPreview>, required: true},
|
||||
message: {type: Object as PropType<ClientMessage>, required: true},
|
||||
},
|
||||
emits: ["toggle-link-preview"],
|
||||
|
|
|
@ -78,7 +78,8 @@ import {
|
|||
watch,
|
||||
} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
import type {ClientChan, ClientMessage, ClientNetwork, LinkPreview} from "../js/types";
|
||||
import {ClientChan, ClientMessage, ClientNetwork, ClientLinkPreview} from "../js/types";
|
||||
import Msg from "../../src/models/msg";
|
||||
|
||||
type CondensedMessageContainer = {
|
||||
type: "condensed";
|
||||
|
@ -107,8 +108,8 @@ export default defineComponent({
|
|||
const historyObserver = ref<IntersectionObserver | null>(null);
|
||||
const skipNextScrollEvent = ref(false);
|
||||
const unreadMarkerShown = ref(false);
|
||||
// TODO: make this a ref?
|
||||
let isWaitingForNextTick = false;
|
||||
|
||||
const isWaitingForNextTick = ref(false);
|
||||
|
||||
const jumpToBottom = () => {
|
||||
skipNextScrollEvent.value = true;
|
||||
|
@ -243,7 +244,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const shouldDisplayDateMarker = (
|
||||
message: ClientMessage | CondensedMessageContainer,
|
||||
message: Msg | ClientMessage | CondensedMessageContainer,
|
||||
id: number
|
||||
) => {
|
||||
const previousMessage = condensedMessages[id - 1];
|
||||
|
@ -271,7 +272,7 @@ export default defineComponent({
|
|||
return false;
|
||||
};
|
||||
|
||||
const isPreviousSource = (currentMessage: ClientMessage, id: number) => {
|
||||
const isPreviousSource = (currentMessage: ClientMessage | Msg, id: number) => {
|
||||
const previousMessage = condensedMessages[id - 1];
|
||||
return !!(
|
||||
previousMessage &&
|
||||
|
@ -291,7 +292,7 @@ export default defineComponent({
|
|||
const keepScrollPosition = () => {
|
||||
// If we are already waiting for the next tick to force scroll position,
|
||||
// we have no reason to perform more checks and set it again in the next tick
|
||||
if (isWaitingForNextTick) {
|
||||
if (isWaitingForNextTick.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -305,10 +306,10 @@ export default defineComponent({
|
|||
if (props.channel.historyLoading) {
|
||||
const heightOld = el.scrollHeight - el.scrollTop;
|
||||
|
||||
isWaitingForNextTick = true;
|
||||
isWaitingForNextTick.value = true;
|
||||
|
||||
nextTick(() => {
|
||||
isWaitingForNextTick = false;
|
||||
isWaitingForNextTick.value = false;
|
||||
skipNextScrollEvent.value = true;
|
||||
el.scrollTop = el.scrollHeight - heightOld;
|
||||
}).catch(() => {
|
||||
|
@ -319,16 +320,16 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
isWaitingForNextTick = true;
|
||||
isWaitingForNextTick.value = true;
|
||||
nextTick(() => {
|
||||
isWaitingForNextTick = false;
|
||||
isWaitingForNextTick.value = false;
|
||||
jumpToBottom();
|
||||
}).catch(() => {
|
||||
// no-op
|
||||
});
|
||||
};
|
||||
|
||||
const onLinkPreviewToggle = (preview: LinkPreview, message: ClientMessage) => {
|
||||
const onLinkPreviewToggle = (preview: ClientLinkPreview, message: ClientMessage) => {
|
||||
keepScrollPosition();
|
||||
|
||||
// Tell the server we're toggling so it remembers at page reload
|
||||
|
|
|
@ -199,7 +199,7 @@
|
|||
import {computed, watch, defineComponent, nextTick, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
|
||||
import Mousetrap from "mousetrap";
|
||||
import Draggable from "vuedraggable";
|
||||
import {VueDraggableNext} from "vue-draggable-next";
|
||||
import {filter as fuzzyFilter} from "fuzzy";
|
||||
import NetworkLobby from "./NetworkLobby.vue";
|
||||
import Channel from "./Channel.vue";
|
||||
|
@ -220,7 +220,7 @@ export default defineComponent({
|
|||
JoinChannel,
|
||||
NetworkLobby,
|
||||
Channel,
|
||||
Draggable,
|
||||
Draggable: VueDraggableNext,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
|
@ -280,7 +280,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
if (store.state.activeChannel) {
|
||||
collapseNetwork(store.state.activeChannel.network, false);
|
||||
collapseNetworkHelper(store.state.activeChannel.network, false);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -533,7 +533,7 @@ export default defineComponent({
|
|||
scrollToActive,
|
||||
selectResult,
|
||||
navigateResults,
|
||||
|
||||
onChannelSort,
|
||||
onNetworkSort,
|
||||
onDraggableTouchStart,
|
||||
onDraggableTouchMove,
|
||||
|
|
|
@ -12,17 +12,14 @@ export default defineComponent({
|
|||
network: {type: Object as PropType<ClientNetwork>, required: false},
|
||||
},
|
||||
setup(props) {
|
||||
const render = () => {
|
||||
return parse(
|
||||
typeof props.text !== "undefined" ? props.text : props.message!.text,
|
||||
props.message,
|
||||
props.network
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
render,
|
||||
};
|
||||
//
|
||||
},
|
||||
render(context) {
|
||||
return parse(
|
||||
typeof context.text !== "undefined" ? context.text : context.message.text,
|
||||
context.message,
|
||||
context.network
|
||||
);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -161,7 +161,7 @@ export default defineComponent({
|
|||
name: "NotificationSettings",
|
||||
setup() {
|
||||
const store = useStore();
|
||||
|
||||
console.log(store);
|
||||
const isIOS = computed(
|
||||
() =>
|
||||
[
|
||||
|
|
|
@ -91,7 +91,7 @@ export default defineComponent({
|
|||
NetworkList,
|
||||
},
|
||||
props: {
|
||||
overlay: {type: Object as PropType<HTMLElement>, required: true},
|
||||
overlay: {type: Object as PropType<HTMLElement | null>, required: true},
|
||||
},
|
||||
setup(props) {
|
||||
const isDevelopment = process.env.NODE_ENV !== "production";
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
<template>
|
||||
<button class="lt" aria-label="Toggle channel list" @click="$store.commit('toggleSidebar')" />
|
||||
<button class="lt" aria-label="Toggle channel list" @click="store.commit('toggleSidebar')" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SidebarToggle",
|
||||
setup() {
|
||||
const store = useStore();
|
||||
|
||||
return {
|
||||
store,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -160,11 +160,11 @@ router.afterEach((to) => {
|
|||
}
|
||||
|
||||
// When switching out of a channel, mark everything as read
|
||||
if (channel.messages.length > 0) {
|
||||
if (channel.messages?.length > 0) {
|
||||
channel.firstUnread = channel.messages[channel.messages.length - 1].id;
|
||||
}
|
||||
|
||||
if (channel.messages.length > 100) {
|
||||
if (channel.messages?.length > 100) {
|
||||
channel.messages.splice(0, channel.messages.length - 100);
|
||||
channel.moreHistoryAvailable = true;
|
||||
}
|
||||
|
@ -172,7 +172,7 @@ router.afterEach((to) => {
|
|||
});
|
||||
|
||||
function navigate(routeName: string, params: any = {}) {
|
||||
if (router.currentRoute.name) {
|
||||
if (router.currentRoute.value.name) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
router.push({name: routeName, params}).catch(() => {});
|
||||
} else {
|
||||
|
|
15
client/js/types.d.ts
vendored
15
client/js/types.d.ts
vendored
|
@ -27,10 +27,15 @@ type ClientUser = User & {
|
|||
//
|
||||
};
|
||||
|
||||
type ClientChan = Omit<Chan, "users"> & {
|
||||
type ClientMessage = Message & {
|
||||
time: number;
|
||||
};
|
||||
|
||||
type ClientChan = Omit<Chan, "users" | "messages"> & {
|
||||
moreHistoryAvailable: boolean;
|
||||
editTopic: boolean;
|
||||
users: ClientUser[];
|
||||
messages: ClientMessage[];
|
||||
|
||||
// these are added in store/initChannel
|
||||
pendingMessage: string;
|
||||
|
@ -53,10 +58,6 @@ type ClientNetwork = Omit<Network, "channels"> & {
|
|||
channels: ClientChan[];
|
||||
};
|
||||
|
||||
type ClientMessage = Message & {
|
||||
//
|
||||
};
|
||||
|
||||
type NetChan = {
|
||||
channel: ClientChan;
|
||||
network: ClientNetwork;
|
||||
|
@ -68,7 +69,9 @@ type ClientMention = Mention & {
|
|||
channel: NetChan | null;
|
||||
};
|
||||
|
||||
type LinkPreview = LinkPreview;
|
||||
type ClientLinkPreview = LinkPreview & {
|
||||
sourceLoaded?: boolean;
|
||||
};
|
||||
|
||||
declare module "*.vue" {
|
||||
const Component: ReturnType<typeof defineComponent>;
|
||||
|
|
|
@ -83,7 +83,3 @@ VueApp.config.errorHandler = function (e) {
|
|||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
};
|
||||
|
||||
VueApp.config.globalProperties = {
|
||||
$store: store as TypedStore,
|
||||
};
|
||||
|
|
|
@ -28,7 +28,7 @@ export type LinkPreview = {
|
|||
size: number;
|
||||
link: string; // Send original matched link to the client
|
||||
shown: boolean | null;
|
||||
error: undefined | any;
|
||||
error: undefined | string;
|
||||
message: undefined | string;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue