mirror of
https://github.com/thelounge/thelounge.git
synced 2024-06-10 09:42:18 +02:00
Fix image viewer, reset parse typings for now, fix loading messages on chan switch
This commit is contained in:
parent
4740d1d574
commit
9a57e218b4
|
@ -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>
|
||||
|
@ -35,30 +35,6 @@ import useCloseChannel from "../js/hooks/use-close-channel";
|
|||
import {ClientChan, ClientNetwork} from "../js/types";
|
||||
import ChannelWrapper from "./ChannelWrapper.vue";
|
||||
|
||||
// export default defineComponent({
|
||||
// name: "Channel",
|
||||
// components: {
|
||||
// ChannelWrapper,
|
||||
// },
|
||||
// props: {
|
||||
// network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
// channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
// active: Boolean,
|
||||
// isFiltering: Boolean,
|
||||
// },
|
||||
// computed: {
|
||||
// unreadCount(): string {
|
||||
// return roundBadgeNumber(this.channel.unread);
|
||||
// },
|
||||
// },
|
||||
// methods: {
|
||||
// close(): void {
|
||||
// this.$root?.closeChannel(this.channel);
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
//
|
||||
|
||||
export default defineComponent({
|
||||
name: "Channel",
|
||||
components: {
|
||||
|
|
|
@ -42,7 +42,6 @@ import {switchToChannel} from "../js/router";
|
|||
|
||||
export default defineComponent({
|
||||
name: "ChannelWrapper",
|
||||
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
|
@ -82,7 +81,7 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
return `${type}: ${props.channel.name} ${extra.length ? `(${extra.join(", ")})` : ""}`;
|
||||
return `${type}: ${props.channel.name}${extra.length ? `(${extra.join(", ")})` : ""}`;
|
||||
};
|
||||
|
||||
const click = () => {
|
||||
|
|
|
@ -105,6 +105,7 @@
|
|||
:network="network"
|
||||
:channel="channel"
|
||||
:focused="focused"
|
||||
@scrolled-to-bottom="onScrolledToBottom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -174,10 +175,12 @@ export default defineComponent({
|
|||
return undefined;
|
||||
});
|
||||
|
||||
const onScrolledToBottom = (data: boolean) => {
|
||||
props.channel.scrolledToBottom = data;
|
||||
};
|
||||
|
||||
const channelChanged = () => {
|
||||
// Triggered when active channel is set or changed
|
||||
// props.channel.highlight = 0;
|
||||
// props.channel.unread = 0;
|
||||
emit("channel-changed", props.channel);
|
||||
|
||||
socket.emit("open", props.channel.id);
|
||||
|
@ -268,6 +271,7 @@ export default defineComponent({
|
|||
topicInput,
|
||||
specialComponent,
|
||||
editTopicRef,
|
||||
onScrolledToBottom,
|
||||
hideUserVisibleError,
|
||||
editTopic,
|
||||
saveTopic,
|
||||
|
|
|
@ -35,8 +35,8 @@
|
|||
v-for="user in users"
|
||||
:key="user.original.nick + '-search'"
|
||||
:on-hover="hoverUser"
|
||||
:active="user.original === (activeUser as any)"
|
||||
:user="user.original"
|
||||
:active="user.original === activeUser"
|
||||
:user="(user.original as any)"
|
||||
v-html="user.string"
|
||||
/>
|
||||
</template>
|
||||
|
@ -241,6 +241,7 @@ export default defineComponent({
|
|||
groupedUsers,
|
||||
userSearchInput,
|
||||
activeUser,
|
||||
userlist,
|
||||
|
||||
setUserSearchInput,
|
||||
getModeClass,
|
||||
|
|
|
@ -326,7 +326,9 @@ export default defineComponent({
|
|||
// 2. If image is zoomed in, simply dragging it will move it around
|
||||
const onImageMouseDown = (e: MouseEvent) => {
|
||||
// todo: ignore if in touch event currently?
|
||||
|
||||
// only left mouse
|
||||
// TODO: e.buttons?
|
||||
if (e.which !== 1) {
|
||||
return;
|
||||
}
|
||||
|
@ -467,6 +469,7 @@ export default defineComponent({
|
|||
nextImage,
|
||||
onImageTouchStart,
|
||||
computeImageStyles,
|
||||
viewer,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -146,7 +146,7 @@ import eventbus from "../js/eventbus";
|
|||
import friendlysize from "../js/helpers/friendlysize";
|
||||
import {useStore} from "../js/store";
|
||||
import type {ClientChan, ClientLinkPreview} from "../js/types";
|
||||
import {imageViewerKey, useImageViewer} from "./App.vue";
|
||||
import {imageViewerKey} from "./App.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "LinkPreview",
|
||||
|
@ -166,6 +166,7 @@ export default defineComponent({
|
|||
|
||||
const showMoreButton = ref(false);
|
||||
const isContentShown = ref(false);
|
||||
const imageViewer = inject(imageViewerKey);
|
||||
|
||||
const content = ref<HTMLDivElement | null>(null);
|
||||
const container = ref<HTMLDivElement | null>(null);
|
||||
|
@ -234,9 +235,8 @@ export default defineComponent({
|
|||
|
||||
const onThumbnailClick = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const imageViewer = inject(imageViewerKey);
|
||||
|
||||
if (!imageViewer.value) {
|
||||
if (!imageViewer?.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ export default defineComponent({
|
|||
const onClick = () => {
|
||||
props.link.shown = !props.link.shown;
|
||||
emit("toggle-link-preview", props.link, props.message);
|
||||
// this.$parent.$emit("toggle-link-preview", this.link, this.$parent.message);
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<div class="mentions-info">
|
||||
<div>
|
||||
<span class="from">
|
||||
<Username :user="message.from" />
|
||||
<Username :user="(message.from as any)" />
|
||||
<template v-if="message.channel">
|
||||
in {{ message.channel.channel.name }} on
|
||||
{{ message.channel.network.name }}
|
||||
|
|
|
@ -104,7 +104,8 @@ export default defineComponent({
|
|||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
focused: String,
|
||||
},
|
||||
setup(props) {
|
||||
emits: ["scrolled-to-bottom"],
|
||||
setup(props, {emit}) {
|
||||
const store = useStore();
|
||||
|
||||
const chat = ref<HTMLDivElement | null>(null);
|
||||
|
@ -116,7 +117,7 @@ export default defineComponent({
|
|||
|
||||
const jumpToBottom = () => {
|
||||
skipNextScrollEvent.value = true;
|
||||
props.channel.scrolledToBottom = true;
|
||||
emit("scrolled-to-bottom", true);
|
||||
|
||||
const el = chat.value;
|
||||
|
||||
|
@ -358,7 +359,7 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
props.channel.scrolledToBottom = el.scrollHeight - el.scrollTop - el.offsetHeight <= 30;
|
||||
emit("scrolled-to-bottom", el.scrollHeight - el.scrollTop - el.offsetHeight <= 30);
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
|
@ -385,7 +386,7 @@ export default defineComponent({
|
|||
watch(
|
||||
() => props.channel.id,
|
||||
() => {
|
||||
props.channel.scrolledToBottom = true;
|
||||
emit("scrolled-to-bottom", 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
|
||||
|
@ -434,6 +435,7 @@ export default defineComponent({
|
|||
chat,
|
||||
store,
|
||||
onShowMoreClick,
|
||||
loadMoreButton,
|
||||
onCopy,
|
||||
condensedMessages,
|
||||
shouldDisplayDateMarker,
|
||||
|
|
|
@ -122,6 +122,40 @@ export default defineComponent({
|
|||
store.commit("sidebarOpen", state);
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (!touchStartPos.value?.screenX || !touchCurPos.value?.screenX) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = touchCurPos.value.screenX - touchStartPos.value.screenX;
|
||||
const absDiff = Math.abs(diff);
|
||||
|
||||
if (
|
||||
absDiff > menuWidth.value / 2 ||
|
||||
(Date.now() - touchStartTime.value < 180 && absDiff > 50)
|
||||
) {
|
||||
toggle(diff > 0);
|
||||
}
|
||||
|
||||
document.body.removeEventListener("touchmove", onTouchMove);
|
||||
document.body.removeEventListener("touchend", onTouchEnd);
|
||||
|
||||
store.commit("sidebarDragging", false);
|
||||
|
||||
if (sidebar.value) {
|
||||
sidebar.value.style.transform = "";
|
||||
}
|
||||
|
||||
if (props.overlay) {
|
||||
props.overlay.style.opacity = "";
|
||||
}
|
||||
|
||||
touchStartPos.value = null;
|
||||
touchCurPos.value = null;
|
||||
touchStartTime.value = 0;
|
||||
menuIsMoving.value = false;
|
||||
};
|
||||
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
const touch = (touchCurPos.value = e.touches.item(0));
|
||||
|
||||
|
@ -173,42 +207,16 @@ export default defineComponent({
|
|||
sidebar.value.style.transform = "translate3d(" + distX.toString() + "px, 0, 0)";
|
||||
}
|
||||
|
||||
if (props.overlay) {
|
||||
props.overlay.style.opacity = `${distX / menuWidth.value}`;
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (!touchStartPos.value?.screenX || !touchCurPos.value?.screenX) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = touchCurPos.value.screenX - touchStartPos.value.screenX;
|
||||
const absDiff = Math.abs(diff);
|
||||
|
||||
if (
|
||||
absDiff > menuWidth.value / 2 ||
|
||||
(Date.now() - touchStartTime.value < 180 && absDiff > 50)
|
||||
) {
|
||||
toggle(diff > 0);
|
||||
}
|
||||
|
||||
document.body.removeEventListener("touchmove", onTouchMove);
|
||||
document.body.removeEventListener("touchend", onTouchEnd);
|
||||
|
||||
store.commit("sidebarDragging", false);
|
||||
|
||||
if (sidebar.value) {
|
||||
sidebar.value.style.transform = "";
|
||||
}
|
||||
|
||||
props.overlay.style.opacity = "";
|
||||
|
||||
touchStartPos.value = null;
|
||||
touchCurPos.value = null;
|
||||
touchStartTime.value = 0;
|
||||
menuIsMoving.value = false;
|
||||
};
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
if (!sidebar.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
touchStartPos.value = touchCurPos.value = e.touches.item(0);
|
||||
|
||||
if (e.touches.length !== 1) {
|
||||
|
@ -216,7 +224,7 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
const styles = window.getComputedStyle(this.$refs.sidebar);
|
||||
const styles = window.getComputedStyle(sidebar.value);
|
||||
|
||||
menuWidth.value = parseFloat(styles.width);
|
||||
menuIsAbsolute.value = styles.position === "absolute";
|
||||
|
|
|
@ -17,11 +17,10 @@ import eventbus from "../js/eventbus";
|
|||
import colorClass from "../js/helpers/colorClass";
|
||||
import type {ClientChan, ClientNetwork, ClientUser} from "../js/types";
|
||||
|
||||
type UsernameUser = Partial<UserInMessage> &
|
||||
Partial<{
|
||||
type UsernameUser = Partial<UserInMessage> & {
|
||||
mode?: string;
|
||||
nick: string;
|
||||
mode: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "Username",
|
||||
|
@ -47,8 +46,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) {
|
||||
|
|
|
@ -38,7 +38,7 @@ function sortParts(a: Part, b: Part) {
|
|||
return a.start - b.start || b.end - a.end;
|
||||
}
|
||||
|
||||
type MergedParts = (TextPart | NamePart | EmojiPart | ChannelPart | LinkPart)[];
|
||||
export type MergedParts = (TextPart | NamePart | EmojiPart | ChannelPart | LinkPart)[];
|
||||
|
||||
// Merge the style fragments within the text parts, taking into account
|
||||
// boundaries and text sections that have not matched to links or channels.
|
||||
|
|
|
@ -1,22 +1,44 @@
|
|||
// TODO: type
|
||||
// @ts-nocheck
|
||||
|
||||
"use strict";
|
||||
|
||||
import {h as createElement, VNode} from "vue";
|
||||
import parseStyle from "./ircmessageparser/parseStyle";
|
||||
import findChannels from "./ircmessageparser/findChannels";
|
||||
import {findLinks} from "./ircmessageparser/findLinks";
|
||||
import findEmoji from "./ircmessageparser/findEmoji";
|
||||
import findNames from "./ircmessageparser/findNames";
|
||||
import merge from "./ircmessageparser/merge";
|
||||
import findChannels, {ChannelPart} from "./ircmessageparser/findChannels";
|
||||
import {findLinks, LinkPart} from "./ircmessageparser/findLinks";
|
||||
import findEmoji, {EmojiPart} from "./ircmessageparser/findEmoji";
|
||||
import findNames, {NamePart} from "./ircmessageparser/findNames";
|
||||
import merge, {MergedParts, Part} from "./ircmessageparser/merge";
|
||||
import emojiMap from "./fullnamemap.json";
|
||||
import LinkPreviewToggle from "../../components/LinkPreviewToggle.vue";
|
||||
import LinkPreviewFileSize from "../../components/LinkPreviewFileSize.vue";
|
||||
import InlineChannel from "../../components/InlineChannel.vue";
|
||||
import Username from "../../components/Username.vue";
|
||||
import {h as createElement, VNode} from "vue";
|
||||
import {ClientMessage, ClientNetwork} from "../types";
|
||||
|
||||
const emojiModifiersRegex = /[\u{1f3fb}-\u{1f3ff}]|\u{fe0f}/gu;
|
||||
|
||||
type Fragment = {
|
||||
class?: string[];
|
||||
text?: string;
|
||||
};
|
||||
|
||||
type StyledFragment = Fragment & {
|
||||
textColor?: string;
|
||||
bgColor?: string;
|
||||
hexColor?: string;
|
||||
hexBgColor?: string;
|
||||
|
||||
bold?: boolean;
|
||||
italic?: boolean;
|
||||
underline?: boolean;
|
||||
monospace?: boolean;
|
||||
strikethrough?: boolean;
|
||||
};
|
||||
|
||||
// Create an HTML `span` with styling information for a given fragment
|
||||
// TODO: remove any
|
||||
function createFragment(fragment: Record<any, any>) {
|
||||
function createFragment(fragment: StyledFragment): VNode | string | undefined {
|
||||
const classes: string[] = [];
|
||||
|
||||
if (fragment.bold) {
|
||||
|
@ -24,12 +46,10 @@ function createFragment(fragment: Record<any, any>) {
|
|||
}
|
||||
|
||||
if (fragment.textColor !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
classes.push("irc-fg" + fragment.textColor);
|
||||
}
|
||||
|
||||
if (fragment.bgColor !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
classes.push("irc-bg" + fragment.bgColor);
|
||||
}
|
||||
|
||||
|
@ -49,7 +69,14 @@ function createFragment(fragment: Record<any, any>) {
|
|||
classes.push("irc-monospace");
|
||||
}
|
||||
|
||||
const data = {} as Record<string, any>;
|
||||
const data: {
|
||||
class?: string[];
|
||||
style?: Record<string, string>;
|
||||
} = {
|
||||
class: undefined,
|
||||
style: undefined,
|
||||
};
|
||||
|
||||
let hasData = false;
|
||||
|
||||
if (classes.length > 0) {
|
||||
|
@ -60,17 +87,15 @@ function createFragment(fragment: Record<any, any>) {
|
|||
if (fragment.hexColor) {
|
||||
hasData = true;
|
||||
data.style = {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
color: `#${fragment.hexColor}`,
|
||||
};
|
||||
|
||||
if (fragment.hexBgColor) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
data.style["background-color"] = `#${fragment.hexBgColor}`;
|
||||
}
|
||||
}
|
||||
|
||||
return hasData ? createElement("span", data, fragment.text) : (fragment.text as string);
|
||||
return hasData ? createElement("span", data, fragment.text) : fragment.text;
|
||||
}
|
||||
|
||||
// Transform an IRC message potentially filled with styling control codes, URLs,
|
||||
|
@ -83,42 +108,40 @@ function parse(text: string, message?: ClientMessage, network?: ClientNetwork) {
|
|||
// On the plain text, find channels and URLs, returned as "parts". Parts are
|
||||
// arrays of objects containing start and end markers, as well as metadata
|
||||
// depending on what was found (channel or link).
|
||||
const channelPrefixes = network?.serverOptions?.CHANTYPES || ["#", "&"];
|
||||
const userModes = network?.serverOptions?.PREFIX.symbols || ["!", "@", "%", "+"];
|
||||
const channelPrefixes = network ? network.serverOptions.CHANTYPES : ["#", "&"];
|
||||
const userModes = network
|
||||
? network.serverOptions.PREFIX?.prefix?.map((pref) => pref.symbol)
|
||||
: ["!", "@", "%", "+"];
|
||||
const channelParts = findChannels(cleanText, channelPrefixes, userModes);
|
||||
const linkParts = findLinks(cleanText);
|
||||
const emojiParts = findEmoji(cleanText);
|
||||
// TODO: remove type casting.
|
||||
const nameParts = findNames(cleanText, message ? (message.users as string[]) || [] : []);
|
||||
const nameParts = findNames(cleanText, message ? message.users || [] : []);
|
||||
|
||||
const parts = [...channelParts, ...linkParts, ...emojiParts, ...nameParts];
|
||||
const parts = (channelParts as MergedParts)
|
||||
.concat(linkParts)
|
||||
.concat(emojiParts)
|
||||
.concat(nameParts);
|
||||
|
||||
// Merge the styling information with the channels / URLs / nicks / text objects and
|
||||
// generate HTML strings with the resulting fragments
|
||||
return merge(parts, styleFragments, cleanText).map((textPart) => {
|
||||
const fragments = textPart.fragments?.map((fragment) => createFragment(fragment)) as (
|
||||
| VNode
|
||||
| string
|
||||
)[];
|
||||
const fragments = textPart.fragments.map((fragment) => createFragment(fragment));
|
||||
|
||||
// Wrap these potentially styled fragments with links and channel buttons
|
||||
// TODO: fix typing
|
||||
if ("link" in textPart) {
|
||||
if (textPart.link) {
|
||||
const preview =
|
||||
message &&
|
||||
message.previews &&
|
||||
// @ts-ignore
|
||||
message.previews.find((p) => p.link === textPart.link);
|
||||
const link = createElement(
|
||||
"a",
|
||||
{
|
||||
// @ts-ignore
|
||||
"^href": textPart.link,
|
||||
"^dir": preview ? null : "auto",
|
||||
"^target": "_blank",
|
||||
"^rel": "noopener",
|
||||
href: textPart.link,
|
||||
dir: preview ? null : "auto",
|
||||
target: "_blank",
|
||||
rel: "noopener",
|
||||
},
|
||||
() => fragments
|
||||
fragments
|
||||
);
|
||||
|
||||
if (!preview) {
|
||||
|
@ -129,22 +152,16 @@ function parse(text: string, message?: ClientMessage, network?: ClientNetwork) {
|
|||
|
||||
if (preview.size > 0) {
|
||||
linkEls.push(
|
||||
// @ts-ignore
|
||||
createElement(LinkPreviewFileSize, {
|
||||
props: {
|
||||
size: preview.size,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
linkEls.push(
|
||||
// @ts-ignore
|
||||
createElement(LinkPreviewToggle, {
|
||||
props: {
|
||||
link: preview,
|
||||
message: message,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -153,66 +170,49 @@ function parse(text: string, message?: ClientMessage, network?: ClientNetwork) {
|
|||
return createElement(
|
||||
"span",
|
||||
{
|
||||
attrs: {
|
||||
dir: "auto",
|
||||
},
|
||||
},
|
||||
() => linkEls
|
||||
linkEls
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
} else if (textPart.channel) {
|
||||
return createElement(
|
||||
InlineChannel,
|
||||
{
|
||||
props: {
|
||||
// @ts-ignore
|
||||
channel: textPart.channel,
|
||||
},
|
||||
},
|
||||
() => fragments
|
||||
{
|
||||
default: () => fragments,
|
||||
}
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
} else if (textPart.emoji) {
|
||||
// @ts-ignore
|
||||
const emojiWithoutModifiers = textPart.emoji.replace(emojiModifiersRegex, "");
|
||||
const title = emojiMap[emojiWithoutModifiers]
|
||||
? `Emoji: ${emojiMap[emojiWithoutModifiers] as string}`
|
||||
? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`Emoji: ${emojiMap[emojiWithoutModifiers]}`
|
||||
: null;
|
||||
|
||||
return createElement(
|
||||
"span",
|
||||
{
|
||||
class: ["emoji"],
|
||||
attrs: {
|
||||
role: "img",
|
||||
"aria-label": title,
|
||||
title: title,
|
||||
},
|
||||
},
|
||||
() => fragments
|
||||
fragments
|
||||
);
|
||||
// @ts-ignore
|
||||
} else if (textPart.nick) {
|
||||
return createElement(
|
||||
// @ts-ignore
|
||||
Username,
|
||||
{
|
||||
props: {
|
||||
user: {
|
||||
// @ts-ignore
|
||||
nick: textPart.nick,
|
||||
},
|
||||
// @ts-ignore
|
||||
channel: textPart.channel,
|
||||
network,
|
||||
},
|
||||
attrs: {
|
||||
dir: "auto",
|
||||
},
|
||||
},
|
||||
() => fragments
|
||||
{
|
||||
default: () => fragments,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
3
client/js/types.d.ts
vendored
3
client/js/types.d.ts
vendored
|
@ -27,8 +27,9 @@ type ClientUser = User & {
|
|||
//
|
||||
};
|
||||
|
||||
type ClientMessage = Message & {
|
||||
type ClientMessage = Omit<Message, "users"> & {
|
||||
time: number;
|
||||
users: string[];
|
||||
};
|
||||
|
||||
type ClientChan = Omit<Chan, "users" | "messages"> & {
|
||||
|
|
|
@ -273,12 +273,16 @@ class Uploader {
|
|||
const fullURL = new URL(url, location.toString()).toString();
|
||||
const textbox = document.getElementById("input");
|
||||
|
||||
if (!textbox) {
|
||||
if (!(textbox instanceof HTMLTextAreaElement)) {
|
||||
throw new Error("Could not find textbox in upload");
|
||||
}
|
||||
|
||||
const initStart = textbox.selectionStart;
|
||||
|
||||
if (!initStart) {
|
||||
throw new Error("Could not find selection start in textbox in upload");
|
||||
}
|
||||
|
||||
// Get the text before the cursor, and add a space if it's not in the beginning
|
||||
const headToCursor = initStart > 0 ? textbox.value.substr(0, initStart) + " " : "";
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ socket.once("push:issubscribed", function (hasSubscriptionOnServer) {
|
|||
// If client has push registration but the server knows nothing about it,
|
||||
// this subscription is broken and client has to register again
|
||||
if (subscription && hasSubscriptionOnServer === false) {
|
||||
subscription.unsubscribe().then((successful) => {
|
||||
void subscription.unsubscribe().then((successful) => {
|
||||
store.commit(
|
||||
"pushNotificationState",
|
||||
successful ? "supported" : "unsupported"
|
||||
|
|
|
@ -15,7 +15,7 @@ import inputs from "./plugins/inputs";
|
|||
import PublicClient from "./plugins/packages/publicClient";
|
||||
import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
|
||||
import TextFileMessageStorage from "./plugins/messageStorage/text";
|
||||
import Network, {NetworkWithIrcFramework} from "./models/network";
|
||||
import Network, {IgnoreListItem, NetworkWithIrcFramework} from "./models/network";
|
||||
import ClientManager from "./clientManager";
|
||||
import {MessageStorage, SearchQuery, SearchResponse} from "./plugins/messageStorage/types";
|
||||
|
||||
|
@ -219,8 +219,7 @@ class Client {
|
|||
let network: Network | null = null;
|
||||
let chan: Chan | null | undefined = null;
|
||||
|
||||
for (const i in this.networks) {
|
||||
const n = this.networks[i];
|
||||
for (const n of this.networks) {
|
||||
chan = _.find(n.channels, {id: channelId});
|
||||
|
||||
if (chan) {
|
||||
|
@ -236,7 +235,7 @@ class Client {
|
|||
return false;
|
||||
}
|
||||
|
||||
connect(args: any, isStartup = false) {
|
||||
connect(args: Record<string, any>, isStartup = false) {
|
||||
const client = this;
|
||||
const channels: Chan[] = [];
|
||||
|
||||
|
@ -267,19 +266,20 @@ class Client {
|
|||
"User '" +
|
||||
client.name +
|
||||
"' on network '" +
|
||||
args.name +
|
||||
(args.name as string) +
|
||||
"' has an invalid channel which has been ignored"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO; better typing for args
|
||||
const network = new Network({
|
||||
uuid: args.uuid,
|
||||
uuid: args.uuid as string,
|
||||
name: String(
|
||||
args.name || (Config.values.lockNetwork ? Config.values.defaults.name : "") || ""
|
||||
),
|
||||
host: String(args.host || ""),
|
||||
port: parseInt(args.port, 10),
|
||||
port: parseInt(args.port as string, 10),
|
||||
tls: !!args.tls,
|
||||
userDisconnected: !!args.userDisconnected,
|
||||
rejectUnauthorized: !!args.rejectUnauthorized,
|
||||
|
@ -291,9 +291,9 @@ class Client {
|
|||
sasl: String(args.sasl || ""),
|
||||
saslAccount: String(args.saslAccount || ""),
|
||||
saslPassword: String(args.saslPassword || ""),
|
||||
commands: args.commands || [],
|
||||
commands: (args.commands as string[]) || [],
|
||||
channels: channels,
|
||||
ignoreList: args.ignoreList ? args.ignoreList : [],
|
||||
ignoreList: args.ignoreList ? (args.ignoreList as IgnoreListItem[]) : [],
|
||||
|
||||
proxyEnabled: !!args.proxyEnabled,
|
||||
proxyHost: String(args.proxyHost || ""),
|
||||
|
@ -316,6 +316,8 @@ class Client {
|
|||
|
||||
(network as NetworkWithIrcFramework).createIrcFramework(client);
|
||||
|
||||
// TODO
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
events.forEach(async (plugin) => {
|
||||
(await import(`./plugins/irc-events/${plugin}`)).default.apply(client, [
|
||||
network.irc,
|
||||
|
@ -363,7 +365,7 @@ class Client {
|
|||
let friendlyAgent = "";
|
||||
|
||||
if (agent.browser.name) {
|
||||
friendlyAgent = `${agent.browser.name} ${agent.browser.major}`;
|
||||
friendlyAgent = `${agent.browser.name} ${agent.browser.major || ""}`;
|
||||
} else {
|
||||
friendlyAgent = "Unknown browser";
|
||||
}
|
||||
|
@ -421,7 +423,7 @@ class Client {
|
|||
// so that reloading the page will open this channel
|
||||
this.lastActiveChannel = target.chan.id;
|
||||
|
||||
let text = data.text;
|
||||
let text: string = data.text;
|
||||
|
||||
// This is either a normal message or a command escaped with a leading '/'
|
||||
if (text.charAt(0) !== "/" || text.charAt(1) === "/") {
|
||||
|
@ -438,11 +440,11 @@ class Client {
|
|||
|
||||
text = "say " + text.replace(/^\//, "");
|
||||
} else {
|
||||
text = text.substr(1);
|
||||
text = text.substring(1);
|
||||
}
|
||||
|
||||
const args = text.split(" ");
|
||||
const cmd = args.shift().toLowerCase();
|
||||
const cmd = args?.shift()?.toLowerCase() || "";
|
||||
|
||||
const irc = target.network.irc;
|
||||
let connected = irc && irc.connection && irc.connection.connected;
|
||||
|
|
|
@ -30,6 +30,9 @@ export type LinkPreview = {
|
|||
shown: boolean | null;
|
||||
error: undefined | string;
|
||||
message: undefined | string;
|
||||
|
||||
media: string;
|
||||
mediaType: string;
|
||||
};
|
||||
|
||||
export default function (client: Client, chan: Chan, msg: Msg, cleanText: string) {
|
||||
|
@ -65,6 +68,8 @@ export default function (client: Client, chan: Chan, msg: Msg, cleanText: string
|
|||
shown: null,
|
||||
error: undefined,
|
||||
message: undefined,
|
||||
media: "",
|
||||
mediaType: "",
|
||||
};
|
||||
|
||||
cleanLinks.push(preview);
|
||||
|
@ -88,11 +93,13 @@ export default function (client: Client, chan: Chan, msg: Msg, cleanText: string
|
|||
}
|
||||
|
||||
function parseHtml(preview, res, client: Client) {
|
||||
return new Promise((resolve) => {
|
||||
// TODO:
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
return new Promise((resolve: (preview: LinkPreview | null) => void) => {
|
||||
const $ = cheerio.load(res.data);
|
||||
|
||||
return parseHtmlMedia($, preview, client)
|
||||
.then((newRes) => resolve(newRes))
|
||||
.then((newRes) => resolve(newRes as any))
|
||||
.catch(() => {
|
||||
preview.type = "link";
|
||||
preview.head =
|
||||
|
@ -140,6 +147,8 @@ function parseHtml(preview, res, client: Client) {
|
|||
preview.thumbActualUrl = thumb;
|
||||
}
|
||||
|
||||
// TODO
|
||||
// @ts-ignore
|
||||
resolve(resThumb);
|
||||
})
|
||||
.catch(() => resolve(null));
|
||||
|
@ -466,7 +475,10 @@ function fetch(uri: string, headers: Record<string, string>) {
|
|||
})
|
||||
.on("error", (e) => reject(e))
|
||||
.on("data", (data) => {
|
||||
buffer = Buffer.concat([buffer, data], buffer.length + data.length);
|
||||
buffer = Buffer.concat(
|
||||
[buffer, data],
|
||||
buffer.length + (data as Array<any>).length
|
||||
);
|
||||
|
||||
if (buffer.length >= limit) {
|
||||
gotStream.destroy();
|
||||
|
@ -474,7 +486,7 @@ function fetch(uri: string, headers: Record<string, string>) {
|
|||
})
|
||||
.on("end", () => gotStream.destroy())
|
||||
.on("close", () => {
|
||||
let type: string = "";
|
||||
let type = "";
|
||||
|
||||
// If we downloaded more data then specified in Content-Length, use real data size
|
||||
const size = contentLength > buffer.length ? contentLength : buffer.length;
|
||||
|
|
Loading…
Reference in a new issue