Compare commits

...

14 commits

Author SHA1 Message Date
59c9acdc9e
feat: remove loading on this page in favor of layout-wide loading
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-05-29 02:36:44 +02:00
3b1581d950
feat: add loading on client layout 2025-05-29 02:36:10 +02:00
a324cc9300
style: fix indentation 2025-05-29 02:33:48 +02:00
a439f9481a
feat: manage loading state in auth middleware 2025-05-28 23:38:52 +02:00
379b85db4e
feat: remove Loading component from app.vue 2025-05-28 23:38:04 +02:00
751bdcfd9a
Merge branch 'main' into dev 2025-05-28 22:45:05 +02:00
a5f0e19716
feat: added check for if refresh returned access token 2025-05-28 22:40:26 +02:00
7ad2b6f299
style: fix indentation 2025-05-28 22:38:54 +02:00
6548f9329e
fix: redirect query set to undefined due to missing if statement check 2025-05-28 22:38:11 +02:00
e6bff0042d
feat: switched from setting height to 100% to using 100dvh 2025-05-28 22:37:25 +02:00
585e79dac6
feat: set baseURL back to root rather than /web 2025-05-28 22:36:14 +02:00
93b7bf9154
Merge branch 'main' of ssh://git.gorb.app:2022/gorb/frontend
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-05-28 20:35:09 +02:00
19fcccfb5b
feat: make chat scroll position to be at the bottom upon load 2025-05-28 20:20:28 +02:00
9981bc4158 feat: add embed field
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-05-27 21:06:02 +02:00
10 changed files with 197 additions and 157 deletions

View file

@ -1,14 +1,11 @@
<template> <template>
<div> <div>
<Loading v-if="loading" />
<NuxtPage /> <NuxtPage />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const loading = useState("loading");
</script> </script>
<style> <style>
@ -18,7 +15,6 @@ body {
box-sizing: border-box; box-sizing: border-box;
color: rgb(190, 190, 190); color: rgb(190, 190, 190);
background-color: rgb(30, 30, 30); background-color: rgb(30, 30, 30);
height: 100%;
margin: 0; margin: 0;
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<div id="message-area"> <div id="message-area">
<div id="messages"> <div id="messages" ref="messagesElement">
<Message v-for="message of messages" :username="displayNames[message.user_uuid]" :text="message.message" <Message v-for="message of messages" :username="displayNames[message.user_uuid]" :text="message.message"
:timestamp="uuidToTimestamp(message.uuid)" format="12" /> :timestamp="uuidToTimestamp(message.uuid)" format="12" />
</div> </div>
@ -29,7 +29,7 @@ if (messagesRes && props.reverse) {
messagesRes.reverse(); messagesRes.reverse();
} }
const messages = ref(messagesRes ?? []); const messages = ref<MessageResponse[]>([]);
const displayNames = ref<Record<string, string>>({}); const displayNames = ref<Record<string, string>>({});
@ -37,6 +37,18 @@ const route = useRoute();
const messageInput = ref<string>(); const messageInput = ref<string>();
const messagesElement = ref<HTMLDivElement>();
if (messagesRes) messages.value = messagesRes;
const displayNamesArr: Record<string, string> = {};
for (const message of messages.value) {
if (!displayNamesArr[message.user_uuid]) {
const displayName = await getDisplayName(message.user_uuid);
displayNamesArr[message.user_uuid] = displayName;
}
}
displayNames.value = displayNamesArr;
const accessToken = useCookie("access_token").value; const accessToken = useCookie("access_token").value;
const apiBase = useCookie("api_base").value; const apiBase = useCookie("api_base").value;
const { refresh } = useAuth(); const { refresh } = useAuth();
@ -88,14 +100,7 @@ function sendMessage(e: Event) {
} }
onMounted(async () => { onMounted(async () => {
const displayNamesArr: Record<string, string> = {}; messagesElement.value?.scrollTo({ top: messagesElement.value.scrollHeight });
for (const message of messages.value) {
if (!displayNamesArr[message.user_uuid]) {
const displayName = await getDisplayName(message.user_uuid);
displayNamesArr[message.user_uuid] = displayName;
}
}
displayNames.value = displayNamesArr;
}); });
</script> </script>
@ -117,17 +122,9 @@ onMounted(async () => {
#message-box { #message-box {
border: 1px solid rgb(70, 70, 70); border: 1px solid rgb(70, 70, 70);
padding-bottom: 1dvh;
padding-top: 1dvh;
margin-bottom: 1dvh; margin-bottom: 1dvh;
height: 7%;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
#message-form {
height: 50%;
width: 100%;
} }
#message-input { #message-input {

View file

@ -60,17 +60,21 @@ export const useAuth = () => {
async function refresh() { async function refresh() {
console.log("refreshing"); console.log("refreshing");
const res = await fetchWithApi("/auth/refresh", { const res = await fetchWithApi("/auth/refresh", {
method: "POST" method: "POST"
}) as any; }) as any;
console.log("finished refreshing:", res); console.log("finished refreshing:", res);
accessToken.value = res?.access_token; if (res.access_token) {
console.log("set new access token"); accessToken.value = res.access_token;
console.log("set new access token");
} else {
console.log("refresh didn't return access token");
}
} }
async function fetchUser() { async function fetchUser() {
if (!accessToken.value) return; if (!accessToken.value) return;
console.log("fetchuser access token:", accessToken.value); console.log("fetchuser access token:", accessToken.value);
const res = await fetchWithApi("/users/me") as UserResponse; const res = await fetchWithApi("/users/me") as UserResponse;
user.value = res; user.value = res;
return user.value; return user.value;

View file

@ -1,55 +1,55 @@
<template> <template>
<Loading v-if="!mounted" /> <NuxtLayout>
<div v-else id="root-container" style="margin-top: 5dvh;"> <div id="root-container" style="margin-top: 5dvh;">
<div id="main-container"> <div id="main-container">
<div v-if="!instanceUrl"> <div v-if="!instanceUrl">
<div v-if="instanceError" style="color: red;"> <div v-if="instanceError" style="color: red;">
{{ instanceError }} {{ instanceError }}
</div>
<form @submit="selectInstance">
<div>
<label for="instance-url">Instance URL</label>
<br>
<input type="url" name="instance-url" id="instance-url" required v-model="instanceUrlInput">
</div>
<div>
<button type="submit">Next</button>
</div>
</form>
</div> </div>
<form @submit="selectInstance"> <div v-else id="auth-form-container">
<div> <slot />
<label for="instance-url">Instance URL</label>
<br>
<input type="url" name="instance-url" id="instance-url" required v-model="instanceUrlInput">
</div>
<div>
<button type="submit">Next</button>
</div>
</form>
</div>
<div v-else id="auth-form-container">
<slot />
</div>
<div v-if="auth.accessToken.value">
You're logged in!
<form @submit="logout">
<div>
<label for="logout-password">Password</label>
<br>
<input type="password" name="logout-password" id="logout-password" v-model="form.password" required>
</div>
<div>
<button type="submit">Log out</button>
</div>
</form>
<div>
<button @click="refresh">Refresh</button>
</div> </div>
<div> <div v-if="auth.accessToken.value">
<button @click="showUser">Show user</button> You're logged in!
</div> <form @submit="logout">
<div> <div>
<button @click="getUser">Get me</button> <label for="logout-password">Password</label>
<br>
<input type="password" name="logout-password" id="logout-password" v-model="form.password" required>
</div>
<div>
<button type="submit">Log out</button>
</div>
</form>
<div>
<button @click="refresh">Refresh</button>
</div>
<div>
<button @click="showUser">Show user</button>
</div>
<div>
<button @click="getUser">Get me</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </NuxtLayout>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { FetchError } from 'ofetch'; import { FetchError } from 'ofetch';
const mounted = ref(false);
const redirectTo = useRoute().query.redirect_to; const redirectTo = useRoute().query.redirect_to;
const apiVersion = useRuntimeConfig().public.apiVersion; const apiVersion = useRuntimeConfig().public.apiVersion;
@ -64,7 +64,6 @@ if (auth.accessToken.value) {
} }
onMounted(() => { onMounted(() => {
mounted.value = true;
const cookie = useCookie("instance_url").value; const cookie = useCookie("instance_url").value;
instanceUrl.value = cookie; instanceUrl.value = cookie;
console.log(cookie); console.log(cookie);

View file

@ -1,5 +1,6 @@
<template> <template>
<div id="client-root"> <Loading v-show="loading" />
<div :class="{ hidden: loading, visible: !loading }" id="client-root">
<div id="homebar"> <div id="homebar">
<div class="homebar-item"> <div class="homebar-item">
main bar main bar
@ -21,6 +22,8 @@
<script lang="ts" setup> <script lang="ts" setup>
const loading = useState("loading", () => false);
const servers = [ const servers = [
{ {
name: "Test", name: "Test",
@ -90,11 +93,21 @@ function sendMessage(e: Event) {
<style> <style>
#client-root { #client-root {
/* border: 1px solid white; */ /* border: 1px solid white; */
height: 100%; height: 100dvh;
display: grid; display: grid;
grid-template-columns: 1fr 4fr 18fr 4fr; grid-template-columns: 1fr 4fr 18fr 4fr;
grid-template-rows: 4dvh auto; grid-template-rows: 4dvh auto;
text-align: center; text-align: center;
}
.hidden {
opacity: 0%;
}
.visible {
opacity: 100%;
transition-duration: 500ms;
} }
#homebar { #homebar {
@ -114,7 +127,6 @@ function sendMessage(e: Event) {
#__nuxt { #__nuxt {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
height: 100%;
} }
.grid-column { .grid-column {

View file

@ -1,5 +1,6 @@
export default defineNuxtRouteMiddleware(async (to, from) => { export default defineNuxtRouteMiddleware(async (to, from) => {
console.log("to.path:", to.path); console.log("to.path:", to.path);
const loading = useState("loading");
const accessToken = useCookie("access_token").value; const accessToken = useCookie("access_token").value;
if (["/login", "/register"].includes(to.path)) { if (["/login", "/register"].includes(to.path)) {
if (accessToken) { if (accessToken) {
@ -9,9 +10,15 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
}; };
if (!accessToken) { if (!accessToken) {
loading.value = true;
console.log("set loading to true");
const { refresh } = useAuth(); const { refresh } = useAuth();
console.log("hi"); console.log("hi");
await refresh(); await refresh();
return await navigateTo("/login"); const query = new URLSearchParams();
query.set("redirect_to", to.path);
loading.value = false;
console.log("set loading to false");
return await navigateTo("/login" + (query ?? ""));
} }
}) })

View file

@ -10,7 +10,19 @@ export default defineNuxtConfig({
E.g.: baseURL set to "/web" would host at https://gorb.app/web E.g.: baseURL set to "/web" would host at https://gorb.app/web
Default is "/" (aka root), which hosts at https://gorb.app/ Default is "/" (aka root), which hosts at https://gorb.app/
*/ */
baseURL: "/web" baseURL: "/",
head: {
title: 'Gorb',
// this is purely used to embed in that other chat app, and similar stuff
meta: [
{ property: 'og:title', content: 'Gorb' },
{ property: 'og:description', content: 'Gorb is an open-source (and eventually federated) chat platform where you can join and chat in servers, chat privately in DMs, and more.' },
{ property: 'og:url', content: 'https://gorb.app/web' },
{ property: 'og:type', content: 'website' },
{ property: 'og:site_name', content: 'Gorb' },
{ property: 'theme-color', content: "#df5f0b" }
]
}
}, },
runtimeConfig: { runtimeConfig: {
public: { public: {

View file

@ -123,10 +123,10 @@ async function register(e: Event) {
e.preventDefault(); e.preventDefault();
console.log("Sending registration data"); console.log("Sending registration data");
try { try {
await auth.register(form.username, form.email, form.password); await auth.register(form.username, form.email, form.password);
return await navigateTo(query.redirect_to); return await navigateTo(query.redirect_to);
} catch (error) { } catch (error) {
console.error("Error registering:", error); console.error("Error registering:", error);
} }
//return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string); //return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string);
} }

View file

@ -39,25 +39,20 @@
const route = useRoute(); const route = useRoute();
const server: GuildResponse | undefined = await fetchWithApi(`servers/${route.params.serverId}`); const loading = useState("loading");
const channels: ChannelResponse[] | undefined = await fetchWithApi(
`servers/${route.params.serverId}/channels`
);
const channel: ChannelResponse | undefined = await fetchWithApi(
route.path
);
const channelUrlPath = `servers/${route.params.serverId}/channels/${route.params.channelId}`; const channelUrlPath = `servers/${route.params.serverId}/channels/${route.params.channelId}`;
console.log("channel:", channel); const server = ref<GuildResponse | undefined>();
const channels = ref<ChannelResponse[] | undefined>();
const channel = ref<ChannelResponse | undefined>();
const showInvitePopup = ref(false); const showInvitePopup = ref(false);
import type { ChannelResponse, GuildResponse, MessageResponse } from "~/types/interfaces"; import type { ChannelResponse, GuildResponse, MessageResponse } from "~/types/interfaces";
//const servers = await fetchWithApi("/servers") as { uuid: string, name: string, description: string }[]; //const servers = await fetchWithApi("/servers") as { uuid: string, name: string, description: string }[];
//console.log("servers:", servers); //console.log("channelid: servers:", servers);
const members = [ const members = [
{ {
id: "3287484395", id: "3287484395",
@ -106,6 +101,24 @@ const members = [
} }
]; ];
onMounted(async () => {
loading.value = true;
console.log("channelid: set loading to true");
server.value = await fetchWithApi(`servers/${route.params.serverId}`);
channels.value = await fetchWithApi(
`servers/${route.params.serverId}/channels`
);
channel.value = await fetchWithApi(
route.path
);
console.log("channelid: channel:", channel);
await sleep(3000);
loading.value = false;
console.log("channelid: set loading to false");
});
function showServerSettings() { } function showServerSettings() { }
function toggleInvitePopup(e: Event) { function toggleInvitePopup(e: Event) {

View file

@ -9,74 +9,74 @@ 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"); const accessToken = useCookie("access_token");
console.log("access token:", accessToken.value); 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")
console.log("apiBase:", apiBase); console.log("apiBase:", apiBase);
if (!apiBase) { if (!apiBase) {
console.log("no api base"); console.log("no api base");
return; return;
} }
console.log("path:", path) console.log("path:", path)
const { revoke, refresh } = useAuth(); const { revoke, refresh } = useAuth();
console.log("access token 2:", accessToken.value); console.log("access token 2:", accessToken.value);
let headers: HeadersInit = {}; let headers: HeadersInit = {};
if (accessToken.value) { if (accessToken.value) {
headers = { headers = {
...options.headers, ...options.headers,
"Authorization": `Bearer ${accessToken.value}` "Authorization": `Bearer ${accessToken.value}`
}; };
} else { } else {
headers = { headers = {
...options.headers ...options.headers
}; };
} }
let reauthFailed = false; let reauthFailed = false;
while (!reauthFailed) { 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, {
...options, ...options,
headers, headers,
credentials: "include" credentials: "include"
}); });
return res; return res;
} catch (error: any) { } catch (error: any) {
console.error("Error fetching resource"); console.error("Error fetching resource");
if (error?.response?.status === 401) { if (error?.response?.status === 401) {
console.log("Error status is 401"); console.log("Error status is 401");
if (!path.startsWith("/auth/refresh")) { if (!path.startsWith("/auth/refresh")) {
console.log("Path is not refresh endpoint"); console.log("Path is not refresh endpoint");
try { try {
console.log("Trying to refresh"); console.log("Trying to refresh");
await refresh(); await refresh();
console.log("Successfully refreshed token"); console.log("Successfully refreshed token");
} catch (error: any) { } catch (error: any) {
console.log("Failed to refresh token"); console.log("Failed to refresh token");
if (error?.response?.status === 401) { if (error?.response?.status === 401) {
console.log("Refresh returned 401"); console.log("Refresh returned 401");
reauthFailed = true; reauthFailed = true;
console.log("Revoking"); console.log("Revoking");
await revoke(); await revoke();
console.log("Redirecting to login"); console.log("Redirecting to login");
await navigateTo("/login"); await navigateTo("/login");
console.log("redirected"); console.log("redirected");
return; return;
} }
} }
} else { } else {
console.log("Path is refresh endpoint, throwing error"); console.log("Path is refresh endpoint, throwing error");
throw error; throw error;
} }
} }
console.log("throwing error"); console.log("throwing error");
throw error; throw error;
} }
} }
} }