Merge branch 'main' into guild-settings
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed
This commit is contained in:
commit
76bef24a9a
68 changed files with 2620 additions and 305 deletions
|
@ -5,6 +5,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
await navigateTo("/me/", { replace: true })
|
||||
|
||||
definePageMeta({
|
||||
layout: "client"
|
||||
|
|
|
@ -44,10 +44,9 @@ const apiBase = useCookie("api_base");
|
|||
|
||||
if (apiBase.value) {
|
||||
console.log("apiBase:", apiBase.value);
|
||||
const statsUrl = new URL("/stats", apiBase.value).href;
|
||||
const { status, data } = await useFetch<StatsResponse>(statsUrl);
|
||||
if (status.value == "success" && data.value) {
|
||||
registrationEnabled.value = data.value.registration_enabled;
|
||||
const stats = await useApi().fetchInstanceStats(apiBase.value);
|
||||
if (stats) {
|
||||
registrationEnabled.value = stats.registration_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
15
pages/me/[userId].vue
Normal file
15
pages/me/[userId].vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<NuxtLayout name="client">
|
||||
<DirectMessagesSidebar />
|
||||
<MessageArea channel-url="channels/01970e8c-a09c-76a0-9c98-80a43364bea7"/> <!-- currently just links to the default channel -->
|
||||
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
56
pages/me/friends.vue
Normal file
56
pages/me/friends.vue
Normal file
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<NuxtLayout name="client">
|
||||
<DirectMessagesSidebar />
|
||||
<div :id="$style['page-content']">
|
||||
<div :id="$style['navigation-bar']">
|
||||
<Button :text="`All Friends – ${friends?.length}`" variant="neutral" :callback="() => updateFilter('all')" />
|
||||
<Button :text="`Online – ${0}`" variant="neutral" :callback="() => updateFilter('online')" />
|
||||
<Button :text="`Pending – ${0}`" variant="neutral" :callback="() => updateFilter('pending')" />
|
||||
<Button text="Add Friend" variant="normal" :callback="() => updateFilter('add')" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<AddFriend v-if="filter == 'add'"></AddFriend>
|
||||
<FriendsList v-else :variant="filter"></FriendsList>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue';
|
||||
import Button from '~/components/UserInterface/Button.vue';
|
||||
import AddFriend from '~/components/Me/AddFriend.vue';
|
||||
import FriendsList from '~/components/Me/FriendsList.vue';
|
||||
|
||||
const { fetchFriends } = useApi();
|
||||
|
||||
let filter = ref("all");
|
||||
|
||||
const friends = await fetchFriends()
|
||||
|
||||
function updateFilter(newFilter: string) {
|
||||
filter.value = newFilter;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
#page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
margin: .75em;
|
||||
}
|
||||
|
||||
#navigation-bar {
|
||||
display: flex;
|
||||
align-items: left;
|
||||
text-align: left;
|
||||
flex-direction: row;
|
||||
|
||||
gap: .5em;
|
||||
}
|
||||
</style>
|
13
pages/me/index.vue
Normal file
13
pages/me/index.vue
Normal file
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<NuxtLayout name="client">
|
||||
<DirectMessagesSidebar />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<NuxtLayout>
|
||||
<form v-if="registrationEnabled" @submit="register">
|
||||
<form v-if="registrationEnabled && !registrationSubmitted && !showEmailVerificationScreen" @submit="register">
|
||||
<div>
|
||||
<!--
|
||||
<span class="form-error" v-if="errors.username.length > 0">
|
||||
|
@ -32,32 +32,87 @@
|
|||
<button type="submit">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else-if="registrationEnabled && (registrationSubmitted || showEmailVerificationScreen) && !emailSent">
|
||||
<p v-if="emailVerificationRequired">
|
||||
This instance requires email verification to use it.
|
||||
<br><br>
|
||||
<span v-if="registrationSubmitted">
|
||||
Please open the link sent to your email.
|
||||
</span>
|
||||
<span v-else-if="showEmailVerificationScreen">
|
||||
Please click on the link you've already received, or click on the button below to receive a new email.
|
||||
</span>
|
||||
</p>
|
||||
<p v-else>
|
||||
Would you like to verify your email?
|
||||
<!--
|
||||
<br>
|
||||
This is required for resetting your password and making other important changes.
|
||||
-->
|
||||
</p>
|
||||
<Button v-if="(!emailVerificationRequired || showEmailVerificationScreen) && !emailSent" text="Send email" variant="neutral" :callback="sendEmail"></Button>
|
||||
</div>
|
||||
<div v-else-if="emailSent">
|
||||
<p>
|
||||
An email has been sent and should arrive soon.
|
||||
<br>
|
||||
If you don't see it in your inbox, try checking the spam folder.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h3>This instance has disabled registration.</h3>
|
||||
</div>
|
||||
<div>
|
||||
Already have an account? <NuxtLink :href="loginUrl">Log in</NuxtLink>!
|
||||
<div v-if="loggedIn">
|
||||
<Button text="Log out" variant="scary" :callback="() => {}"></Button>
|
||||
</div>
|
||||
<div v-else>
|
||||
Already have an account? <NuxtLink :href="loginUrl">Log in</NuxtLink>!
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { StatsResponse } from '~/types/interfaces';
|
||||
definePageMeta({
|
||||
layout: "auth"
|
||||
})
|
||||
});
|
||||
|
||||
const registrationEnabled = useState("registrationEnabled", () => true);
|
||||
const emailVerificationRequired = useState("emailVerificationRequired", () => false);
|
||||
const registrationSubmitted = ref(false);
|
||||
const emailSent = ref(false);
|
||||
|
||||
const auth = useAuth();
|
||||
|
||||
const loggedIn = ref(await auth.getUser());
|
||||
|
||||
const query = new URLSearchParams(useRoute().query as Record<string, string>);
|
||||
|
||||
const user = await useAuth().getUser();
|
||||
|
||||
if (user?.email_verified) {
|
||||
if (query.get("redirect_to")) {
|
||||
await navigateTo(query.get("redirect_to"));
|
||||
} else {
|
||||
await navigateTo("/");
|
||||
}
|
||||
}
|
||||
|
||||
const showEmailVerificationScreen = query.get("special") == "verify_email";
|
||||
console.log("show email verification screen?", showEmailVerificationScreen);
|
||||
|
||||
const { fetchInstanceStats, sendVerificationEmail } = useApi();
|
||||
|
||||
console.log("wah");
|
||||
console.log("weoooo")
|
||||
const apiBase = useCookie("api_base");
|
||||
console.log("apiBase:", apiBase.value);
|
||||
if (apiBase.value) {
|
||||
const { status, data, error } = await useFetch<StatsResponse>(`${apiBase.value}/stats`);
|
||||
if (status.value == "success" && data.value) {
|
||||
registrationEnabled.value = data.value.registration_enabled;
|
||||
console.log("set registration enabled value to:", data.value.registration_enabled);
|
||||
const stats = await fetchInstanceStats(apiBase.value);
|
||||
if (stats) {
|
||||
registrationEnabled.value = stats.registration_enabled;
|
||||
console.log("set registration enabled value to:", stats.registration_enabled);
|
||||
emailVerificationRequired.value = stats.email_verification_required;
|
||||
console.log("set email verification required value to:", stats.email_verification_required);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,8 +145,6 @@ const errorMessages = reactive({
|
|||
*/
|
||||
|
||||
//const authStore = useAuthStore();
|
||||
const auth = useAuth();
|
||||
const query = useRoute().query as Record<string, string>;
|
||||
const searchParams = new URLSearchParams(query);
|
||||
const loginUrl = `/login?${searchParams}`
|
||||
|
||||
|
@ -133,13 +186,22 @@ async function register(e: Event) {
|
|||
console.log("Sending registration data");
|
||||
try {
|
||||
await auth.register(form.username, form.email, form.password);
|
||||
return await navigateTo(query.redirect_to);
|
||||
if (!emailVerificationRequired.value) {
|
||||
return await navigateTo(query.get("redirect_to"));
|
||||
}
|
||||
await sendVerificationEmail();
|
||||
registrationSubmitted.value = true;
|
||||
} catch (error) {
|
||||
console.error("Error registering:", error);
|
||||
}
|
||||
//return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string);
|
||||
}
|
||||
|
||||
async function sendEmail() {
|
||||
await sendVerificationEmail();
|
||||
emailSent.value = true;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -10,7 +10,7 @@
|
|||
<InvitePopup v-if="showInvitePopup" />
|
||||
</div>
|
||||
<div id="channels-list">
|
||||
<Channel v-for="channel of channels" :name="channel.name"
|
||||
<ChannelEntry v-for="channel of channels" :name="channel.name"
|
||||
:uuid="channel.uuid" :current-uuid="(route.params.channelId as string)"
|
||||
:href="`/servers/${route.params.serverId}/channels/${channel.uuid}`" />
|
||||
</div>
|
||||
|
@ -18,17 +18,14 @@
|
|||
<MessageArea :channel-url="channelUrlPath" />
|
||||
<div id="members-container">
|
||||
<div id="members-list">
|
||||
<div class="member-item" v-for="member of members" tabindex="0">
|
||||
<img v-if="member.user.avatar" class="member-avatar" :src="member.user.avatar" :alt="member.user.display_name ?? member.user.username" />
|
||||
<Icon v-else class="member-avatar" name="lucide:user" />
|
||||
<span class="member-display-name">{{ member.user.display_name ?? member.user.username }}</span>
|
||||
</div>
|
||||
<MemberEntry v-for="member of members" :member="member" tabindex="0"/>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ChannelEntry from "~/components/Guild/ChannelEntry.vue";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
|
@ -43,7 +40,7 @@ const channel = ref<ChannelResponse | undefined>();
|
|||
const showInvitePopup = ref(false);
|
||||
const showGuildSettings = ref(false);
|
||||
|
||||
import type { ChannelResponse, GuildResponse, MessageResponse } from "~/types/interfaces";
|
||||
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse } from "~/types/interfaces";
|
||||
|
||||
//const servers = await fetchWithApi("/servers") as { uuid: string, name: string, description: string }[];
|
||||
//console.log("channelid: servers:", servers);
|
||||
|
@ -74,47 +71,54 @@ function toggleInvitePopup(e: Event) {
|
|||
e.preventDefault();
|
||||
showInvitePopup.value = !showInvitePopup.value;
|
||||
}
|
||||
|
||||
function handleMemberClick(member: GuildMemberResponse) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
#middle-left-column {
|
||||
padding-left: 1dvw;
|
||||
padding-right: 1dvw;
|
||||
border-right: 1px solid rgb(70, 70, 70);
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
border-right: 1px solid var(--padding-color);
|
||||
background: var(--optional-channel-list-background);
|
||||
background-color: var(--sidebar-background-color);
|
||||
}
|
||||
|
||||
#members-container {
|
||||
padding-top: 1dvh;
|
||||
padding-left: 1dvw;
|
||||
padding-right: 1dvw;
|
||||
border-left: 1px solid rgb(70, 70, 70);
|
||||
min-width: 15rem;
|
||||
max-width: 15rem;
|
||||
border-left: 1px solid var(--padding-color);
|
||||
background: var(--optional-member-list-background);
|
||||
}
|
||||
|
||||
#members-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
padding-left: 1dvw;
|
||||
padding-right: 1dvw;
|
||||
margin-top: 1dvh;
|
||||
padding-left: 1.25em;
|
||||
padding-right: 1.25em;
|
||||
padding-top: 0.75em;
|
||||
padding-bottom: 0.75em;
|
||||
max-height: calc(100% - 0.75em * 2); /* 100% - top and bottom */
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: grid;
|
||||
grid-template-columns: 2dvw 1fr;
|
||||
display: flex;
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
gap: 1em;
|
||||
gap: .5em;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#channels-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1dvh;
|
||||
gap: .5em;
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
|
|
209
pages/settings.vue
Normal file
209
pages/settings.vue
Normal file
|
@ -0,0 +1,209 @@
|
|||
<template>
|
||||
<div id="settings-page-container">
|
||||
<div id="settings-page">
|
||||
<div id="sidebar">
|
||||
<ul>
|
||||
<p>
|
||||
<span @click="$router.go(-1)">
|
||||
<Icon class="back-button" size="2em" name="lucide:circle-arrow-left" alt="Back"></Icon>
|
||||
</span>
|
||||
</p>
|
||||
<VerticalSpacer />
|
||||
|
||||
<!-- categories and dynamic settings pages -->
|
||||
<div v-for="category in categories" :key="category.displayName">
|
||||
<h2>{{ category.displayName }}</h2>
|
||||
<li v-for="page in category.pages" :key="page.displayName" @click="selectCategory(page)"
|
||||
:class="{ 'sidebar-focus': selectedPage === page.displayName }">
|
||||
{{ page.displayName }}
|
||||
</li>
|
||||
<VerticalSpacer />
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<Button text="Log Out" :callback=logout variant="scary"></Button>
|
||||
</p>
|
||||
<VerticalSpacer />
|
||||
|
||||
<p id="links-and-socials">
|
||||
<NuxtLink href="https://git.gorb.app/gorb/frontend" title="Source"><Icon name="lucide:git-branch-plus" /></NuxtLink>
|
||||
<NuxtLink href="https://docs.gorb.app" title="Backend Documentation"><Icon name="lucide:book-open-text" /></NuxtLink>
|
||||
</p>
|
||||
|
||||
<p style="font-size: .8em; color: var(--secondary-text-color)">
|
||||
Version Hash: {{ appConfig.public.gitHash }}
|
||||
<br>
|
||||
Build Time: {{ appConfig.public.buildTimeString }}
|
||||
</p>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="sub-page">
|
||||
<component :is="currentPage.pageData" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue';
|
||||
import Button from '~/components/UserInterface/Button.vue';
|
||||
|
||||
const { logout } = useAuth()
|
||||
const appConfig = useRuntimeConfig()
|
||||
|
||||
interface Page {
|
||||
displayName: string;
|
||||
pageData: any; // is actually Component but TS is yelling at me :(
|
||||
}
|
||||
|
||||
interface Category {
|
||||
displayName: string;
|
||||
pages: Page[];
|
||||
}
|
||||
|
||||
import Profile from '~/components/Settings/UserSettings/Profile.vue';
|
||||
import Account from '~/components/Settings/UserSettings/Account.vue';
|
||||
import Privacy from '~/components/Settings/UserSettings/Privacy.vue';
|
||||
import Devices from '~/components/Settings/UserSettings/Devices.vue';
|
||||
import Connections from '~/components/Settings/UserSettings/Connections.vue';
|
||||
|
||||
import Appearance from '~/components/Settings/AppSettings/Appearance.vue';
|
||||
import Notifications from '~/components/Settings/AppSettings/Notifications.vue';
|
||||
import Keybinds from '~/components/Settings/AppSettings/Keybinds.vue';
|
||||
import Language from '~/components/Settings/AppSettings/Language.vue';
|
||||
|
||||
const settingsCategories = {
|
||||
userSettings: {
|
||||
displayName: "User Settings",
|
||||
pages: [
|
||||
{ displayName: "Profile", pageData: Profile },
|
||||
{ displayName: "Account", pageData: Account },
|
||||
{ displayName: "Privacy", pageData: Privacy },
|
||||
{ displayName: "Devices", pageData: Devices },
|
||||
{ displayName: "Connections", pageData: Connections },
|
||||
]
|
||||
},
|
||||
appSettings: {
|
||||
displayName: "App Settings",
|
||||
pages: [
|
||||
{ displayName: "Appearance", pageData: Appearance },
|
||||
{ displayName: "Notifications", pageData: Notifications },
|
||||
{ displayName: "Keybinds", pageData: Keybinds },
|
||||
{ displayName: "Language", pageData: Language },
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
const categories = Object.values(settingsCategories);
|
||||
|
||||
let currentPage = ref(categories[0].pages[0]);
|
||||
let selectedPage = ref(currentPage.value.displayName); // used to highlight the current channel
|
||||
|
||||
function selectCategory(page: Page) {
|
||||
currentPage.value = page;
|
||||
selectedPage.value = page.displayName;
|
||||
};
|
||||
|
||||
// redirects to you privacy if you go to settings#privacy
|
||||
onMounted(() => {
|
||||
const hash = window.location.hash.substring(1).toLowerCase();
|
||||
const foundPage = categories.flatMap(category => category.pages).find(page => page.displayName.toLowerCase() === hash);
|
||||
|
||||
if (foundPage) {
|
||||
currentPage.value = foundPage;
|
||||
selectedPage.value = foundPage.displayName;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#settings-page-container {
|
||||
height: 100%;
|
||||
align-content: center;
|
||||
overflow-y: hidden;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#settings-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
min-width: 25dvw;
|
||||
max-width: 25dvw;
|
||||
background: var(--optional-channel-list-background);
|
||||
background-color: var(--sidebar-background-color);
|
||||
color: var(--text-color);
|
||||
padding: 1dvh 1dvw;
|
||||
margin-left: 0;
|
||||
|
||||
overflow-y: auto;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#sidebar h2 {
|
||||
font-size: 0.95em;
|
||||
padding: 0 0.8dvw;
|
||||
}
|
||||
|
||||
#sidebar ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#sidebar li {
|
||||
border-radius: 8px;
|
||||
padding: 0.8dvh 0.8dvw;
|
||||
font-size: 1.4em;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
#sidebar p {
|
||||
margin: 2dvh 0.8dvw;
|
||||
}
|
||||
|
||||
.sidebar-focus {
|
||||
background-color: var(--sidebar-highlighted-background-color);
|
||||
}
|
||||
|
||||
#sidebar li:hover {
|
||||
background-color: var(--sidebar-highlighted-background-color);
|
||||
}
|
||||
|
||||
#sub-page {
|
||||
flex-grow: 1;
|
||||
min-width: 70dvw;
|
||||
max-width: 70dvw;
|
||||
padding-left: 1.5rem;
|
||||
margin-right: auto;
|
||||
|
||||
overflow-y: auto;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
cursor: pointer;
|
||||
color: var(--primary-color);
|
||||
transition: color 100ms;
|
||||
}
|
||||
|
||||
.back-button:hover{
|
||||
color: var(--primary-highlighted-color);
|
||||
}
|
||||
|
||||
#links-and-socials * {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
/* applies to child pages too */
|
||||
:deep(.subtitle) {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
font-weight: 800;
|
||||
margin: 4dvh 0 0.5dvh 0.25dvw;
|
||||
}
|
||||
</style>
|
|
@ -15,6 +15,12 @@ const token = useRoute().query.token;
|
|||
try {
|
||||
const res = await fetchWithApi("/auth/verify-email", { query: { token } });
|
||||
console.log("hi");
|
||||
const query = useRoute().query;
|
||||
if (query.redirect_to) {
|
||||
await navigateTo(`/?redirect_to=${query.redirect_to}`);
|
||||
} else {
|
||||
await navigateTo("/");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error verifying email:", error);
|
||||
errorMessage.value = error;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue