Compare commits

..

No commits in common. "a38589615b7fe13efd36ca302a1410c05bc32718" and "a8e8c6b2ef1aecc1c563ed7ec465b6c4d6a0a889" have entirely different histories.

11 changed files with 126 additions and 147 deletions

View file

@ -1,11 +1,11 @@
<template> <template>
<div v-if="isCurrentChannel" class="channel-list-link-container rounded-corners current-channel" tabindex="0"> <div v-if="isCurrentChannel" class="channel-list-link-container rounded-corners current-channel">
<NuxtLink class="channel-list-link" :href="props.href" tabindex="-1"> <NuxtLink class="channel-list-link" :href="props.href">
# {{ props.name }} # {{ props.name }}
</NuxtLink> </NuxtLink>
</div> </div>
<div v-else class="channel-list-link-container rounded-corners" tabindex="0"> <div v-else class="channel-list-link-container rounded-corners">
<NuxtLink class="channel-list-link" :href="props.href" tabindex="-1"> <NuxtLink class="channel-list-link" :href="props.href">
# {{ props.name }} # {{ props.name }}
</NuxtLink> </NuxtLink>
</div> </div>

View file

@ -1,5 +1,5 @@
<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" :class="{ 'message-margin-bottom': props.marginBottom }">
<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" />
@ -13,10 +13,12 @@
{{ messageDate }} {{ messageDate }}
</span> </span>
</div> </div>
<div class="message-text" v-html="sanitized"></div> <div class="message-text">
{{ text }}
</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">
<div class="left-column"> <div class="left-column">
<div> <div>
<span :class="{ 'invisible': dateHidden }" class="message-date" :title="date.toString()"> <span :class="{ 'invisible': dateHidden }" class="message-date" :title="date.toString()">
@ -25,15 +27,14 @@
</div> </div>
</div> </div>
<div class="message-data"> <div class="message-data">
<div class="message-text" v-html="sanitized"></div> <div class="message-text">
{{ text }}
</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import DOMPurify from 'dompurify';
import { parseInline } from 'marked';
const props = defineProps<{ const props = defineProps<{
class?: string, class?: string,
img?: string | null, img?: string | null,
@ -73,11 +74,7 @@ if (props.format == "12") {
console.log("message:", props.text); console.log("message:", props.text);
console.log("author:", props.username); console.log("author:", props.username);
const sanitized = ref<string>();
onMounted(async () => { onMounted(async () => {
const parsed = await parseInline(props.text, {gfm: true });
sanitized.value = DOMPurify.sanitize(parsed, { ALLOWED_TAGS: ["strong", "em", "br", "blockquote", "code", "ul", "ol", "li", "a"] });
console.log("adding listeners") console.log("adding listeners")
await nextTick(); await nextTick();
messageElement.value?.addEventListener("mouseenter", (e: Event) => { messageElement.value?.addEventListener("mouseenter", (e: Event) => {
@ -105,8 +102,8 @@ onMounted(async () => {
align-items: center; align-items: center;
} }
.normal-message { .message-margin-bottom {
margin-top: 1dvh; margin-bottom: 1dvh;
} }
#last-message { #last-message {

View file

@ -185,7 +185,6 @@ onMounted(async () => {
olderMessages.reverse(); olderMessages.reverse();
console.log("older messages:", olderMessages); console.log("older messages:", olderMessages);
if (olderMessages.length == 0) return; if (olderMessages.length == 0) return;
olderMessages.reverse();
for (const [i, oldMessage] of olderMessages.entries()) { for (const [i, oldMessage] of olderMessages.entries()) {
console.log("old message:", oldMessage); console.log("old message:", oldMessage);
messages.value.unshift(oldMessage); messages.value.unshift(oldMessage);

View file

@ -10,7 +10,7 @@
{{ props.user.username || "username" }} - {{ props.user.pronouns || "un/defined" }} {{ props.user.username || "username" }} - {{ props.user.pronouns || "un/defined" }}
</p> </p>
<div id="about-me"> <div id="about-me">
{{ props.user.about || "about me" }} <span>About me</span>
</div> </div>
</div> </div>
</div> </div>
@ -74,11 +74,8 @@ const props = defineProps<{
} }
#about-me { #about-me {
background-color: #34200f; margin-top: 4px;
border-radius: 12px; padding: 4px;
margin-top: 32px;
padding: 16px;
font-size: 16px; font-size: 16px;
} }
</style> </style>

View file

@ -2,24 +2,21 @@
<div> <div>
<input id="hidden-pfp-uploader" type="file" accept="image/*" style="display: none;"> <input id="hidden-pfp-uploader" type="file" accept="image/*" style="display: none;">
<h1>My Account</h1> <h1>My Account</h1>
<div class="profile-and-user-data-fields"> <div class="profile-and-user-data-fields">
<div class="user-data-fields"> <div class="user-data-fields">
<h3 class="subtitle">AVATAR</h3> <h3 class="subtitle">AVATAR</h3>
<Button text="Change Avatar" :callback="changeAvatar"></Button> <Button text="Change Avatar" :callback="changeAvatar"></Button>
<Button text="Remove Avatar" :callback="removeAvatar" <Button text="Remove Avatar" :callback="removeAvatar" style="margin-left: 10px; background-color: grey;"></Button>
style="margin-left: 10px; background-color: grey;"></Button>
<h3 class="subtitle">DISPLAY NAME</h3> <h3 class="subtitle">DISPLAY NAME</h3>
<input class="profile-data-input" type="text" v-model="user.display_name" placeholder="Enter display name" /> <input type="text" v-model="user.display_name" placeholder="Enter display name" />
<h3 class="subtitle">USERNAME</h3> <h3 class="subtitle">USERNAME</h3>
<input class="profile-data-input" type="text" v-model="user.username" placeholder="Enter username" /> <input type="text" v-model="user.username" placeholder="Enter username" />
<h3 class="subtitle">PRONOUNS</h3> <h3 class="subtitle">PRONOUNS</h3>
<input class="profile-data-input" type="text" v-model="user.pronouns" placeholder="Enter pronouns" /> <input type="text" v-model="user.pronouns" placeholder="Enter pronouns" />
<h3 class="subtitle">ABOUT ME</h3> <h3 class="subtitle">ABOUT ME</h3>
<input class="profile-data-input" type="text" v-model="user.about" placeholder="About You" /> <p>{{ user?.about_me || "TBD" }}</p>
<br>
<br>
<Button text="Save Changes" :callback="saveChanges"></Button> <Button text="Save Changes" :callback="saveChanges"></Button>
</div> </div>
<Userpopup :user=user_me class="profile"></Userpopup> <Userpopup :user=user_me class="profile"></Userpopup>
@ -32,10 +29,10 @@
<br> <br>
<h2>Password (and eventually authenticator)</h2> <h2>Password (and eventually authenticator)</h2>
<Button text="Reset Password (tbd)" :callback=resetPassword></Button> <Button text="Reset Password (tbd)" :callback=resetPassword></Button>
<h2>Account Deletion</h2> <h2>Account Deletion</h2>
<ButtonScary text="Delete Account (tbd)" :callback=deleteAccount></ButtonScary> <ButtonScary text="Delete Account (tbd)" :callback=deleteAccount></ButtonScary>
</div> </div>
</template> </template>
@ -53,33 +50,38 @@ const user = user_me!
let new_pfp_file: any = null let new_pfp_file: any = null
const saveChanges = async () => { const saveChanges = async () => {
const formData = new FormData()
if (new_pfp_file !== null) {
formData.append("avatar", new_pfp_file)
}
const bytes = new TextEncoder().encode(JSON.stringify({
display_name: user.display_name,
username: user.username,
pronouns: user.pronouns,
about: user.about,
}));
formData.append('json', new Blob([bytes], { type: 'application/json' }));
try { try {
await fetchWithApi('/me', { const formData = new FormData()
method: 'PATCH',
const pfpInput = document.getElementById("hidden-pfp-uploader") as HTMLInputElement | null;
if (pfpInput) {
if (pfpInput.files?.length && pfpInput.files.length > 0) {
console.log(pfpInput.files[0])
formData.append("avatar", pfpInput.files[0])
}
}
const bytes = new TextEncoder().encode(JSON.stringify({
display_name: user.display_name,
username: user.username,
pronouns: user.pronouns,
}));
formData.append("json", new Blob([bytes], { type: "application/json" }));
await fetchWithApi("/me", {
method: "PATCH",
body: formData body: formData
}) })
user_reference = Object.assign({}, await fetchUser()) user_reference = Object.assign({}, user_me)
alert('success!!') alert("success!!")
} catch (error: any) { } catch (error: any) {
if (error?.response?.status !== 200) { if (error?.response?.status !== 200) {
alert(`error ${error?.response?.status} met whilst trying to update profile info`) const errorData = await error?.response?.json()
alert(`error ${error?.response?.status} met whilst trying to update profile info\n${errorData}`)
} }
} }
}; };
@ -89,28 +91,23 @@ const removeAvatar = async () => {
} }
const changeAvatar = async () => { const changeAvatar = async () => {
let input = document.createElement('input'); const pfpInput = document.getElementById("hidden-pfp-uploader") as HTMLInputElement | null;
input.type = 'file';
input.accept = 'image/*';
input.onchange = async (e) => { // upload_field.onchange = async(e) => {
if (input.files?.length && input.files.length > 0) { // console.log(upload_field.files)
const file = input.files[0]; // if (upload_field.files?.length && upload_field.files.length > 0) {
if (!file) return; // const file = upload_field.files[0];
// if (!file) return;
new_pfp_file = file // const reader = new FileReader();
// reader.onload = (e) => {
const reader = new FileReader(); // user.avatar = e?.target?.result;
reader.onload = (e) => { // };
if (e.target?.result && typeof e.target.result === 'string') { // reader.readAsDataURL(file);
user.avatar = e.target.result; // }
} // }
};
reader.readAsDataURL(file);
}
}
input.click() pfpInput?.click()
} }
const resetPassword = async () => { const resetPassword = async () => {
@ -135,8 +132,7 @@ const deleteAccount = async () => {
display: flex; display: flex;
} }
.profile-container, .profile-container, .user-data-fields {
.user-data-fields {
min-width: 350px; min-width: 350px;
} }
@ -145,16 +141,4 @@ const deleteAccount = async () => {
font-weight: 800; font-weight: 800;
margin: 12px 0; margin: 12px 0;
} }
.profile-data-input {
min-width: 300px;
margin: 2px;
padding: 2px 10px;
height: 40px;
font-size: 16px;
border-radius: 8px;
border: none;
color: white;
background-color: #54361b;
}
</style> </style>

View file

@ -123,6 +123,30 @@ const members = [
grid-row: 1; grid-row: 1;
} }
#test {
grid-column: 3;
grid-row: 1;
}
.member-item {
display: flex;
justify-content: center;
align-items: center;
}
#message-history,
#members-list {
padding-top: 3dvh;
}
#message-history {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-left: 3dvw;
padding-right: 3dvw;
}
#left-column { #left-column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -14,8 +14,6 @@
"@nuxt/icon": "1.13.0", "@nuxt/icon": "1.13.0",
"@nuxt/image": "1.10.0", "@nuxt/image": "1.10.0",
"@pinia/nuxt": "0.11.0", "@pinia/nuxt": "0.11.0",
"dompurify": "^3.2.6",
"marked": "^15.0.12",
"nuxt": "^3.17.0", "nuxt": "^3.17.0",
"pinia": "^3.0.2", "pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0", "pinia-plugin-persistedstate": "^4.2.0",

View file

@ -24,13 +24,11 @@
</div> </div>
</div> </div>
<MessageArea :channel-url="channelUrlPath" /> <MessageArea :channel-url="channelUrlPath" />
<div id="members-container"> <div id="members-list">
<div id="members-list"> <div class="member-item" v-for="member of members">
<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" />
<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" />
<Icon v-else class="member-avatar" name="lucide:user" /> <span class="member-display-name">{{ member.user.display_name ?? member.user.username }}</span>
<span class="member-display-name">{{ member.user.display_name ?? member.user.username }}</span>
</div>
</div> </div>
</div> </div>
</NuxtLayout> </NuxtLayout>
@ -81,6 +79,18 @@ function toggleInvitePopup(e: Event) {
</script> </script>
<style> <style>
.member-item {
display: flex;
justify-content: center;
align-items: center;
margin-top: .5em;
margin-bottom: .5em;
gap: .5em;
}
#members-list {
padding-top: 3dvh;
}
#middle-left-column { #middle-left-column {
padding-left: 1dvw; padding-left: 1dvw;
@ -88,29 +98,12 @@ function toggleInvitePopup(e: Event) {
border-right: 1px solid rgb(70, 70, 70); border-right: 1px solid rgb(70, 70, 70);
} }
#members-container { #members-list {
padding-top: 1dvh;
padding-left: 1dvw; padding-left: 1dvw;
padding-right: 1dvw; padding-right: 1dvw;
border-left: 1px solid rgb(70, 70, 70); border-left: 1px solid rgb(70, 70, 70);
} }
#members-list {
display: grid;
grid-template-columns: auto;
}
.member-item {
display: grid;
grid-template-columns: 2dvw auto;
margin-top: .5em;
margin-bottom: .5em;
gap: 1em;
align-items: center;
text-align: left;
}
#channels-list { #channels-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -123,9 +116,4 @@ function toggleInvitePopup(e: Event) {
border-radius: 50%; border-radius: 50%;
} }
.member-display-name {
overflow: hidden;
text-overflow: ellipsis;
}
</style> </style>

View file

@ -69,6 +69,24 @@ const settingsCategories = {
{ display_name: "Language", page_data: Language }, { display_name: "Language", page_data: Language },
] ]
}, },
app_settings2: {
display_name: "App Settings",
pages: [
{ display_name: "Appearance", page_data: Appearance },
{ display_name: "Notifications", page_data: Notifications },
{ display_name: "Keybinds", page_data: Keybinds },
{ display_name: "Language", page_data: Language },
]
},
app_settings3: {
display_name: "App Settings",
pages: [
{ display_name: "Appearance", page_data: Appearance },
{ display_name: "Notifications", page_data: Notifications },
{ display_name: "Keybinds", page_data: Keybinds },
{ display_name: "Language", page_data: Language },
]
},
}; };
const categories = Object.values(settingsCategories); const categories = Object.values(settingsCategories);

26
pnpm-lock.yaml generated
View file

@ -20,12 +20,6 @@ importers:
'@pinia/nuxt': '@pinia/nuxt':
specifier: 0.11.0 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))) version: 0.11.0(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)))
dompurify:
specifier: ^3.2.6
version: 3.2.6
marked:
specifier: ^15.0.12
version: 15.0.12
nuxt: nuxt:
specifier: ^3.17.0 specifier: ^3.17.0
version: 3.17.0(@netlify/blobs@8.2.0)(@parcel/watcher@2.5.1)(@types/node@22.15.3)(db0@0.3.2)(eslint@9.25.1(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.29.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.40.1)(terser@5.39.0)(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1) version: 3.17.0(@netlify/blobs@8.2.0)(@parcel/watcher@2.5.1)(@types/node@22.15.3)(db0@0.3.2)(eslint@9.25.1(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.29.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.40.1)(terser@5.39.0)(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1)
@ -1205,9 +1199,6 @@ packages:
'@types/triple-beam@1.3.5': '@types/triple-beam@1.3.5':
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
@ -2122,9 +2113,6 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
dompurify@3.2.6:
resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==}
domutils@3.2.2: domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@ -3104,11 +3092,6 @@ packages:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'} engines: {node: '>=8'}
marked@15.0.12:
resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==}
engines: {node: '>= 18'}
hasBin: true
math-intrinsics@1.1.0: math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -6111,9 +6094,6 @@ snapshots:
'@types/triple-beam@1.3.5': {} '@types/triple-beam@1.3.5': {}
'@types/trusted-types@2.0.7':
optional: true
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 22.15.3 '@types/node': 22.15.3
@ -7102,10 +7082,6 @@ snapshots:
dependencies: dependencies:
domelementtype: 2.3.0 domelementtype: 2.3.0
dompurify@3.2.6:
optionalDependencies:
'@types/trusted-types': 2.0.7
domutils@3.2.2: domutils@3.2.2:
dependencies: dependencies:
dom-serializer: 2.0.0 dom-serializer: 2.0.0
@ -8194,8 +8170,6 @@ snapshots:
dependencies: dependencies:
semver: 6.3.1 semver: 6.3.1
marked@15.0.12: {}
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
mdn-data@2.0.28: {} mdn-data@2.0.28: {}

View file

@ -59,7 +59,7 @@ export interface UserResponse {
display_name: string | null, display_name: string | null,
avatar: string | null, avatar: string | null,
pronouns: string | null, pronouns: string | null,
about: string | null, about_me: string | null,
email?: string, email?: string,
email_verified?: boolean email_verified?: boolean
} }