Chat is rendering

This commit is contained in:
Max Leiter 2022-05-23 12:17:10 -07:00
parent 52c13f49c1
commit f189e9766c
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
12 changed files with 151 additions and 159 deletions

View file

@ -14,7 +14,10 @@
id="context-menu"
ref="contextMenu"
role="menu"
:style="style"
:style="{
top: style.top + 'px',
left: style.left + 'px',
}"
tabindex="-1"
@mouseleave="activeItem = -1"
@keydown.enter.prevent="clickActiveItem"

View file

@ -24,11 +24,6 @@ export default defineComponent({
},
focused: Boolean,
},
// methods: {
// forceUpdate(){
// this.$forceUpdate()
// }
// },
setup(props) {
const localeDate = computed(() => dayjs(props.message.time).format("D MMMM YYYY"));

View file

@ -66,6 +66,7 @@ import Message from "./Message.vue";
import MessageCondensed from "./MessageCondensed.vue";
import DateMarker from "./DateMarker.vue";
import {
defineExpose,
computed,
defineComponent,
nextTick,
@ -247,7 +248,7 @@ export default defineComponent({
message: Msg | ClientMessage | CondensedMessageContainer,
id: number
) => {
const previousMessage = condensedMessages[id - 1];
const previousMessage = condensedMessages.value[id - 1];
if (!previousMessage) {
return true;
@ -431,6 +432,7 @@ export default defineComponent({
shouldDisplayUnreadMarker,
keepScrollPosition,
isPreviousSource,
jumpToBottom,
onLinkPreviewToggle,
};
},

View file

@ -46,7 +46,7 @@
</template>
<script lang="ts">
import {defineComponent, PropType} from "vue";
import {computed, defineComponent, PropType} from "vue";
import collapseNetwork from "../js/helpers/collapseNetwork";
import roundBadgeNumber from "../js/helpers/roundBadgeNumber";
import ChannelWrapper from "./ChannelWrapper.vue";
@ -68,24 +68,34 @@ export default defineComponent({
isFiltering: Boolean,
},
emits: ["toggle-join-channel"],
computed: {
channel(): ClientChan {
return this.network.channels[0];
},
joinChannelLabel(): string {
return this.isJoinChannelShown ? "Cancel" : "Join a channel…";
},
unreadCount(): string {
return roundBadgeNumber(this.channel.unread);
},
},
methods: {
onCollapseClick(): void {
collapseNetwork(this.network, !this.network.isCollapsed);
},
getExpandLabel(network: ClientNetwork): string {
setup(props) {
const channel = computed(() => {
return props.network.channels[0];
});
const joinChannelLabel = computed(() => {
return props.isJoinChannelShown ? "Cancel" : "Join a channel…";
});
const unreadCount = computed(() => {
return roundBadgeNumber(channel.value.unread);
});
const onCollapseClick = () => {
collapseNetwork(props.network, !props.network.isCollapsed);
};
const getExpandLabel = (network: ClientNetwork) => {
return network.isCollapsed ? "Expand" : "Collapse";
},
};
return {
channel,
joinChannelLabel,
unreadCount,
onCollapseClick,
getExpandLabel,
};
},
});
</script>

View file

@ -19,7 +19,7 @@
<script lang="ts">
import ParsedMessage from "../ParsedMessage.vue";
import localetime from "../../js/helpers/localetime";
import localeTime from "../../js/helpers/localetime";
import {defineComponent, PropType} from "vue";
import type {ClientNetwork, ClientChan} from "../../js/types";
@ -32,10 +32,14 @@ export default defineComponent({
network: {type: Object as PropType<ClientNetwork>, required: true},
channel: {type: Object as PropType<ClientChan>, required: true},
},
methods: {
localetime(date: number | Date): string {
return localetime(date);
},
setup() {
const localetime = (date: number | Date) => {
return localeTime(date);
};
return {
localetime,
};
},
});
</script>

View file

@ -59,48 +59,65 @@
import storage from "../../js/localStorage";
import socket from "../../js/socket";
import RevealPassword from "../RevealPassword.vue";
import {defineComponent} from "vue";
import {defineComponent, onBeforeUnmount, onMounted, ref} from "vue";
export default defineComponent({
name: "SignIn",
components: {
RevealPassword,
},
data() {
return {
inFlight: false,
errorShown: false,
setup() {
const inFlight = ref(false);
const errorShown = ref(false);
const username = ref<HTMLInputElement | null>(null);
const password = ref<HTMLInputElement | null>(null);
const onAuthFailed = () => {
inFlight.value = false;
errorShown.value = true;
};
},
mounted() {
socket.on("auth:failed", this.onAuthFailed);
},
beforeUnmount() {
socket.off("auth:failed", this.onAuthFailed);
},
methods: {
onAuthFailed() {
this.inFlight = false;
this.errorShown = true;
},
onSubmit(event) {
const onSubmit = (event: Event) => {
event.preventDefault();
this.inFlight = true;
this.errorShown = false;
if (!username.value || !password.value) {
return;
}
inFlight.value = true;
errorShown.value = false;
const values = {
user: this.$refs.username.value,
password: this.$refs.password.value,
user: username.value?.value,
password: password.value?.value,
};
storage.set("user", values.user);
socket.emit("auth:perform", values);
},
getStoredUser() {
};
const getStoredUser = () => {
return storage.get("user");
},
};
onMounted(() => {
socket.on("auth:failed", onAuthFailed);
});
onBeforeUnmount(() => {
socket.off("auth:failed", onAuthFailed);
});
return {
inFlight,
errorShown,
username,
password,
onSubmit,
getStoredUser,
};
},
});
</script>

View file

@ -587,6 +587,12 @@ p {
/* End icons */
#app {
height: 100%;
width: 100%;
}
#viewport {
display: flex;
height: 100%;

View file

@ -1,5 +1,5 @@
// Generates a string from "color-1" to "color-32" based on an input string
export default (str) => {
export default (str: string) => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
@ -11,5 +11,5 @@ export default (str) => {
due to A being ascii 65 (100 0001)
while a being ascii 97 (110 0001)
*/
return "color-" + (1 + (hash % 32));
return "color-" + (1 + (hash % 32)).toString();
};

View file

@ -120,7 +120,7 @@ function parse(text: string, message?: ClientMessage, network?: ClientNetwork) {
rel: "noopener",
},
},
fragments
() => fragments
);
if (!preview) {
@ -159,8 +159,9 @@ function parse(text: string, message?: ClientMessage, network?: ClientNetwork) {
dir: "auto",
},
},
linkEls
() => linkEls
);
// @ts-ignore
} else if (textPart.channel) {
return createElement(
@ -171,8 +172,9 @@ function parse(text: string, message?: ClientMessage, network?: ClientNetwork) {
channel: textPart.channel,
},
},
fragments
() => fragments
);
// @ts-ignore
} else if (textPart.emoji) {
// @ts-ignore
@ -191,7 +193,7 @@ function parse(text: string, message?: ClientMessage, network?: ClientNetwork) {
title: title,
},
},
fragments
() => fragments
);
// @ts-ignore
} else if (textPart.nick) {
@ -205,14 +207,14 @@ function parse(text: string, message?: ClientMessage, network?: ClientNetwork) {
nick: textPart.nick,
},
// @ts-ignore
channel: messageChannel,
channel: textPart.channel,
network,
},
attrs: {
dir: "auto",
},
},
fragments
() => fragments
);
}

11
client/js/types.d.ts vendored
View file

@ -79,13 +79,10 @@ declare module "*.vue" {
}
declare module "vue" {
interface ComponentCustomProperties {
// vue-router should do this for us
$router: import("vue-router").Router;
// TODO: Vue struggles with typing using the options API, so we should switch to composition API
// $root
}
// interface ComponentCustomProperties {
// // TODO: Vue struggles with typing using the options API, so we should switch to composition API
// // $root
// }
}
declare module "vue-router" {

View file

@ -43,7 +43,7 @@ self.addEventListener("fetch", function (event) {
return;
}
event.respondWith(networkOrCache(event));
return event.respondWith(networkOrCache(event));
});
async function putInCache(request, response) {

View file

@ -1,29 +1,27 @@
import {expect} from "chai";
import {renderToString} from "@vue/server-test-utils";
import ParsedMessageTestWrapper from "../../components/ParsedMessageTestWrapper.vue";
import {mount} from "@vue/test-utils";
import ParsedMessage from "../../../../client/components/ParsedMessage.vue";
import {ClientMessage} from "../../../../client/js/types";
async function getParsedMessageContents(text: any, message: any) {
let contents = await renderToString(ParsedMessageTestWrapper, {
propsData: {
function getParsedMessageContents(text: string, message?: any) {
const wrapper = mount(ParsedMessage, {
props: {
text,
message,
},
});
// The wrapper adds a surrounding div to the message html, so we clean that out here
contents = contents.replace(/^<div data-server-rendered="true">([^]+)<\/div>$/m, "$1");
return contents;
return wrapper.html();
}
describe("IRC formatted message parser", () => {
it("should not introduce xss", async () => {
it("should not introduce xss", () => {
const testCases = [
{
input: "<img onerror='location.href=\"//youtube.com\"'>",
expected:
'&lt;img onerror=\'location.href=&quot;<a href="http://youtube.com" dir="auto" target="_blank" rel="noopener">//youtube.com</a>&quot;\'&gt;',
'&lt;img onerror=\'location.href="<a href="http://youtube.com" dir="auto" target="_blank" rel="noopener">//youtube.com</a>"\'&gt;',
},
{
input: '#&">bug',
@ -32,16 +30,13 @@ describe("IRC formatted message parser", () => {
},
];
const actual = await Promise.all(
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const actual = testCases.map((testCase) => getParsedMessageContents(testCase.input));
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should skip all <32 ASCII codes except linefeed", async () => {
it("should skip all <32 ASCII codes except linefeed", () => {
const testCases = [
{
input: "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1B\x1D\x1D\x1E\x1Ftext\x0Awithcontrolcodestest",
@ -50,16 +45,13 @@ describe("IRC formatted message parser", () => {
},
];
const actual = await Promise.all(
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const actual = testCases.map((testCase) => getParsedMessageContents(testCase.input));
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should find urls", async () => {
it("should find urls", () => {
const testCases = [
{
input: "irc://irc.example.com/thelounge",
@ -102,16 +94,13 @@ describe("IRC formatted message parser", () => {
},
];
const actual = await Promise.all(
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const actual = testCases.map((testCase) => getParsedMessageContents(testCase.input));
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("url with a dot parsed correctly", async () => {
it("url with a dot parsed correctly", () => {
const input =
"bonuspunkt: your URL parser misparses this URL: https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx";
const correctResult =
@ -120,13 +109,12 @@ describe("IRC formatted message parser", () => {
"https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx" +
"</a>";
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
const actual = await getParsedMessageContents(input);
const actual = getParsedMessageContents(input);
expect(actual).to.deep.equal(correctResult);
});
it("should balance brackets", async () => {
it("should balance brackets", () => {
const testCases = [
{
input: "<https://theos.kyriasis.com/~kyrias/stats/archlinux.html>",
@ -162,16 +150,13 @@ describe("IRC formatted message parser", () => {
},
];
const actual = await Promise.all(
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const actual = testCases.map((testCase) => getParsedMessageContents(testCase.input));
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should not find urls", async () => {
it("should not find urls", () => {
const testCases = [
{
input: "text www. text",
@ -183,16 +168,13 @@ describe("IRC formatted message parser", () => {
},
];
const actual = await Promise.all(
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const actual = testCases.map((testCase) => getParsedMessageContents(testCase.input));
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should find channels", async () => {
it("should find channels", () => {
const testCases = [
{
input: "#a",
@ -241,16 +223,13 @@ describe("IRC formatted message parser", () => {
},
];
const actual = await Promise.all(
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const actual = testCases.map((testCase) => getParsedMessageContents(testCase.input));
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should not find channels", async () => {
it("should not find channels", () => {
const testCases = [
{
input: "hi#test",
@ -262,10 +241,7 @@ describe("IRC formatted message parser", () => {
},
];
const actual = await Promise.all(
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const actual = testCases.map((testCase) => getParsedMessageContents(testCase.input));
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
@ -362,9 +338,8 @@ describe("IRC formatted message parser", () => {
'<span class="irc-bold">bold</span>' + " " + '<span class="irc-bold">bold</span>',
},
].forEach(({name, input, expected}) => {
it(`should handle style characters: ${name}`, async () => {
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
expect(await getParsedMessageContents(input)).to.equal(expected);
it(`should handle style characters: ${name}`, () => {
expect(getParsedMessageContents(input)).to.equal(expected);
});
});
@ -391,7 +366,7 @@ describe("IRC formatted message parser", () => {
expect(actual).to.deep.equal(expected);
});
it("should not find nicks", async () => {
it("should not find nicks", () => {
const testCases = [
{
users: ["MaxLeiter, test"],
@ -411,16 +386,13 @@ describe("IRC formatted message parser", () => {
},
];
const actual = await Promise.all(
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const actual = testCases.map((testCase) => getParsedMessageContents(testCase.input));
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should go bonkers like mirc", async () => {
it("should go bonkers like mirc", () => {
const testCases = [
{
input: "\x02irc\x0f://\x1dirc.example.com\x0f/\x034,8thelounge",
@ -443,10 +415,7 @@ describe("IRC formatted message parser", () => {
},
];
const actual = await Promise.all(
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const actual = testCases.map((testCase) => getParsedMessageContents(testCase.input));
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
@ -521,13 +490,12 @@ describe("IRC formatted message parser", () => {
'<span dir="auto" role="button" tabindex="0" class="inline-channel">#i❤thelounge</span>',
},
].forEach(({name, input, expected}) => {
it(`should find emoji: ${name}`, async () => {
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
expect(await getParsedMessageContents(input)).to.equal(expected);
it(`should find emoji: ${name}`, () => {
expect(getParsedMessageContents(input)).to.equal(expected);
});
});
it("should optimize generated html", async () => {
it("should optimize generated html", () => {
const testCases = [
{
input: 'test \x0312#\x0312\x0312"te\x0312st\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312a',
@ -539,16 +507,13 @@ describe("IRC formatted message parser", () => {
},
];
const actual = await Promise.all(
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const actual = testCases.map((testCase) => getParsedMessageContents(testCase.input));
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should trim common protocols", async () => {
it("should trim common protocols", () => {
const testCases = [
{
input: "like..http://example.com",
@ -568,16 +533,13 @@ describe("IRC formatted message parser", () => {
},
];
const actual = await Promise.all(
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const actual = testCases.map((testCase) => getParsedMessageContents(testCase.input));
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should not find channel in fragment", async () => {
it("should not find channel in fragment", () => {
const testCases = [
{
input: "http://example.com/#hash",
@ -588,19 +550,15 @@ describe("IRC formatted message parser", () => {
},
];
const actual = await Promise.all(
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const actual = testCases.map((testCase) => getParsedMessageContents(testCase.input));
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should not overlap parts", async () => {
it("should not overlap parts", () => {
const input = "Url: http://example.com/path Channel: ##channel";
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
const actual = await getParsedMessageContents(input);
const actual = getParsedMessageContents(input);
expect(actual).to.equal(
'Url: <a href="http://example.com/path" dir="auto" target="_blank" rel="noopener">http://example.com/path</a> ' +
@ -608,10 +566,9 @@ describe("IRC formatted message parser", () => {
);
});
it("should handle overlapping parts by using first starting", async () => {
it("should handle overlapping parts by using first starting", () => {
const input = "#test-https://example.com";
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
const actual = await getParsedMessageContents(input);
const actual = getParsedMessageContents(input);
expect(actual).to.equal(
'<span dir="auto" role="button" tabindex="0" class="inline-channel">' +
@ -620,10 +577,9 @@ describe("IRC formatted message parser", () => {
);
});
it("should find links separated by tab character", async () => {
it("should find links separated by tab character", () => {
const input = "example.com\texample.org";
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
const actual = await getParsedMessageContents(input);
const actual = getParsedMessageContents(input);
expect(actual).to.equal(
'<a href="http://example.com" dir="auto" target="_blank" rel="noopener">example.com</a>' +