diff --git a/README.md b/README.md index b86a3a8..ecdb2d5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Nuxt Minimal Starter + + Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. ## Setup diff --git a/app.vue b/app.vue index fbc6572..686a15e 100644 --- a/app.vue +++ b/app.vue @@ -6,24 +6,62 @@ \ No newline at end of file diff --git a/components/Channel.vue b/components/Guild/ChannelEntry.vue similarity index 87% rename from components/Channel.vue rename to components/Guild/ChannelEntry.vue index 1828641..98f210e 100644 --- a/components/Channel.vue +++ b/components/Guild/ChannelEntry.vue @@ -23,19 +23,19 @@ const isCurrentChannel = props.uuid == props.currentUuid; .channel-list-link { text-decoration: none; color: inherit; - padding-left: .5dvw; - padding-right: .5dvw; + padding-left: .25em; + padding-right: .25em; } .channel-list-link-container { text-align: left; display: flex; - height: 4dvh; + height: 1.5em; white-space: nowrap; align-items: center; } .current-channel { - background-color: rgb(70, 70, 70); + background-color: var(--sidebar-highlighted-background-color); } \ No newline at end of file diff --git a/components/Me/AddFriend.vue b/components/Me/AddFriend.vue new file mode 100644 index 0000000..49aa112 --- /dev/null +++ b/components/Me/AddFriend.vue @@ -0,0 +1,63 @@ + + + + \ No newline at end of file diff --git a/components/Me/DirectMessagesSidebar.vue b/components/Me/DirectMessagesSidebar.vue new file mode 100644 index 0000000..cfe07fd --- /dev/null +++ b/components/Me/DirectMessagesSidebar.vue @@ -0,0 +1,41 @@ + + + + + \ No newline at end of file diff --git a/components/Me/FriendsList.vue b/components/Me/FriendsList.vue new file mode 100644 index 0000000..44c606f --- /dev/null +++ b/components/Me/FriendsList.vue @@ -0,0 +1,58 @@ + + + + + \ No newline at end of file diff --git a/components/Member/MemberEntry.vue b/components/Member/MemberEntry.vue new file mode 100644 index 0000000..1ea4170 --- /dev/null +++ b/components/Member/MemberEntry.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/components/Message.vue b/components/Message.vue index 5cd7773..15d656f 100644 --- a/components/Message.vue +++ b/components/Message.vue @@ -1,22 +1,61 @@ + + \ No newline at end of file diff --git a/components/Popups/CropPopup.vue b/components/Popups/CropPopup.vue new file mode 100644 index 0000000..12c3a0b --- /dev/null +++ b/components/Popups/CropPopup.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/components/Loading.vue b/components/Popups/Loading.vue similarity index 100% rename from components/Loading.vue rename to components/Popups/Loading.vue diff --git a/components/Settings/AppSettings/Appearance.vue b/components/Settings/AppSettings/Appearance.vue new file mode 100644 index 0000000..b81929f --- /dev/null +++ b/components/Settings/AppSettings/Appearance.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/components/Settings/AppSettings/Keybinds.vue b/components/Settings/AppSettings/Keybinds.vue new file mode 100644 index 0000000..ea54137 --- /dev/null +++ b/components/Settings/AppSettings/Keybinds.vue @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/components/Settings/AppSettings/Language.vue b/components/Settings/AppSettings/Language.vue new file mode 100644 index 0000000..b1c3a8a --- /dev/null +++ b/components/Settings/AppSettings/Language.vue @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/components/Settings/AppSettings/Notifications.vue b/components/Settings/AppSettings/Notifications.vue new file mode 100644 index 0000000..2e6de9c --- /dev/null +++ b/components/Settings/AppSettings/Notifications.vue @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/components/Settings/UserSettings/Account.vue b/components/Settings/UserSettings/Account.vue new file mode 100644 index 0000000..f096f74 --- /dev/null +++ b/components/Settings/UserSettings/Account.vue @@ -0,0 +1,97 @@ + + + + + \ No newline at end of file diff --git a/components/Settings/UserSettings/Connections.vue b/components/Settings/UserSettings/Connections.vue new file mode 100644 index 0000000..97190ec --- /dev/null +++ b/components/Settings/UserSettings/Connections.vue @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/components/Settings/UserSettings/Devices.vue b/components/Settings/UserSettings/Devices.vue new file mode 100644 index 0000000..7006a12 --- /dev/null +++ b/components/Settings/UserSettings/Devices.vue @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/components/Settings/UserSettings/Privacy.vue b/components/Settings/UserSettings/Privacy.vue new file mode 100644 index 0000000..854ac43 --- /dev/null +++ b/components/Settings/UserSettings/Privacy.vue @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/components/Settings/UserSettings/Profile.vue b/components/Settings/UserSettings/Profile.vue new file mode 100644 index 0000000..99880fa --- /dev/null +++ b/components/Settings/UserSettings/Profile.vue @@ -0,0 +1,154 @@ + + + + + \ No newline at end of file diff --git a/components/User/UserEntry.vue b/components/User/UserEntry.vue new file mode 100644 index 0000000..b463759 --- /dev/null +++ b/components/User/UserEntry.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/components/User/UserPopup.vue b/components/User/UserPopup.vue new file mode 100644 index 0000000..a3a15cb --- /dev/null +++ b/components/User/UserPopup.vue @@ -0,0 +1,89 @@ + + + + + \ No newline at end of file diff --git a/components/UserInterface/Button.vue b/components/UserInterface/Button.vue new file mode 100644 index 0000000..66706de --- /dev/null +++ b/components/UserInterface/Button.vue @@ -0,0 +1,51 @@ + + + + + \ No newline at end of file diff --git a/components/UserInterface/RadioButtons.vue b/components/UserInterface/RadioButtons.vue new file mode 100644 index 0000000..c36b5d0 --- /dev/null +++ b/components/UserInterface/RadioButtons.vue @@ -0,0 +1,111 @@ + + + + + \ No newline at end of file diff --git a/components/UserInterface/VerticalSpacer.vue b/components/UserInterface/VerticalSpacer.vue new file mode 100644 index 0000000..8ac1bd6 --- /dev/null +++ b/components/UserInterface/VerticalSpacer.vue @@ -0,0 +1,12 @@ + + + diff --git a/composables/api.ts b/composables/api.ts index 0bdea4e..dc2fe4f 100644 --- a/composables/api.ts +++ b/composables/api.ts @@ -1,4 +1,4 @@ -import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse } from "~/types/interfaces"; +import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse, StatsResponse, UserResponse } from "~/types/interfaces"; export const useApi = () => { async function fetchGuilds(): Promise { @@ -24,14 +24,26 @@ export const useApi = () => { async function fetchMember(guildId: string, memberId: string): Promise { return await fetchWithApi(`/guilds/${guildId}/members/${memberId}`); } - + async function fetchUsers() { return await fetchWithApi(`/users`); } - + async function fetchUser(userId: string) { return await fetchWithApi(`/users/${userId}`); } + + async function fetchFriends(): Promise { + return await fetchWithApi('/me/friends') + } + + async function addFriend(username: string): Promise { + return await fetchWithApi('/me/friends', { method: "POST", body: { username } }); + } + + async function removeFriend(userId: string): Promise { + return await fetchWithApi(`/me/friends/${userId}`, { method: "DELETE" }); + } async function fetchMessages(channelId: string, options?: { amount?: number, offset?: number }): Promise { return await fetchWithApi(`/channels/${channelId}/messages`, { query: { amount: options?.amount ?? 100, offset: options?.offset ?? 0 } }); @@ -52,6 +64,15 @@ export const useApi = () => { async function createChannel(guildId: string, name: string, description?: string): Promise { return await fetchWithApi(`/guilds/${guildId}/channels`, { method: "POST", body: { name, description } }); } + + async function fetchInstanceStats(apiBase: string): Promise { + return await $fetch(`${apiBase}/stats`, { method: "GET" }); + } + + async function sendVerificationEmail(): Promise { + const email = useAuth().user.value?.email; + await fetchWithApi("/auth/verify-email", { method: "POST", body: { email } }); + } return { fetchGuilds, @@ -62,10 +83,15 @@ export const useApi = () => { fetchMember, fetchUsers, fetchUser, + fetchFriends, + addFriend, + removeFriend, fetchMessages, fetchMessage, createGuild, joinGuild, - createChannel + createChannel, + fetchInstanceStats, + sendVerificationEmail } } diff --git a/composables/auth.ts b/composables/auth.ts index 19ac694..bceecb3 100644 --- a/composables/auth.ts +++ b/composables/auth.ts @@ -7,6 +7,7 @@ export const useAuth = () => { async function clearAuth() { accessToken.value = null; user.value = null; + await navigateTo("/login"); } async function register(username: string, email: string, password: string) { @@ -37,13 +38,19 @@ export const useAuth = () => { //await fetchUser(); } - async function logout(password: string) { - console.log("password:", password); + async function logout() { console.log("access:", accessToken.value); - const hashedPass = await hashPassword(password); - console.log("hashed"); - const res = await fetchWithApi("/auth/revoke", { + await fetchWithApi("/auth/logout", { method: "GET", credentials: "include" }); + clearAuth(); + + return await navigateTo("/login"); + } + + async function revoke(password: string) { + const hashedPass = await hashPassword(password); + + await fetchWithApi("/auth/revoke", { method: "POST", body: { @@ -54,10 +61,6 @@ export const useAuth = () => { clearAuth(); } - async function revoke() { - clearAuth(); - } - async function refresh() { console.log("refreshing"); const res = await fetchWithApi("/auth/refresh", { @@ -75,7 +78,7 @@ export const useAuth = () => { async function fetchUser() { if (!accessToken.value) return; console.log("fetchuser access token:", accessToken.value); - const res = await fetchWithApi("/users/me") as UserResponse; + const res = await fetchWithApi("/me") as UserResponse; user.value = res; return user.value; } @@ -88,8 +91,22 @@ export const useAuth = () => { return user.value; } + + // as in email the password link + async function resetPassword() { + // ... + } + + async function disableAccount() { + // ... + } + + async function deleteAccount() { + // ... + } + return { - accessToken, + clearAuth, register, login, logout, diff --git a/layouts/auth.vue b/layouts/auth.vue index b7d5c5e..95eca49 100644 --- a/layouts/auth.vue +++ b/layouts/auth.vue @@ -20,30 +20,7 @@
- Instance URL is set to {{ instanceUrl }} -
-
- You're logged in! -
-
- -
- -
-
- -
-
-
- -
-
- -
-
- -
+ Instance URL is set to {{ instanceUrl }}
@@ -51,7 +28,6 @@ \ No newline at end of file diff --git a/middleware/auth.global.ts b/middleware/auth.global.ts index 2a9b752..c0da664 100644 --- a/middleware/auth.global.ts +++ b/middleware/auth.global.ts @@ -2,7 +2,21 @@ export default defineNuxtRouteMiddleware(async (to, from) => { console.log("to.fullPath:", to.fullPath); const loading = useState("loading"); const accessToken = useCookie("access_token").value; - if (["/login", "/register"].includes(to.path)) { + const apiBase = useCookie("api_base").value; + const { fetchInstanceStats } = useApi(); + + console.log("[AUTH] instance url:", apiBase); + if (apiBase && !Object.keys(to.query).includes("special") && to.path != "/verify-email") { + const user = await useAuth().getUser(); + const stats = await fetchInstanceStats(apiBase); + console.log("[AUTH] stats:", stats); + console.log("[AUTH] email verification check:", user?.email && !user.email_verified && stats.email_verification_required); + if (user?.email && !user.email_verified && stats.email_verification_required) { + return await navigateTo("/register?special=verify_email"); + } + } + + if (["/login", "/register"].includes(to.path) && !Object.keys(to.query).includes("special")) { console.log("path is login or register"); const apiBase = useCookie("api_base"); console.log("apiBase gotten:", apiBase.value); @@ -19,6 +33,14 @@ export default defineNuxtRouteMiddleware(async (to, from) => { if (parsed.ApiBaseUrl) { apiBase.value = `${parsed.ApiBaseUrl}/v${apiVersion}`; console.log("set apiBase to:", parsed.ApiBaseUrl); + console.log("hHEYOO"); + const instanceUrl = useCookie("instance_url"); + console.log("hHEYOO 2"); + console.log("instance url:", instanceUrl.value); + if (!instanceUrl.value) { + instanceUrl.value = `${requestUrl.protocol}//${requestUrl.host}`; + console.log("set instance url to:", instanceUrl.value); + } } } } diff --git a/nuxt.config.ts b/nuxt.config.ts index 63d9372..46890d1 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -27,7 +27,12 @@ export default defineNuxtConfig({ runtimeConfig: { public: { apiVersion: 1, - messageGroupingMaxDifference: 300000 + messageGroupingMaxDifference: 300000, + buildTimeString: new Date().toISOString(), + gitHash: process.env.GIT_SHORT_REV || "N/A", + defaultThemes: [ + "light", "ash", "dark", "rainbow-capitalism" + ] } }, /* nitro: { diff --git a/package.json b/package.json index b67a598..5d7d19e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@nuxt/icon": "1.13.0", "@nuxt/image": "1.10.0", "@pinia/nuxt": "0.11.0", + "cropperjs": "^2.0.0", "dompurify": "^3.2.6", "marked": "^15.0.12", "nuxt": "^3.17.0", diff --git a/pages/index.vue b/pages/index.vue index f970926..cb5f57e 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -5,6 +5,7 @@ + + \ No newline at end of file diff --git a/pages/me/friends.vue b/pages/me/friends.vue new file mode 100644 index 0000000..a0d0384 --- /dev/null +++ b/pages/me/friends.vue @@ -0,0 +1,56 @@ + + + + + \ No newline at end of file diff --git a/pages/me/index.vue b/pages/me/index.vue new file mode 100644 index 0000000..e875c56 --- /dev/null +++ b/pages/me/index.vue @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/pages/register.vue b/pages/register.vue index 3e2c4c4..708fed0 100644 --- a/pages/register.vue +++ b/pages/register.vue @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/pages/servers/[serverId]/channels/[channelId].vue b/pages/servers/[serverId]/channels/[channelId].vue index dae60bc..4393a5d 100644 --- a/pages/servers/[serverId]/channels/[channelId].vue +++ b/pages/servers/[serverId]/channels/[channelId].vue @@ -9,7 +9,7 @@
-
@@ -17,17 +17,14 @@
-
- - - {{ member.user.display_name ?? member.user.username }} -
+
\ No newline at end of file diff --git a/pages/verify-email.vue b/pages/verify-email.vue index cae02ea..f160a1d 100644 --- a/pages/verify-email.vue +++ b/pages/verify-email.vue @@ -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; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07816d4..6a8cf2c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@pinia/nuxt': specifier: 0.11.0 version: 0.11.0(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))) + cropperjs: + specifier: ^2.0.0 + version: 2.0.0 dompurify: specifier: ^3.2.6 version: 3.2.6 @@ -205,6 +208,39 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} + '@cropper/element-canvas@2.0.0': + resolution: {integrity: sha512-GPtGJgSm92crJhhhwUsaMw3rz2KfJWWSz7kRAlufFEV/EHTP5+6r6/Z1BCGRna830i+Avqbm435XLOtA7PVJwA==} + + '@cropper/element-crosshair@2.0.0': + resolution: {integrity: sha512-KfPfyrdeFvUC31Ws7ATtcalWWSaMtrC6bMoCipZhqbUOE7wZoL4ecDSL6BUOZxPa74awZUqfzirCDjHvheBfyw==} + + '@cropper/element-grid@2.0.0': + resolution: {integrity: sha512-i78SQ0IJTLFveKX6P7svkfMYVdgHrQ8ZmmEw8keFy9n1ZVbK+SK0UHK5FNMRNI/gtVhKJOGEnK/zeyjUdj4Iyw==} + + '@cropper/element-handle@2.0.0': + resolution: {integrity: sha512-ZJvW+0MkK9E8xYymGdoruaQn2kwjSHFpNSWinjyq6csuVQiCPxlX5ovAEDldmZ9MWePPtWEi3vLKQOo2Yb0T8g==} + + '@cropper/element-image@2.0.0': + resolution: {integrity: sha512-9BxiTS/aHRmrjopaFQb9mQQXmx4ruhYHGkDZMVz24AXpMFjUY6OpqrWse/WjzD9tfhMFvEdu17b3VAekcAgpeg==} + + '@cropper/element-selection@2.0.0': + resolution: {integrity: sha512-ensNnbIfJsJ8bhbJTH/RXtk2URFvTOO4TvfRk461n2FPEC588D7rwBmUJxQg74IiTi4y1JbCI+6j+4LyzYBLCQ==} + + '@cropper/element-shade@2.0.0': + resolution: {integrity: sha512-jv/2bbNZnhU4W+T4G0c8ADocLIZvQFTXgCf2RFDNhI5UVxurzWBnDdb8Mx8LnVplnkTqO+xUmHZYve0CwgWo+Q==} + + '@cropper/element-viewer@2.0.0': + resolution: {integrity: sha512-zY+3VRN5TvpM8twlphYtXw0tzJL2VgzeK7ufhL1BixVqOdRxwP13TprYIhqwGt9EW/SyJZUiaIu396T89kRX8A==} + + '@cropper/element@2.0.0': + resolution: {integrity: sha512-lsthn0nQq73GExUE7Mg/ss6Q3RXADGDv055hxoLFwvl/wGHgy6ZkYlfLZ/VmgBHC6jDK5IgPBFnqrPqlXWSGBA==} + + '@cropper/elements@2.0.0': + resolution: {integrity: sha512-PQkPo1nUjxLFUQuHYu+6atfHxpX9B41Xribao6wpvmvmNIFML6LQdNqqWYb6LyM7ujsu71CZdBiMT5oetjJVoQ==} + + '@cropper/utils@2.0.0': + resolution: {integrity: sha512-cprLYr+7kK3faGgoOsTW9gIn5sefDr2KwOmgyjzIXk+8PLpW8FgFKEg5FoWfRD5zMAmkCBuX6rGKDK3VdUEGrg==} + '@dabh/diagnostics@2.0.3': resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} @@ -1900,6 +1936,9 @@ packages: resolution: {integrity: sha512-onMB0OkDjkXunhdW9htFjEhqrD54+M94i6ackoUkjHKbRnXdyEyKRelp4nJ1kAz32+s27jP1FsebpJCVl0BsvA==} engines: {node: '>=18.0'} + cropperjs@2.0.0: + resolution: {integrity: sha512-TO2j0Qre01kPHbow4FuTrbdEB4jTmGRySxW49jyEIqlJZuEBfrvCTT0vC3eRB2WBXudDfKi1Onako6DKWKxeAQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -4965,6 +5004,72 @@ snapshots: '@colors/colors@1.6.0': {} + '@cropper/element-canvas@2.0.0': + dependencies: + '@cropper/element': 2.0.0 + '@cropper/utils': 2.0.0 + + '@cropper/element-crosshair@2.0.0': + dependencies: + '@cropper/element': 2.0.0 + '@cropper/utils': 2.0.0 + + '@cropper/element-grid@2.0.0': + dependencies: + '@cropper/element': 2.0.0 + '@cropper/utils': 2.0.0 + + '@cropper/element-handle@2.0.0': + dependencies: + '@cropper/element': 2.0.0 + '@cropper/utils': 2.0.0 + + '@cropper/element-image@2.0.0': + dependencies: + '@cropper/element': 2.0.0 + '@cropper/element-canvas': 2.0.0 + '@cropper/utils': 2.0.0 + + '@cropper/element-selection@2.0.0': + dependencies: + '@cropper/element': 2.0.0 + '@cropper/element-canvas': 2.0.0 + '@cropper/element-image': 2.0.0 + '@cropper/utils': 2.0.0 + + '@cropper/element-shade@2.0.0': + dependencies: + '@cropper/element': 2.0.0 + '@cropper/element-canvas': 2.0.0 + '@cropper/element-selection': 2.0.0 + '@cropper/utils': 2.0.0 + + '@cropper/element-viewer@2.0.0': + dependencies: + '@cropper/element': 2.0.0 + '@cropper/element-canvas': 2.0.0 + '@cropper/element-image': 2.0.0 + '@cropper/element-selection': 2.0.0 + '@cropper/utils': 2.0.0 + + '@cropper/element@2.0.0': + dependencies: + '@cropper/utils': 2.0.0 + + '@cropper/elements@2.0.0': + dependencies: + '@cropper/element': 2.0.0 + '@cropper/element-canvas': 2.0.0 + '@cropper/element-crosshair': 2.0.0 + '@cropper/element-grid': 2.0.0 + '@cropper/element-handle': 2.0.0 + '@cropper/element-image': 2.0.0 + '@cropper/element-selection': 2.0.0 + '@cropper/element-shade': 2.0.0 + '@cropper/element-viewer': 2.0.0 + + '@cropper/utils@2.0.0': {} + '@dabh/diagnostics@2.0.3': dependencies: colorspace: 1.1.4 @@ -6895,6 +7000,11 @@ snapshots: croner@9.0.0: {} + cropperjs@2.0.0: + dependencies: + '@cropper/elements': 2.0.0 + '@cropper/utils': 2.0.0 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 diff --git a/public/favicon.ico b/public/favicon.ico index 18993ad..b3d28fa 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000..2b2d33f --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/themes/ash.css b/public/themes/ash.css new file mode 100644 index 0000000..63aaa53 --- /dev/null +++ b/public/themes/ash.css @@ -0,0 +1,21 @@ +:root { + --text-color: #f0e5e0; + --secondary-text-color: #e8e0db; + --reply-text-color: #969696; + + --chat-background-color: #2f2e2d; + --chat-highlighted-background-color: #3f3b38; + --sidebar-background-color: #3e3a37; + --sidebar-highlighted-background-color: #46423b; + --topbar-background-color: #3a3733; + --chatbox-background-color: #3a3733; + + --padding-color: #e0e0e0; + + --primary-color: #f07028; + --primary-highlighted-color: #f28f4b; + --secondary-color: #683820; + --secondary-highlighted-color: #885830; + --accent-color: #a04b24; + --accent-highlighted-color: #b86038; +} \ No newline at end of file diff --git a/public/themes/ash.json b/public/themes/ash.json new file mode 100644 index 0000000..d5d2a59 --- /dev/null +++ b/public/themes/ash.json @@ -0,0 +1,6 @@ +{ + "displayName": "Ash", + "previewGradient": "45deg, #2f2e2d, #46423b", + "complementaryColor": "white", + "themeUrl": "ash.css" +} \ No newline at end of file diff --git a/public/themes/dark.css b/public/themes/dark.css new file mode 100644 index 0000000..3673d4c --- /dev/null +++ b/public/themes/dark.css @@ -0,0 +1,21 @@ +:root { + --text-color: #f7eee8; + --secondary-text-color: #f0e8e4; + --reply-text-color: #969696; + + --chat-background-color: #1f1e1d; + --chat-highlighted-background-color: #2f2b28; + --sidebar-background-color: #2e2a27; + --sidebar-highlighted-background-color: #36322b; + --topbar-background-color: #2a2723; + --chatbox-background-color: #1a1713; + + --padding-color: #484848; + + --primary-color: #f4741f; + --primary-highlighted-color: #f68a3f; + --secondary-color: #7c4018; + --secondary-highlighted-color: #8f5b2c; + --accent-color: #b35719; + --accent-highlighted-color: #c76a2e; +} \ No newline at end of file diff --git a/public/themes/dark.json b/public/themes/dark.json new file mode 100644 index 0000000..4731d43 --- /dev/null +++ b/public/themes/dark.json @@ -0,0 +1,6 @@ +{ + "displayName": "Dark", + "previewGradient": "45deg, #1f1e1d, #36322b", + "complementaryColor": "white", + "themeUrl": "dark.css" +} \ No newline at end of file diff --git a/public/themes/description.css b/public/themes/description.css new file mode 100644 index 0000000..209fb36 --- /dev/null +++ b/public/themes/description.css @@ -0,0 +1,30 @@ +/* this is not a real theme, but rather a template for themes */ +:root { + --text-color: #161518; + --secondary-text-color: #2b2930; + --reply-text-color: #969696; + + --chat-background-color: #80808000; + --chat-highlighted-background-color: #ffffff20; + --sidebar-background-color: #80808000; + --sidebar-highlighted-background-color: #ffffff20; + --topbar-background-color: #80808000; + --chatbox-background-color: #80808000; + + --padding-color: #80808000; + + --primary-color: #21b1ff80; + --primary-highlighted-color: #18a0df80; + --secondary-color: #ffd80080; + --secondary-highlighted-color: #dfb80080; + --accent-color: #ff218c80; + --accent-highlighted-color: #df1b6f80; + + --optional-body-background: ; /* background element for the body */ + --optional-chat-background: ; /* background element for the chat box */ + --optional-topbar-background: ; /* background element for the topbar */ + --optional-sidebar-background: ; /* background element for left server sidebar */ + --optional-channel-list-background: ; /* background element for channel list and settings list */ + --optional-member-list-background: ; /* background element for member list */ + --optional-message-box-background: ; /* background element for message box */ +} \ No newline at end of file diff --git a/public/themes/light.css b/public/themes/light.css new file mode 100644 index 0000000..fdd4756 --- /dev/null +++ b/public/themes/light.css @@ -0,0 +1,21 @@ +:root { + --text-color: #170f08; + --secondary-text-color: #2f2b28; + --reply-text-color: #969696; + + --chat-background-color: #f0ebe8; + --chat-highlighted-background-color: #e8e4e0; + --sidebar-background-color: #dbd8d4; + --sidebar-highlighted-background-color: #d4d0ca; + --topbar-background-color: #dfdbd6; + --chatbox-background-color: #dfdbd6; + + --padding-color: #484848; + + --primary-color: #df5f0b; + --primary-highlighted-color: #ef6812; + --secondary-color: #e8ac84; + --secondary-highlighted-color: #f8b68a; + --accent-color: #e68b4e; + --accent-highlighted-color: #f69254; +} \ No newline at end of file diff --git a/public/themes/light.json b/public/themes/light.json new file mode 100644 index 0000000..b95c78b --- /dev/null +++ b/public/themes/light.json @@ -0,0 +1,6 @@ +{ + "displayName": "Light", + "previewGradient": "45deg, #f0ebe8, #d4d0ca", + "complementaryColor": "black", + "themeUrl": "light.css" +} \ No newline at end of file diff --git a/public/themes/rainbow-capitalism.css b/public/themes/rainbow-capitalism.css new file mode 100644 index 0000000..0e146d9 --- /dev/null +++ b/public/themes/rainbow-capitalism.css @@ -0,0 +1,29 @@ +:root { + --text-color: #161518; + --secondary-text-color: #2b2930; + --reply-text-color: #969696; + + --chat-background-color: #80808000; + --chat-highlighted-background-color: #ffffff20; + --sidebar-background-color: #80808000; + --sidebar-highlighted-background-color: #ffffff20; + --topbar-background-color: #80808000; + --chatbox-background-color: #80808040; + + --padding-color: #80808000; + + --primary-color: #21b1ff80; + --primary-highlighted-color: #18a0df80; + --secondary-color: #ffd80080; + --secondary-highlighted-color: #dfb80080; + --accent-color: #ff218c80; + --accent-highlighted-color: #df1b6f80; + + /* --optional-body-background: background */ + --optional-body-background: linear-gradient(45deg, #ed222480, #ed222480, #ed222480, #ed222480, #ed222480, #ed222480, #f35b2280, #f9962180, #f5c11e80, #f1eb1b80, #f1eb1b80, #f1eb1b80, #63c72080, #0c9b4980, #21878d80, #3954a580, #61379b80, #93288e80); + --optional-topbar-background: linear-gradient(-12.5deg, cyan, pink, white, pink, cyan); + --optional-sidebar-background: linear-gradient(90deg, #55cdfcd0, #f7a8b8d0, #ffffffd0, #f7a8b8d0, #55cdfcd0); + --optional-channel-list-background: linear-gradient(82deg, #d52c00b0, #e29688b0, #ffffffb0, #d27fa4b0, #a20062b0); + --optional-member-list-background: linear-gradient(3deg, #ff0080, #c8259d, #8c4799, #442e9f, #0032a0); + --optional-message-box-background: linear-gradient(3deg, #ff0080, #c8259d, #8c4799, #442e9f, #0032a0); +} \ No newline at end of file diff --git a/public/themes/rainbow-capitalism.json b/public/themes/rainbow-capitalism.json new file mode 100644 index 0000000..e110ea2 --- /dev/null +++ b/public/themes/rainbow-capitalism.json @@ -0,0 +1,6 @@ +{ + "displayName": "Woke", + "previewGradient": "45deg, #ed2224, #ed2224, #f35b22, #f99621, #f5c11e, #f1eb1b 27%, #f1eb1b, #f1eb1b 33%, #63c720, #0c9b49, #21878d, #3954a5, #61379b, #93288e, #93288e", + "complementaryColor": "white", + "themeUrl": "rainbow-capitalism.css" +} \ No newline at end of file diff --git a/types/hooks.ts b/types/hooks.ts new file mode 100644 index 0000000..73a7894 --- /dev/null +++ b/types/hooks.ts @@ -0,0 +1,7 @@ +import type { RuntimeNuxtHooks } from 'nuxt/schema'; + +declare module "nuxt/schema" { + interface RuntimeNuxtHooks { + "app:message:right-clicked": (payload: { messageId: string }) => void + } +} diff --git a/types/interfaces.ts b/types/interfaces.ts index 82a538b..e26648a 100644 --- a/types/interfaces.ts +++ b/types/interfaces.ts @@ -44,7 +44,8 @@ export interface MessageResponse { channel_uuid: string, user_uuid: string, message: string, - user: UserResponse + reply_to: string | null, + user: UserResponse, } export interface InviteResponse { @@ -58,9 +59,12 @@ export interface UserResponse { username: string, display_name: string | null, avatar: string | null, + pronouns: string | null, + about: string | null, email?: string, - email_verified?: boolean - } + email_verified?: boolean, + friends_since: string | null, +} export interface StatsResponse { accounts: number, @@ -94,3 +98,8 @@ export interface ModalProps { onClose?: () => void, onCancel?: () => void } + +export interface ContextMenuItem { + name: string, + callback: (...args: any[]) => any; +} diff --git a/types/props.ts b/types/props.ts new file mode 100644 index 0000000..aa6ff0c --- /dev/null +++ b/types/props.ts @@ -0,0 +1,20 @@ +import type { MessageResponse, UserResponse } from "./interfaces"; + +export interface MessageProps { + class?: string, + img?: string | null, + author?: UserResponse + text: string, + timestamp: number, + format: "12" | "24", + type: "normal" | "grouped", + marginBottom: boolean, + last: boolean, + messageId: string, + replyingTo?: boolean, + editing?: boolean, + me: UserResponse + message: MessageResponse, + replyMessage?: MessageResponse + isMentioned?: boolean, +} \ No newline at end of file diff --git a/types/settings.ts b/types/settings.ts new file mode 100644 index 0000000..28e3bfc --- /dev/null +++ b/types/settings.ts @@ -0,0 +1,9 @@ +export interface ClientSettings { + selectedThemeId?: string, // the ID of the theme, not the URL, for example "dark" + timeFormat?: TimeFormat +} + +export interface TimeFormat { + index: number, + format: "auto" | "12" | "24" +} \ No newline at end of file diff --git a/utils/createContextMenu.ts b/utils/createContextMenu.ts new file mode 100644 index 0000000..f3c4aab --- /dev/null +++ b/utils/createContextMenu.ts @@ -0,0 +1,17 @@ +import { render } from "vue"; +import ContextMenu from "~/components/ContextMenu.vue"; +import type { ContextMenuItem } from "~/types/interfaces"; + +export default (e: MouseEvent, menuItems: ContextMenuItem[]) => { + console.log("Rendering new context menu"); + const menuContainer = document.createElement("div"); + menuContainer.id = "context-menu"; + document.body.appendChild(menuContainer); + const contextMenu = h(ContextMenu, { + menuItems, + cursorX: e.clientX, + cursorY: e.clientY + }); + render(contextMenu, menuContainer); + console.log("Rendered"); +} diff --git a/utils/editMessage.ts b/utils/editMessage.ts new file mode 100644 index 0000000..5cc3ce8 --- /dev/null +++ b/utils/editMessage.ts @@ -0,0 +1,24 @@ +import type { MessageProps } from "~/types/props"; + +export default async (element: HTMLDivElement, props: MessageProps) => { + console.log("message:", element); + const me = await fetchWithApi("/me") as any; + if (props.author?.uuid == me.uuid) { + const text = element.getElementsByClassName("message-text")[0] as HTMLDivElement; + text.contentEditable = "true"; + text.focus(); + const range = document.createRange(); + range.selectNodeContents(text); + range.collapse(false); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); + element.addEventListener("keyup", (e) => { + console.log("key released:", e.key); + if (e.key == "Escape") { + text.contentEditable = "false"; + } + text.blur(); + }, { once: true }); + } +} diff --git a/utils/fetchWithApi.ts b/utils/fetchWithApi.ts index 4089db8..664412c 100644 --- a/utils/fetchWithApi.ts +++ b/utils/fetchWithApi.ts @@ -18,7 +18,7 @@ export default async (path: string, options: NitroFetchOptions = {}) return; } console.log("path:", path) - const { revoke, refresh } = useAuth(); + const { clearAuth, refresh } = useAuth(); let headers: HeadersInit = {}; @@ -61,8 +61,7 @@ export default async (path: string, options: NitroFetchOptions = {}) if (error?.response?.status === 401) { console.log("Refresh returned 401"); reauthFailed = true; - console.log("Revoking"); - await revoke(); + await clearAuth() console.log("Redirecting to login"); await navigateTo("/login"); console.log("redirected"); diff --git a/utils/getPreferredTimeFormat.ts b/utils/getPreferredTimeFormat.ts new file mode 100644 index 0000000..86b9635 --- /dev/null +++ b/utils/getPreferredTimeFormat.ts @@ -0,0 +1,11 @@ +export default (): "12" | "24" => { + const format = settingsLoad().timeFormat?.format ?? "auto" + + if (format == "12") { + return "12" + } else if (format == "24") { + return "24" + } + + return "24" +} \ No newline at end of file diff --git a/utils/removeContextMenu.ts b/utils/removeContextMenu.ts new file mode 100644 index 0000000..3b42c8b --- /dev/null +++ b/utils/removeContextMenu.ts @@ -0,0 +1,6 @@ +export default () => { + const contextMenu = document.getElementById("context-menu"); + if (contextMenu) { + contextMenu.remove(); + } +} diff --git a/utils/replyToMessage.ts b/utils/replyToMessage.ts new file mode 100644 index 0000000..bbb453b --- /dev/null +++ b/utils/replyToMessage.ts @@ -0,0 +1,14 @@ +import { render } from "vue"; +import MessageReply from "~/components/MessageReply.vue"; +import type { MessageProps } from "~/types/props"; + +export default (element: HTMLDivElement, props: MessageProps) => { + console.log("element:", element); + const messageBox = document.getElementById("message-box") as HTMLDivElement; + if (messageBox) { + const div = document.createElement("div"); + const messageReply = h(MessageReply, { author: props.author?.display_name || props.author!.username, text: props.text || "", id: props.message.uuid, replyId: props.replyMessage?.uuid || element.dataset.messageId!, maxWidth: "full" }); + messageBox.prepend(div); + render(messageReply, div); + } +} diff --git a/utils/settingSave.ts b/utils/settingSave.ts new file mode 100644 index 0000000..fbdefd7 --- /dev/null +++ b/utils/settingSave.ts @@ -0,0 +1,21 @@ +export default (key: string, value: any): void => { + let clientSettingsItem: string | null = localStorage.getItem("clientSettings") + if (typeof clientSettingsItem != "string") { + clientSettingsItem = "{}" + } + + let clientSettings: { [key: string]: any } = {} + try { + clientSettings = JSON.parse(clientSettingsItem) + } catch { + clientSettings = {} + } + + if (typeof clientSettings !== "object") { + clientSettings = {} + } + + clientSettings[key] = value + + localStorage.setItem("clientSettings", JSON.stringify(clientSettings)) +} \ No newline at end of file diff --git a/utils/settingsLoad.ts b/utils/settingsLoad.ts new file mode 100644 index 0000000..e7fdfaf --- /dev/null +++ b/utils/settingsLoad.ts @@ -0,0 +1,21 @@ +import type { ClientSettings } from "~/types/settings" + +export default (): ClientSettings => { + let clientSettingsItem: string | null = localStorage.getItem("clientSettings") + if (typeof clientSettingsItem != "string") { + clientSettingsItem = "{}" + } + + let clientSettings: ClientSettings = {} + try { + clientSettings = JSON.parse(clientSettingsItem) + } catch { + clientSettings = {} + } + + if (typeof clientSettings !== "object") { + clientSettings = {} + } + + return clientSettings +} \ No newline at end of file