Compare commits

...
Sign in to create a new pull request.

18 commits

Author SHA1 Message Date
1302826ba8 Merge branch 'main' into settings-page
All checks were successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-06-23 18:52:41 +00:00
010472c83d
feat: implement scroll position retention 2025-06-07 06:12:47 +02:00
acca8468f0
feat: make <NuxtPage> keepalive 2025-06-07 06:12:46 +02:00
61df171c59
feat: create function to push messages to chat 2025-06-07 06:12:46 +02:00
2c76edaa32
feat: update scrollToBottom() 2025-06-07 06:12:46 +02:00
ccefc8ca19
feat: remove useless css properties 2025-06-07 06:12:46 +02:00
cca348b476
feat: add spacing between grouped messages 2025-06-07 06:12:45 +02:00
c0f4697d00
feat: move some colors to root variables 2025-06-07 06:12:45 +02:00
774e10d68c
fix: automatic auth refresh
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-06 01:57:19 +02:00
d49d533724
feat: change members list back to using flex display
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-06 01:56:15 +02:00
3899843a7c
feat: change message area back to using flex display, improvement for UI 2025-06-06 01:55:18 +02:00
d22e77ed14
feat: improve appearance of members list, same method as messages 2025-06-03 20:53:30 +02:00
67e10a4387
feat: refactor to allow more markdown tags, syling changes to make lists and headings not take up as much space 2025-06-03 20:52:42 +02:00
263c823ceb
fix: tabIndex not working after changing messages to use "display: contents;" 2025-06-03 20:50:56 +02:00
82fde5671d
fix: last message from user not having bottom margin 2025-06-03 20:49:41 +02:00
d986f601de
feat: improve 24-hour to 12-hour format conversion by using Date methods
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-03 20:42:34 +02:00
d85eb03ad0
feat: change message history grids to fix scaling of message timestamps
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-03 16:18:13 +02:00
a56e12149b
fix: modify message css to avoid weird line spacing
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-03 06:15:51 +02:00
9 changed files with 153 additions and 75 deletions

10
app.vue
View file

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<Banner v-if="banner" /> <Banner v-if="banner" />
<NuxtPage /> <NuxtPage :keepalive="true" />
</div> </div>
</template> </template>
@ -12,6 +12,12 @@ const banner = useState("banner", () => false);
</script> </script>
<style> <style>
:root {
--background-color: rgb(30, 30, 30);
--main-text-color: rgb(190, 190, 190);
--outline-border: 1px solid rgb(150, 150, 150);
}
html, html,
body { body {
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
@ -22,7 +28,7 @@ body {
} }
*:focus-visible { *:focus-visible {
outline: 1px solid rgb(150, 150, 150); outline: var(--outline-border);
} }
a { a {

View file

@ -1,38 +1,36 @@
<template> <template>
<div v-if="props.type == 'normal'" :id="props.last ? 'last-message' : undefined" class="message normal-message" :class="{ 'message-margin-bottom': props.marginBottom }" tabindex="0"> <div v-if="props.type == 'normal'" :id="props.last ? 'last-message' : undefined" class="message normal-message">
<div class="left-column"> <div class="left-column">
<img v-if="props.img" class="message-author-avatar" :src="props.img" :alt="username"> <img v-if="props.img" class="message-author-avatar" :src="props.img" :alt="username" />
<Icon v-else name="lucide:user" class="message-author-avatar" /> <Icon v-else name="lucide:user" class="message-author-avatar" />
</div> </div>
<div class="message-data"> <div class="message-data">
<div class="message-metadata"> <div class="message-metadata">
<span class="message-author-username"> <span class="message-author-username" tabindex="0">
{{ username }} {{ username }}
</span> </span>
<span class="message-date" :title="date.toString()"> <span class="message-date" :title="date.toString()">
{{ messageDate }} {{ date.toLocaleTimeString(undefined, { timeStyle: "short" }) }}
</span> </span>
</div> </div>
<div class="message-text" v-html="sanitized"></div> <div class="message-text" v-html="sanitized" tabindex="0"></div>
</div> </div>
</div> </div>
<div v-else ref="messageElement" :id="props.last ? 'last-message' : undefined" class="message grouped-message" tabindex="0"> <div v-else ref="messageElement" :id="props.last ? 'last-message' : undefined" class="message grouped-message" :class="{ 'message-margin-bottom': props.marginBottom }">
<div class="left-column"> <div class="left-column">
<div> <span :class="{ 'invisible': dateHidden }" class="message-date side-message-date" :title="date.toString()">
<span :class="{ 'invisible': dateHidden }" class="message-date" :title="date.toString()"> {{ date.toLocaleTimeString(undefined, { timeStyle: "short" }) }}
{{ messageDate }}
</span> </span>
</div> </div>
</div>
<div class="message-data"> <div class="message-data">
<div class="message-text" v-html="sanitized"></div> <div class="message-text" :class="$style['message-text']" v-html="sanitized" tabindex="0"></div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { parseInline } from 'marked'; import { parse } from 'marked';
const props = defineProps<{ const props = defineProps<{
class?: string, class?: string,
@ -46,38 +44,20 @@ const props = defineProps<{
last: boolean last: boolean
}>(); }>();
const messageDate = ref<string>();
const messageElement = ref<HTMLDivElement>(); const messageElement = ref<HTMLDivElement>();
const dateHidden = ref<boolean>(true); const dateHidden = ref<boolean>(true);
const date = new Date(props.timestamp); const date = new Date(props.timestamp);
let dateHour = date.getHours();
let dateMinute = date.getMinutes();
if (props.format == "12") {
if (dateHour > 12) {
dateHour = dateHour - 12;
messageDate.value = `${dateHour}:${dateMinute < 10 ? "0" + dateMinute : dateMinute} PM`
} else {
if (dateHour == 0) {
dateHour = 12;
}
messageDate.value = `${dateHour}:${dateMinute < 10 ? "0" + dateMinute : dateMinute} ${dateHour >= 0 && dateHour < 13 ? "AM" : "PM"}`
}
} else {
messageDate.value = `${dateHour}:${dateMinute < 10 ? "0" + dateMinute : dateMinute}`
}
console.log("message:", props.text); console.log("message:", props.text);
console.log("author:", props.username); console.log("author:", props.username);
const sanitized = ref<string>(); const sanitized = ref<string>();
onMounted(async () => { onMounted(async () => {
const parsed = await parseInline(props.text, {gfm: true }); const parsed = await parse(props.text, { gfm: true });
sanitized.value = DOMPurify.sanitize(parsed, { ALLOWED_TAGS: ["strong", "em", "br", "blockquote", "code", "ul", "ol", "li", "a"] }); sanitized.value = DOMPurify.sanitize(parsed, { ALLOWED_TAGS: ["strong", "em", "br", "blockquote", "code", "ul", "ol", "li", "a", "h1", "h2", "h3", "h4", "h5", "h6"] });
console.log("adding listeners") console.log("adding listeners")
await nextTick(); await nextTick();
messageElement.value?.addEventListener("mouseenter", (e: Event) => { messageElement.value?.addEventListener("mouseenter", (e: Event) => {
@ -101,14 +81,24 @@ onMounted(async () => {
text-align: left; text-align: left;
/* border: 1px solid lightcoral; */ /* border: 1px solid lightcoral; */
display: grid; display: grid;
grid-template-columns: 1fr 19fr; grid-template-columns: 2dvw 1fr;
align-items: center; align-items: center;
column-gap: 1dvw;
width: 100%;
}
.message:hover {
background-color: rgb(20, 20, 20);
} }
.normal-message { .normal-message {
margin-top: 1dvh; margin-top: 1dvh;
} }
.grouped-message {
margin-top: .3em;
}
#last-message { #last-message {
margin-bottom: 2dvh; margin-bottom: 2dvh;
} }
@ -124,8 +114,8 @@ onMounted(async () => {
margin-left: .5dvw; margin-left: .5dvw;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1dvh; height: fit-content;
height: 100%; width: 100%;
} }
.message-author { .message-author {
@ -134,15 +124,15 @@ onMounted(async () => {
} }
.message-author-avatar { .message-author-avatar {
height: 2.3em; width: 100%;
width: 2.3em;
border-radius: 50%; border-radius: 50%;
} }
.left-column { .left-column {
margin-right: .5dvw; display: flex;
text-align: center; text-align: center;
align-content: center; white-space: nowrap;
} }
.author-username { .author-username {
@ -156,6 +146,13 @@ onMounted(async () => {
cursor: default; cursor: default;
} }
.side-message-date {
font-size: .625em;
display: flex;
align-items: center;
align-content: center;
}
/* /*
.message-date-tooltip { .message-date-tooltip {
height: 20px;; height: 20px;;
@ -163,3 +160,15 @@ onMounted(async () => {
} }
*/ */
</style> </style>
<style module>
.message-text ul, h1, h2, h3, h4, h5, h6 {
padding-top: 1dvh;
padding-bottom: 1dvh;
margin: 0;
}
.message-text ul {
padding-left: 2dvw;
}
</style>

View file

@ -20,7 +20,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { MessageResponse } from '~/types/interfaces'; import type { MessageResponse, ScrollPosition } from '~/types/interfaces';
import scrollToBottom from '~/utils/scrollToBottom'; import scrollToBottom from '~/utils/scrollToBottom';
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>(); const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>();
@ -101,6 +101,11 @@ if (messagesRes) {
} }
} }
function pushMessage(message: MessageResponse) {
groupMessage(message);
messages.value.push(message);
}
const messages = ref<MessageResponse[]>([]); const messages = ref<MessageResponse[]>([]);
const messageInput = ref<string>(); const messageInput = ref<string>();
@ -137,14 +142,13 @@ if (accessToken && apiBase) {
console.log("message uuid:", event.data.uuid); console.log("message uuid:", event.data.uuid);
const parsedData = JSON.parse(event.data); const parsedData = JSON.parse(event.data);
groupMessage(parsedData);
console.log("parsed message type:", messagesType.value[parsedData.uuid]); console.log("parsed message type:", messagesType.value[parsedData.uuid]);
console.log("parsed message timestamp:", messageTimestamps.value[parsedData.uuid]); console.log("parsed message timestamp:", messageTimestamps.value[parsedData.uuid]);
messages.value.push(parsedData); pushMessage(parsedData);
await nextTick(); await nextTick();
if (messagesElement.value) { if (messagesElement.value) {
console.log("scrolling to bottom"); console.log("scrolling to bottom");
scrollToBottom(messagesElement); scrollToBottom(messagesElement.value);
} }
}); });
@ -168,7 +172,7 @@ const route = useRoute();
onMounted(async () => { onMounted(async () => {
if (import.meta.server) return; if (import.meta.server) return;
if (messagesElement.value) { if (messagesElement.value) {
scrollToBottom(messagesElement); scrollToBottom(messagesElement.value);
let fetched = false; let fetched = false;
const amount = messages.value.length; const amount = messages.value.length;
let offset = messages.value.length; let offset = messages.value.length;
@ -203,14 +207,40 @@ onMounted(async () => {
} }
}); });
let scrollPosition = ref<Record<string, ScrollPosition>>({});
onActivated(async () => {
await nextTick();
console.log("scroll activated");
if (messagesElement.value) {
if (scrollPosition.value[route.params.channelId as string]) {
console.log("saved scroll position:", scrollPosition.value);
setScrollPosition(messagesElement.value, scrollPosition.value[route.params.channelId as string]);
console.log("scrolled to saved scroll position");
} else {
scrollToBottom(messagesElement.value);
console.log("scrolled to bottom");
}
}
});
const router = useRouter();
router.beforeEach((to, from, next) => {
console.log("scroll hi");
if (messagesElement.value && from.params.channelId) {
scrollPosition.value[from.params.channelId as string] = getScrollPosition(messagesElement.value)
console.log("set saved scroll position to:", scrollPosition.value);
}
next()
})
</script> </script>
<style scoped> <style scoped>
#message-area { #message-area {
display: grid; display: grid;
grid-template-columns: 1fr;
grid-template-rows: 8fr 1fr; grid-template-rows: 8fr 1fr;
justify-content: space-between;
padding-left: 1dvw; padding-left: 1dvw;
padding-right: 1dvw; padding-right: 1dvw;
overflow: hidden; overflow: hidden;
@ -247,7 +277,6 @@ onMounted(async () => {
overflow-y: scroll; overflow-y: scroll;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1dvh;
padding-left: 1dvw; padding-left: 1dvw;
padding-right: 1dvw; padding-right: 1dvw;
} }

View file

@ -96,13 +96,17 @@ function toggleInvitePopup(e: Event) {
} }
#members-list { #members-list {
display: grid; display: flex;
grid-template-columns: auto; flex-direction: column;
overflow-y: scroll;
padding-left: 1dvw;
padding-right: 1dvw;
margin-top: 1dvh;
} }
.member-item { .member-item {
display: grid; display: grid;
grid-template-columns: 2dvw auto; grid-template-columns: 2dvw 1fr;
margin-top: .5em; margin-top: .5em;
margin-bottom: .5em; margin-bottom: .5em;
gap: 1em; gap: 1em;

View file

@ -72,3 +72,14 @@ export interface StatsResponse {
email_verification_required: boolean, email_verification_required: boolean,
build_number: string build_number: string
} }
export interface ScrollPosition {
scrollHeight: number,
scrollWidth: number,
scrollTop: number,
scrollLeft: number
offsetHeight: number,
offsetWidth: number,
offsetTop: number,
offsetLeft: number
}

View file

@ -9,8 +9,6 @@ export default async <T>(path: string, options: NitroFetchOptions<string> = {})
path = path.slice(0, path.lastIndexOf("/")); path = path.slice(0, path.lastIndexOf("/"));
} }
console.log("formatted path:", path); console.log("formatted path:", path);
const accessToken = useCookie("access_token");
console.log("access token:", accessToken.value);
const apiBase = useCookie("api_base").value; const apiBase = useCookie("api_base").value;
const apiVersion = useRuntimeConfig().public.apiVersion; const apiVersion = useRuntimeConfig().public.apiVersion;
console.log("heyoooo") console.log("heyoooo")
@ -21,10 +19,14 @@ export default async <T>(path: string, options: NitroFetchOptions<string> = {})
} }
console.log("path:", path) console.log("path:", path)
const { revoke, refresh } = useAuth(); const { revoke, refresh } = useAuth();
console.log("access token 2:", accessToken.value);
let headers: HeadersInit = {}; let headers: HeadersInit = {};
let reauthFailed = false;
while (!reauthFailed) {
const accessToken = useCookie("access_token");
console.log("access token:", accessToken.value);
if (accessToken.value) { if (accessToken.value) {
headers = { headers = {
...options.headers, ...options.headers,
@ -35,9 +37,6 @@ export default async <T>(path: string, options: NitroFetchOptions<string> = {})
...options.headers ...options.headers
}; };
} }
let reauthFailed = false;
while (!reauthFailed) {
try { try {
console.log("fetching:", URL.parse(apiBase + path)); console.log("fetching:", URL.parse(apiBase + path));
const res = await $fetch<T>(URL.parse(apiBase + path)!.href, { const res = await $fetch<T>(URL.parse(apiBase + path)!.href, {
@ -74,9 +73,10 @@ export default async <T>(path: string, options: NitroFetchOptions<string> = {})
console.log("Path is refresh endpoint, throwing error"); console.log("Path is refresh endpoint, throwing error");
throw error; throw error;
} }
} } else {
console.log("throwing error"); console.log("throwing error:", error);
throw error; throw error;
} }
} }
}
} }

View file

@ -0,0 +1,14 @@
import type { ScrollPosition } from "~/types/interfaces";
export default (element: HTMLElement): ScrollPosition => {
return {
scrollHeight: element.scrollHeight,
scrollWidth: element.scrollWidth,
scrollTop: element.scrollTop,
scrollLeft: element.scrollLeft,
offsetHeight: element.offsetHeight,
offsetWidth: element.offsetWidth,
offsetTop: element.offsetTop,
offsetLeft: element.offsetLeft
};
}

View file

@ -1,6 +1,6 @@
export default (element: Ref<HTMLElement | undefined, HTMLElement | undefined>) => { export default (element: HTMLElement) => {
if (element.value) { if (element) {
element.value.scrollTo({ top: element.value.scrollHeight }); element.scrollTo({ top: element.scrollHeight });
return; return;
} }
} }

View file

@ -0,0 +1,5 @@
import type { ScrollPosition } from "~/types/interfaces";
export default (element: HTMLElement, scrollPosition: ScrollPosition) => {
return element.scrollTo({ top: scrollPosition.scrollTop, left: scrollPosition.scrollLeft });
}