Compare commits
18 commits
main
...
settings-p
Author | SHA1 | Date | |
---|---|---|---|
1302826ba8 | |||
010472c83d | |||
acca8468f0 | |||
61df171c59 | |||
2c76edaa32 | |||
ccefc8ca19 | |||
cca348b476 | |||
c0f4697d00 | |||
774e10d68c | |||
d49d533724 | |||
3899843a7c | |||
d22e77ed14 | |||
67e10a4387 | |||
263c823ceb | |||
82fde5671d | |||
d986f601de | |||
d85eb03ad0 | |||
a56e12149b |
9 changed files with 153 additions and 75 deletions
10
app.vue
10
app.vue
|
@ -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 {
|
||||||
|
|
|
@ -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,10 +146,29 @@ 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;;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
</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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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,23 +19,24 @@ 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 = {};
|
||||||
|
|
||||||
if (accessToken.value) {
|
|
||||||
headers = {
|
|
||||||
...options.headers,
|
|
||||||
"Authorization": `Bearer ${accessToken.value}`
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
headers = {
|
|
||||||
...options.headers
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let reauthFailed = false;
|
let reauthFailed = false;
|
||||||
while (!reauthFailed) {
|
while (!reauthFailed) {
|
||||||
|
const accessToken = useCookie("access_token");
|
||||||
|
console.log("access token:", accessToken.value);
|
||||||
|
if (accessToken.value) {
|
||||||
|
headers = {
|
||||||
|
...options.headers,
|
||||||
|
"Authorization": `Bearer ${accessToken.value}`
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
headers = {
|
||||||
|
...options.headers
|
||||||
|
};
|
||||||
|
}
|
||||||
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:", error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
console.log("throwing error");
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
utils/getScrollPosition.ts
Normal file
14
utils/getScrollPosition.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5
utils/setScrollPosition.ts
Normal file
5
utils/setScrollPosition.ts
Normal 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 });
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue