Add profile context menu & test profile popup
All checks were successful
Android Build / publish (push) Successful in 53s
Linux Build / publish (push) Successful in 52s

This commit is contained in:
olcxja 2026-06-19 20:04:23 +02:00
commit 516bdf5186
12 changed files with 602 additions and 14 deletions

View file

@ -139,5 +139,10 @@
"larp.redacted": "this meow was hissed away.", "larp.redacted": "this meow was hissed away.",
"placeholder.invitation.code": "invitation code meow", "placeholder.invitation.code": "invitation code meow",
"info.messages.loading.older": "Loading older meowsages...", "info.messages.loading.older": "Loading older meowsages...",
"error:message.not.found": "Meowsage not found" "error:message.not.found": "Meowsage not found",
"title.profile": "cat profile",
"title.settings": "cat settings",
"title.logout": "log out",
"action.edit.profile": "edit cat profile",
"action.open.dm": "open direct meowchat"
} }

View file

@ -139,5 +139,10 @@
"larp.redacted": "This message was deleted.", "larp.redacted": "This message was deleted.",
"placeholder.invitation.code": "invitation code", "placeholder.invitation.code": "invitation code",
"info.messages.loading.older": "Loading older messages...", "info.messages.loading.older": "Loading older messages...",
"error:message.not.found": "Message not found" "error:message.not.found": "Message not found",
"title.profile": "Profile",
"title.settings": "Settings",
"title.logout": "Log out",
"action.edit.profile": "Edit profile",
"action.open.dm": "Open DM"
} }

View file

@ -49,7 +49,7 @@
<sidebarelement id="sidebar-profile"> <sidebarelement id="sidebar-profile">
<indicator> <indicator>
</indicator> </indicator>
<button class="icon-button" onclick="gotoSideProfilePopup()"> <button class="icon-button">
<img id="sidebar-pfp"> <img id="sidebar-pfp">
</button> </button>
</sidebarelement> </sidebarelement>
@ -79,6 +79,16 @@
<div id="fixed-context-menu" class="context-menu"> <div id="fixed-context-menu" class="context-menu">
</div> </div>
<div id="profile-popup-container" class="profile-popup-container" style="display: none;">
<div class="profile-popup-bg" onclick="closeProfile()"></div>
<div class="profile-popup-content">
<button class="profile-popup-close-btn" onclick="closeProfile()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
</button>
<div id="profile-popup-inner"></div>
</div>
</div>
</body> </body>
</html> </html>
<script src="screens.js"></script> <script src="screens.js"></script>

View file

@ -1411,6 +1411,10 @@ if (App) {
} }
function popstate() { function popstate() {
if (isProfileOpen) {
closeProfile();
return;
}
if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details')) { if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details')) {
history.back(); history.back();
if (App) { if (App) {
@ -1421,8 +1425,108 @@ function popstate() {
} }
function gotoSideProfilePopup() {
sidebarProfileButton.addEventListener("click", (e) => {
e.stopPropagation();
history.pushState({trap: true}, "", location.href);
if (fixedContextMenu.classList.contains("show")) {
fixedContextMenu.classList.remove("show");
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
setActiveSidebarIndicator(currentMainIndicator);
} else {
setActiveSidebarIndicator(sidebarProfileIndicator, true);
const rect = sidebarProfileButton.getBoundingClientRect();
showFixedContextMenu({top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left}, profileContextMenu);
}
});
function logout() {
localStorage.clear();
window.location.href = "login/index.html";
}
function gotoSettings() {
if (fixedContextMenu.classList.contains("show")) {
fixedContextMenu.classList.remove("show");
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
setActiveSidebarIndicator(currentMainIndicator);
}
}
let isProfileOpen = false;
async function openProfile(accountId) {
if (fixedContextMenu.classList.contains("show")) {
fixedContextMenu.classList.remove("show");
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
setActiveSidebarIndicator(currentMainIndicator);
}
let popupContainer = document.getElementById("profile-popup-container");
let popupInner = document.getElementById("profile-popup-inner");
//show loading
popupInner.innerHTML = '<div style="display: flex; justify-content: center; align-items: center; height: 100%;"><p>Loading profile...</p></div>';
popupContainer.style.display = "flex";
//force reflow
popupContainer.offsetHeight;
popupContainer.classList.add("show");
isProfileOpen = true;
try {
if (!accountId) accountId = window.id; //var fallback
let nameWithHost = await fetchAsync(`${url}/idtoname?id=${accountId}`);
if (nameWithHost.startsWith("error:")) nameWithHost = accountId; //api fallback
let nameParts = nameWithHost.split(":");
let profileName = nameParts[0] || accountId;
let profileHost = nameParts.slice(1).join(":") || host;
let pfpUrl = await getAvatarUrl(accountId, nameWithHost);
let bannerUrl = `${url}/user/storage/public/getentry?id=${accountId}&e=larp.profile.banner`;
let bannerHtml = `<img src="${bannerUrl}" class="profile-banner" onerror="this.outerHTML='<div class=\\'profile-banner\\'></div>'">`;
let bio = await fetchAsync(`${url}/user/storage/public/getentry?id=${accountId}&e=larp.profile.bio`);
if (bio.startsWith("error:")) bio = "";
let bioHtml = bio && bio.trim() !== "" ? `<div class="profile-bio">${bio.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</div>` : "";
let isOwnProfile = accountId === id;
let actionBtnHtml = isOwnProfile ?
`<button class="profile-action-btn">Edit Profile</button>` :
`<button class="profile-action-btn">Open DM</button>`;
popupInner.innerHTML = `
${bannerHtml}
<div class="profile-avatar-container">
<img src="${pfpUrl}" class="profile-avatar">
</div>
<div class="profile-info">
<div>
<div class="profile-name">${profileName}</div>
<div class="profile-id-host">${profileName}:${profileHost} (${accountId})</div>
</div>
${bioHtml}
${actionBtnHtml}
</div>
`;
} catch (e) {
popupInner.innerHTML = '<div style="display: flex; justify-content: center; align-items: center; height: 100%;"><p>Failed to load profile.</p></div>';
console.error(e);
}
}
function closeProfile() {
let popupContainer = document.getElementById("profile-popup-container");
popupContainer.classList.remove("show");
isProfileOpen = false;
setTimeout(() => {
popupContainer.style.display = "none";
}, 300);
} }
function setActiveRoombarItem(itemId) { function setActiveRoombarItem(itemId) {
@ -1820,7 +1924,7 @@ async function renderDms(res) {
let dms = parsed.dms || {}; let dms = parsed.dms || {};
let dmEntries = Object.entries(dms); let dmEntries = Object.entries(dms);
// Sort by timestamp descending //Sort by timestamp descending
dmEntries.sort((a, b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0)); dmEntries.sort((a, b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0));
let html = ""; let html = "";

View file

@ -217,6 +217,20 @@ var inboxRoomBar = `
//context menus //context menus
var profileContextMenu = `
<button onclick="openProfile(window.id)">
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M367-527q-47-47-47-113t47-113q47-47 113-47t113 47q47 47 47 113t-47 113q-47 47-113 47t-113-47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm296.5-343.5Q560-607 560-640t-23.5-56.5Q513-720 480-720t-56.5 23.5Q400-673 400-640t23.5 56.5Q447-560 480-560t56.5-23.5ZM480-640Zm0 400Z"/></svg>
<span class="blah">title.profile</span>
</button>
<button onclick="gotoSettings()">
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>
<span class="blah">title.settings</span>
</button>
<button onclick="logout()">
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--big-red)"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h280v80H200Zm440-160-55-58 102-102H360v-80h327L585-622l55-58 200 200-200 200Z"/></svg>
<span class="blah" style="color: var(--big-red)">title.logout</span>
</button>
`;
var addSpaceMenu = ` var addSpaceMenu = `
<button onclick="gotoJoinSpace()"> <button onclick="gotoJoinSpace()">
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z"/></svg>

View file

@ -111,6 +111,14 @@ input, textarea {
-webkit-user-select: text !important; -webkit-user-select: text !important;
} }
.chat-message-text, .chat-message-author, .chat-message-timestamp,
.profile-name, .profile-bio, .profile-id-host,
.dm-username, .group-name, .room-name {
user-select: text !important;
-webkit-user-select: text !important;
cursor: auto;
}
indicator { indicator {
content: ""; content: "";
@ -706,7 +714,7 @@ space{
button:hover, input:hover, textarea:hover { button:hover, input:hover, textarea:hover {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
.submit-button:hover { .submit-button:hover, .profile-action-btn:hover {
background-color: rgba(255, 255, 255, 0.5); background-color: rgba(255, 255, 255, 0.5);
} }
.add-action-button:hover { .add-action-button:hover {
@ -730,7 +738,7 @@ button:active, button.is-pressed, input:active, input.is-pressed, textarea:activ
button.active, input.active, textarea.active { button.active, input.active, textarea.active {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
.submit-button:active, .submit-button.is-pressed { .submit-button:active, .submit-button.is-pressed, .profile-action-btn:active, .profile-action-btn.is-pressed {
background-color: rgba(255, 255, 255, 0.5); background-color: rgba(255, 255, 255, 0.5);
} }
.add-action-button:active, .add-action-button.is-pressed { .add-action-button:active, .add-action-button.is-pressed {
@ -758,3 +766,151 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) {
background-color: rgba(255, 255, 255, 0.06); background-color: rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.08);
} }
.profile-popup-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 3000;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
}
.profile-popup-container.show {
pointer-events: auto;
}
.profile-popup-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0);
backdrop-filter: blur(0);
-webkit-backdrop-filter: blur(0);
transition: background 0.3s ease, backdrop-filter 0.3s ease, -webkit-backdrop-filter 0.3s ease;
}
.profile-popup-container.show .profile-popup-bg {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(0.25rem);
-webkit-backdrop-filter: blur(0.25rem);
}
.profile-popup-content {
position: relative;
width: 90vw;
max-width: 31rem;
height: 85vh;
max-height: 44rem;
background: var(--main-bg-color);
border: var(--border-width) solid var(--light-border-color);
border-radius: 0.9rem;
overflow: hidden;
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.5);
opacity: 0;
transform: scale(0.6);
transition: transform 0.2s cubic-bezier(0.4, -0.3, 0.7, 1), opacity 0.2s ease;
display: flex;
flex-direction: column;
}
.profile-popup-container.show .profile-popup-content {
transform: scale(1);
opacity: 1;
transition: transform 0.25s cubic-bezier(0.2, 1.15, 0.4, 1), opacity 0.25s ease;
}
.profile-popup-close-btn {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 10;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(0.25rem);
-webkit-backdrop-filter: blur(0.25rem);
border: none;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.8rem;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
margin: 0;
cursor: pointer;
}
.profile-popup-close-btn svg {
width: 1.5rem;
height: 1.5rem;
}
.profile-popup-close-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
#profile-popup-inner {
width: 100%;
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.profile-banner {
width: 100%;
height: 12.5rem;
background: linear-gradient(135deg, rgba(255,255,255,0.05), rgba(255,255,255,0.1));
object-fit: cover;
flex-shrink: 0;
}
.profile-avatar-container {
position: relative;
margin-top: -3.75rem;
margin-left: 2rem;
width: 7.5rem;
height: 7.5rem;
}
.profile-avatar {
width: 100%;
height: 100%;
border-radius: 0.8rem;
border: var(--border-width) solid rgba(255, 255, 255, 0.2);
object-fit: cover;
background: var(--main-bg-color);
}
.profile-info {
padding: 1rem 2rem 2rem 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.profile-name {
font-size: 2rem;
font-weight: 800;
line-height: 1.1;
margin-bottom: 0.2rem;
word-break: break-word;
}
.profile-id-host {
font-size: 1rem;
opacity: 0.6;
font-weight: 600;
}
.profile-bio {
font-size: 1.05rem;
line-height: 1.5;
white-space: pre-wrap;
background: rgba(255, 255, 255, 0.03);
padding: 1rem;
border-radius: 0.8rem;
border: var(--border-width) solid var(--light-border-color);
word-break: break-word;
}
.profile-action-btn {
align-self: flex-start;
padding: 0.6rem 1.5rem;
font-weight: 700;
background: var(--text-color);
color: var(--main-bg-color);
border-radius: 0.8rem;
margin-top: 0.5rem;
margin-left: 0;
height: auto;
}

View file

@ -139,5 +139,10 @@
"larp.redacted": "this meow was hissed away.", "larp.redacted": "this meow was hissed away.",
"placeholder.invitation.code": "invitation code meow", "placeholder.invitation.code": "invitation code meow",
"info.messages.loading.older": "Loading older meowsages...", "info.messages.loading.older": "Loading older meowsages...",
"error:message.not.found": "Meowsage not found" "error:message.not.found": "Meowsage not found",
"title.profile": "cat profile",
"title.settings": "cat settings",
"title.logout": "log out",
"action.edit.profile": "edit cat profile",
"action.open.dm": "open direct meowchat"
} }

View file

@ -139,5 +139,10 @@
"larp.redacted": "This message was deleted.", "larp.redacted": "This message was deleted.",
"placeholder.invitation.code": "invitation code", "placeholder.invitation.code": "invitation code",
"info.messages.loading.older": "Loading older messages...", "info.messages.loading.older": "Loading older messages...",
"error:message.not.found": "Message not found" "error:message.not.found": "Message not found",
"title.profile": "Profile",
"title.settings": "Settings",
"title.logout": "Log out",
"action.edit.profile": "Edit profile",
"action.open.dm": "Open DM"
} }

View file

@ -49,7 +49,7 @@
<sidebarelement id="sidebar-profile"> <sidebarelement id="sidebar-profile">
<indicator> <indicator>
</indicator> </indicator>
<button class="icon-button" onclick="gotoSideProfilePopup()"> <button class="icon-button">
<img id="sidebar-pfp"> <img id="sidebar-pfp">
</button> </button>
</sidebarelement> </sidebarelement>
@ -79,6 +79,16 @@
<div id="fixed-context-menu" class="context-menu"> <div id="fixed-context-menu" class="context-menu">
</div> </div>
<div id="profile-popup-container" class="profile-popup-container" style="display: none;">
<div class="profile-popup-bg" onclick="closeProfile()"></div>
<div class="profile-popup-content">
<button class="profile-popup-close-btn" onclick="closeProfile()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
</button>
<div id="profile-popup-inner"></div>
</div>
</div>
</body> </body>
</html> </html>
<script src="screens.js"></script> <script src="screens.js"></script>

View file

@ -1411,6 +1411,10 @@ if (App) {
} }
function popstate() { function popstate() {
if (isProfileOpen) {
closeProfile();
return;
}
if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details')) { if (!mainScreen.classList.contains('mobile-content') && !mainScreen.classList.contains('mobile-details')) {
history.back(); history.back();
if (App) { if (App) {
@ -1421,8 +1425,108 @@ function popstate() {
} }
function gotoSideProfilePopup() {
sidebarProfileButton.addEventListener("click", (e) => {
e.stopPropagation();
history.pushState({trap: true}, "", location.href);
if (fixedContextMenu.classList.contains("show")) {
fixedContextMenu.classList.remove("show");
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
setActiveSidebarIndicator(currentMainIndicator);
} else {
setActiveSidebarIndicator(sidebarProfileIndicator, true);
const rect = sidebarProfileButton.getBoundingClientRect();
showFixedContextMenu({top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left}, profileContextMenu);
}
});
function logout() {
localStorage.clear();
window.location.href = "login/index.html";
}
function gotoSettings() {
if (fixedContextMenu.classList.contains("show")) {
fixedContextMenu.classList.remove("show");
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
setActiveSidebarIndicator(currentMainIndicator);
}
}
let isProfileOpen = false;
async function openProfile(accountId) {
if (fixedContextMenu.classList.contains("show")) {
fixedContextMenu.classList.remove("show");
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
setActiveSidebarIndicator(currentMainIndicator);
}
let popupContainer = document.getElementById("profile-popup-container");
let popupInner = document.getElementById("profile-popup-inner");
//show loading
popupInner.innerHTML = '<div style="display: flex; justify-content: center; align-items: center; height: 100%;"><p>Loading profile...</p></div>';
popupContainer.style.display = "flex";
//force reflow
popupContainer.offsetHeight;
popupContainer.classList.add("show");
isProfileOpen = true;
try {
if (!accountId) accountId = window.id; //var fallback
let nameWithHost = await fetchAsync(`${url}/idtoname?id=${accountId}`);
if (nameWithHost.startsWith("error:")) nameWithHost = accountId; //api fallback
let nameParts = nameWithHost.split(":");
let profileName = nameParts[0] || accountId;
let profileHost = nameParts.slice(1).join(":") || host;
let pfpUrl = await getAvatarUrl(accountId, nameWithHost);
let bannerUrl = `${url}/user/storage/public/getentry?id=${accountId}&e=larp.profile.banner`;
let bannerHtml = `<img src="${bannerUrl}" class="profile-banner" onerror="this.outerHTML='<div class=\\'profile-banner\\'></div>'">`;
let bio = await fetchAsync(`${url}/user/storage/public/getentry?id=${accountId}&e=larp.profile.bio`);
if (bio.startsWith("error:")) bio = "";
let bioHtml = bio && bio.trim() !== "" ? `<div class="profile-bio">${bio.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</div>` : "";
let isOwnProfile = accountId === id;
let actionBtnHtml = isOwnProfile ?
`<button class="profile-action-btn">Edit Profile</button>` :
`<button class="profile-action-btn">Open DM</button>`;
popupInner.innerHTML = `
${bannerHtml}
<div class="profile-avatar-container">
<img src="${pfpUrl}" class="profile-avatar">
</div>
<div class="profile-info">
<div>
<div class="profile-name">${profileName}</div>
<div class="profile-id-host">${profileName}:${profileHost} (${accountId})</div>
</div>
${bioHtml}
${actionBtnHtml}
</div>
`;
} catch (e) {
popupInner.innerHTML = '<div style="display: flex; justify-content: center; align-items: center; height: 100%;"><p>Failed to load profile.</p></div>';
console.error(e);
}
}
function closeProfile() {
let popupContainer = document.getElementById("profile-popup-container");
popupContainer.classList.remove("show");
isProfileOpen = false;
setTimeout(() => {
popupContainer.style.display = "none";
}, 300);
} }
function setActiveRoombarItem(itemId) { function setActiveRoombarItem(itemId) {
@ -1820,7 +1924,7 @@ async function renderDms(res) {
let dms = parsed.dms || {}; let dms = parsed.dms || {};
let dmEntries = Object.entries(dms); let dmEntries = Object.entries(dms);
// Sort by timestamp descending //Sort by timestamp descending
dmEntries.sort((a, b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0)); dmEntries.sort((a, b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0));
let html = ""; let html = "";

View file

@ -217,6 +217,20 @@ var inboxRoomBar = `
//context menus //context menus
var profileContextMenu = `
<button onclick="openProfile(window.id)">
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M367-527q-47-47-47-113t47-113q47-47 113-47t113 47q47 47 47 113t-47 113q-47 47-113 47t-113-47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm296.5-343.5Q560-607 560-640t-23.5-56.5Q513-720 480-720t-56.5 23.5Q400-673 400-640t23.5 56.5Q447-560 480-560t56.5-23.5ZM480-640Zm0 400Z"/></svg>
<span class="blah">title.profile</span>
</button>
<button onclick="gotoSettings()">
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>
<span class="blah">title.settings</span>
</button>
<button onclick="logout()">
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--big-red)"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h280v80H200Zm440-160-55-58 102-102H360v-80h327L585-622l55-58 200 200-200 200Z"/></svg>
<span class="blah" style="color: var(--big-red)">title.logout</span>
</button>
`;
var addSpaceMenu = ` var addSpaceMenu = `
<button onclick="gotoJoinSpace()"> <button onclick="gotoJoinSpace()">
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z"/></svg>

View file

@ -111,6 +111,14 @@ input, textarea {
-webkit-user-select: text !important; -webkit-user-select: text !important;
} }
.chat-message-text, .chat-message-author, .chat-message-timestamp,
.profile-name, .profile-bio, .profile-id-host,
.dm-username, .group-name, .room-name {
user-select: text !important;
-webkit-user-select: text !important;
cursor: auto;
}
indicator { indicator {
content: ""; content: "";
@ -706,7 +714,7 @@ space{
button:hover, input:hover, textarea:hover { button:hover, input:hover, textarea:hover {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
.submit-button:hover { .submit-button:hover, .profile-action-btn:hover {
background-color: rgba(255, 255, 255, 0.5); background-color: rgba(255, 255, 255, 0.5);
} }
.add-action-button:hover { .add-action-button:hover {
@ -730,7 +738,7 @@ button:active, button.is-pressed, input:active, input.is-pressed, textarea:activ
button.active, input.active, textarea.active { button.active, input.active, textarea.active {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
.submit-button:active, .submit-button.is-pressed { .submit-button:active, .submit-button.is-pressed, .profile-action-btn:active, .profile-action-btn.is-pressed {
background-color: rgba(255, 255, 255, 0.5); background-color: rgba(255, 255, 255, 0.5);
} }
.add-action-button:active, .add-action-button.is-pressed { .add-action-button:active, .add-action-button.is-pressed {
@ -758,3 +766,151 @@ sidebarelement:has(.icon-button:active) indicator:not(.active) {
background-color: rgba(255, 255, 255, 0.06); background-color: rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.08);
} }
.profile-popup-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 3000;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
}
.profile-popup-container.show {
pointer-events: auto;
}
.profile-popup-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0);
backdrop-filter: blur(0);
-webkit-backdrop-filter: blur(0);
transition: background 0.3s ease, backdrop-filter 0.3s ease, -webkit-backdrop-filter 0.3s ease;
}
.profile-popup-container.show .profile-popup-bg {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(0.25rem);
-webkit-backdrop-filter: blur(0.25rem);
}
.profile-popup-content {
position: relative;
width: 90vw;
max-width: 31rem;
height: 85vh;
max-height: 44rem;
background: var(--main-bg-color);
border: var(--border-width) solid var(--light-border-color);
border-radius: 0.9rem;
overflow: hidden;
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.5);
opacity: 0;
transform: scale(0.6);
transition: transform 0.2s cubic-bezier(0.4, -0.3, 0.7, 1), opacity 0.2s ease;
display: flex;
flex-direction: column;
}
.profile-popup-container.show .profile-popup-content {
transform: scale(1);
opacity: 1;
transition: transform 0.25s cubic-bezier(0.2, 1.15, 0.4, 1), opacity 0.25s ease;
}
.profile-popup-close-btn {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 10;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(0.25rem);
-webkit-backdrop-filter: blur(0.25rem);
border: none;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.8rem;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
margin: 0;
cursor: pointer;
}
.profile-popup-close-btn svg {
width: 1.5rem;
height: 1.5rem;
}
.profile-popup-close-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
#profile-popup-inner {
width: 100%;
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.profile-banner {
width: 100%;
height: 12.5rem;
background: linear-gradient(135deg, rgba(255,255,255,0.05), rgba(255,255,255,0.1));
object-fit: cover;
flex-shrink: 0;
}
.profile-avatar-container {
position: relative;
margin-top: -3.75rem;
margin-left: 2rem;
width: 7.5rem;
height: 7.5rem;
}
.profile-avatar {
width: 100%;
height: 100%;
border-radius: 0.8rem;
border: var(--border-width) solid rgba(255, 255, 255, 0.2);
object-fit: cover;
background: var(--main-bg-color);
}
.profile-info {
padding: 1rem 2rem 2rem 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.profile-name {
font-size: 2rem;
font-weight: 800;
line-height: 1.1;
margin-bottom: 0.2rem;
word-break: break-word;
}
.profile-id-host {
font-size: 1rem;
opacity: 0.6;
font-weight: 600;
}
.profile-bio {
font-size: 1.05rem;
line-height: 1.5;
white-space: pre-wrap;
background: rgba(255, 255, 255, 0.03);
padding: 1rem;
border-radius: 0.8rem;
border: var(--border-width) solid var(--light-border-color);
word-break: break-word;
}
.profile-action-btn {
align-self: flex-start;
padding: 0.6rem 1.5rem;
font-weight: 700;
background: var(--text-color);
color: var(--main-bg-color);
border-radius: 0.8rem;
margin-top: 0.5rem;
margin-left: 0;
height: auto;
}