Add fetching invites
All checks were successful
Android Build / publish (push) Successful in 30s
Linux Build / publish (push) Successful in 53s

This commit is contained in:
olcxja 2026-05-21 11:02:02 +02:00
commit 81e222250d
10 changed files with 690 additions and 262 deletions

View file

@ -74,12 +74,21 @@
"title.unread": "Unread",
"desc.no.invites": "No invites found",
"desc.no.notifications": "No notifications found",
"desc.fetching.invites": "Fetching invites...",
"desc.fetching.notifications": "Fetching notifications...",
"desc.invite.dm.received": "{0} ({1}) invited you to chat",
"desc.invite.dm.sent": "You invited {0} ({1}) to chat",
"desc.invite.group.received": "{0} ({1}) invited you to a group",
"desc.invite.group.sent": "You invited {0} ({1}) to a group",
"action.fetching.invites.sent": "Fetching sent invites...",
"action.fetching.invites.recv": "Fetching received invites...",
"action.dm.fetch": "Fetching dms...",
"action.dm.adding": "Adding...",
"action.auth": "Authenticating...",
"action.dm.adding": "Adding...",
"action.invite.revoking": "Revoking invite...",
"action.invite.accepting": "Accepting invite...",
"action.invite.declining": "Declining invite...",
"title.sign.up": "Sign Up",
"title.sign.in": "Sign In",

View file

@ -94,7 +94,7 @@
{
await updateProtocolAndUrl(window.location.hostname);
}
sidebarPfp.src = await getAvatarUrl(username);
sidebarPfp.src = await getAvatarUrl(`${username}:${host}`);
showAction("action.auth", "startauth");
let res = await Auth(username, password);

View file

@ -406,6 +406,7 @@ function showNotification(message, type = 'info', duration = 3500) {
notif.addEventListener('transitionend', () => {
notif.remove();
});
setTimeout(() => notif.remove(), 300);
}, duration);
}
@ -419,7 +420,7 @@ function showAction(message, actionid) {
}
const notif = document.createElement('div');
notif.className = `notification action`;
notif.className = `notification`;
notif.textContent = processBlah(message);
notif.id = `notification-${actionid}`;
@ -438,9 +439,10 @@ function clearAction(actionid) {
notifications.forEach(notif => {
notif.classList.remove('show');
notif.addEventListener('transitionend', () => {
notif.addEventListener('transitionend', async () => {
notif.remove();
}, { once: true });
setTimeout(() => notif.remove(), 300);
});
}
@ -596,13 +598,27 @@ async function initBlahs() {
}
}
function splitLimit(str, separator, limit) {
const parts = str.split(separator);
if (parts.length <= limit) {
return parts;
}
return [
...parts.slice(0, limit - 1),
parts.slice(limit - 1).join(separator)
];
}
function processBlah(blahmessage) {
try {
if (!blahmessage.includes(":")) {
blahmessage = `:${blahmessage}`;
}
let split = blahmessage.split(":");
let split = splitLimit(blahmessage, ":", 3);
let values = [];
try {
values = split[2].split(";");
@ -614,6 +630,11 @@ function processBlah(blahmessage) {
let valueslist = "";
for (let i = 0; i < values.length; i++) {
let value = processBlah(values[i]);
if (value.startsWith(":"))
{
value = value.substring(1);
}
valueslist+=`${value}, `;
message = message.replaceAll(`{${i}}`, value);
@ -964,60 +985,153 @@ function setActiveRoombarItem(itemId) {
}
}
async function renderInvites(res, type) {
let container = document.getElementById("invites-container");
if (!container) return;
if (res && res != "") {
res = JSON.parse(res);
let invites = [
...Object.entries(res.dms).map(([id, value]) => ({
id,
type: "dm",
...value
})),
...Object.entries(res.groups).map(([id, value]) => ({
id,
type: "group",
...value
}))
].sort((a, b) => b.timestamp - a.timestamp);
container.classList.remove("empty");
let html = "";
let count = 0;
for (let i = 0; i < invites.length; i++) {
let invite = invites[i];
if (invite.type === "dm") {
let id = invite.id;
if (!id.includes(":")) {
id += `:${host}`;
}
let username = await fetchAsync(`${url}/idtoname?id=${id}`);
if (!(username && username.trim() !== "")) return;
count++;
let pfp = await getAvatarUrl(username);
let actions = "";
let desc = "";
if (type === "received") {
desc = `:desc.invite.dm.received:${username};${id}`;
actions = `
<button class="icon-button" style="background-color: var(--big-green); border-color: transparent;" onclick="acceptInvite('${id}')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#fff"><path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z"/></svg>
</button>
<button class="icon-button" style="background-color: var(--big-red); border-color: transparent;" onclick="declineInvite('${id}')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#fff"><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>
`;
} else {
desc = `:desc.invite.dm.sent:${username};${id}`;
actions = `
<button class="icon-button" style="background-color: var(--big-red); border-color: transparent;" onclick="revokeInvite('${id}')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#fff"><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>
`;
}
let entry = invitesEntry
.replaceAll("{username}", username.split(':')[0])
.replaceAll("{pfp}", pfp)
.replaceAll("{desc}", desc)
.replaceAll("{actions}", actions);
html += entry;
}
}
if (count > 0) {
container.innerHTML = html;
} else {
container.classList.add("empty");
container.innerHTML = invitesEmptyState;
}
} else {
container.classList.add("empty");
container.innerHTML = invitesEmptyState;
}
let blahTags = container.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
}
async function loadInvites(tab) {
try {
showAction(`action.fetching.invites.${tab === 'received' ? 'recv' : 'sent'}`, `fetching.invites.${tab}`);
let res = await fetchEncrypted(`user/invites/${tab}`);
await renderInvites(res, tab);
} catch (e) {
showBlahNotification("error:something.wrong");
console.error(e);
await renderInvites("", tab);
}
clearAction(`fetching.invites.${tab}`);
}
async function switchInvitesTab(tab) {
if (!document.getElementById(`tab-invites-${tab}`).classList.contains('tab-inactive')) return;
document.getElementById('tab-invites-received').classList.add('tab-inactive');
document.getElementById('tab-invites-sent').classList.add('tab-inactive');
document.getElementById(`tab-invites-${tab}`).classList.remove('tab-inactive');
let container = document.getElementById("invites-container");
let innerString = "";
if (tab === "received")
{
try {
showAction("action.fetching.invites.recv", "fetching.invites.recv");
let res = await fetchEncrypted("user/invites/received");
if (res !== "")
{
if (res.includes(",")) {
console.log(res);
}
else
{
console.log(res);
container.innerHTML = invitesEntry;
}
}
}
catch (e) {
showBlahNotification("error:something.wrong");
console.error(e);
}
clearAction("fetching.invites.recv");
await loadInvites(tab);
}
async function acceptInvite(username) { //TODO
try {
showAction("action.invite.accepting", "invite.action");
let res = await fetchEncrypted("user/dm/create", username);
clearAction("invite.action");
showBlahNotification(res || "success:invite.accepted");
await loadInvites('received');
} catch (e) {
clearAction("invite.action");
showBlahNotification("error:something.wrong");
}
else if (tab === "sent")
{
try {
showAction("action.fetching.invites.sent", "fetching.invites.sent");
let res = await fetchEncrypted("user/invites/sent");
if (res !== "")
{
if (res.includes(",")) {
console.log(res);
}
else
{
console.log(res);
container.innerHTML = invitesEntry;
}
}
}
catch (e) {
showBlahNotification("error:something.wrong");
console.error(e);
}
clearAction("fetching.invites.sent");
}
async function declineInvite(username) { //TODO
try {
showAction("action.invite.declining", "invite.action");
let res = await fetchEncrypted("user/dm/decline", username);
clearAction("invite.action");
showBlahNotification(res || "success:invite.declined");
await loadInvites('received');
} catch (e) {
clearAction("invite.action");
showBlahNotification("error:something.wrong");
}
}
async function revokeInvite(username) { //TODO
try {
showAction("action.invite.revoking", "invite.action");
let res = await fetchEncrypted("user/dm/revoke", username);
clearAction("invite.action");
showBlahNotification(res || "success:invite.revoked");
await loadInvites('sent');
} catch (e) {
clearAction("invite.action");
showBlahNotification("error:something.wrong");
}
}

View file

@ -73,30 +73,58 @@ var joinSpaceScreen = `
</div>
`;
var invitesEmptyState = `
<svg xmlns="http://www.w3.org/2000/svg" width="4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280L160-640v400h640v-400L480-440Zm0-80 320-200H160l320 200ZM160-640v-80 480-400Z"/></svg>
<br>
<p><blah>desc.no.invites</blah></p>
`;
var invitesLoadingState = `
<svg xmlns="http://www.w3.org/2000/svg" width="4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280L160-640v400h640v-400L480-440Zm0-80 320-200H160l320 200ZM160-640v-80 480-400Z"/></svg>
<br>
<p><blah>desc.fetching.invites</blah></p>
`;
var invitesScreen = `
<div style="display: flex; flex-direction: column; height: 100%;">
<div style="display: flex; gap: 0.5rem; padding: 0.5rem 1rem; border-bottom: var(--border-width) solid var(--light-border-color); flex-shrink: 0;">
<button id="tab-invites-received" class="submit-button blah tab-inactive" onclick="switchInvitesTab('received')" style="margin:0; padding: 0 1.5rem;">title.received</button>
<button id="tab-invites-sent" class="submit-button blah tab-inactive" onclick="switchInvitesTab('sent')" style="margin:0; padding: 0 1.5rem;">title.sent</button>
</div>
<div id="invites-container" style="flex-grow: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; opacity: 0.5;">
<svg xmlns="http://www.w3.org/2000/svg" width="4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280L160-640v400h640v-400L480-440Zm0-80 320-200H160l320 200ZM160-640v-80 480-400Z"/></svg>
<br>
<p><blah>desc.no.invites</blah></p>
<div id="invites-container" class="list-container empty">
${invitesLoadingState}
</div>
</div>
`;
var notifisEmptyState = `
<svg xmlns="http://www.w3.org/2000/svg" width="4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M160-200v-80h80v-280q0-83 50-147.5T420-792v-28q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820v28q80 20 130 84.5T720-560v280h80v80H160Zm320-300Zm0 420q-33 0-56.5-23.5T400-160h160q0 33-23.5 56.5T480-80ZM320-280h320v-280q0-66-47-113t-113-47q-66 0-113 47t-47 113v280Z"/></svg>
<br>
<p><blah>desc.no.notifications</blah></p>
`;
var notifisScreen = `
<div style="display: flex; flex-direction: column; height: 100%;">
<div style="display: flex; gap: 0.5rem; padding: 0.5rem 1rem; border-bottom: var(--border-width) solid var(--light-border-color); flex-shrink: 0;">
<button id="tab-notifis-all" class="submit-button blah tab-inactive" onclick="switchNotifisTab('all')" style="margin:0; padding: 0 1.5rem;">title.all</button>
<button id="tab-notifis-unread" class="submit-button blah tab-inactive" onclick="switchNotifisTab('unread')" style="margin:0; padding: 0 1.5rem;">title.unread</button>
</div>
<div id="notifis-container" style="flex-grow: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; opacity: 0.5;">
<svg xmlns="http://www.w3.org/2000/svg" width="4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M160-200v-80h80v-280q0-83 50-147.5T420-792v-28q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820v28q80 20 130 84.5T720-560v280h80v80H160Zm320-300Zm0 420q-33 0-56.5-23.5T400-160h160q0 33-23.5 56.5T480-80ZM320-280h320v-280q0-66-47-113t-113-47q-66 0-113 47t-47 113v280Z"/></svg>
<br>
<p><blah>desc.no.notifications</blah></p>
<div id="notifis-container" class="list-container empty">
${notifisEmptyState}
</div>
</div>
`;
var invitesEntry = `
<div class="invite-entry">
<div class="invite-info">
<img src="{pfp}" class="invite-pfp">
<div class="invite-details">
<span class="invite-name">{username}</span>
<span class="invite-desc"><blah>{desc}</blah></span>
</div>
</div>
<div class="invite-actions">
{actions}
</div>
</div>
`;
@ -171,5 +199,3 @@ var addSpaceMenu = `
//elements
var detailsBtn = `<button class="mobile-nav-btn right" onclick="mobileNavDetails()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></button>`;
var backBtnHtml = `<button class="mobile-nav-btn" onclick="mobileNavBack()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"/></svg></button>`;
var invitesEntry = `???`;

View file

@ -13,10 +13,10 @@
--icon-button-height: 3.4rem;
--button-height: 2.4rem;
--border-width: 0.09rem;
--big-default: rgb(207 207 207);
--big-red: rgb(202 0 0);
--big-green: rgb(25 189 0);
--big-default: rgb(207, 207, 207);
--big-red: rgb(195, 75, 75);
--big-green: rgb(75, 165, 95);
}
*:focus {
outline: none;
@ -76,7 +76,7 @@ button, input {
background-color: rgba(255, 255, 255, 0.05);
margin: var(--button-margin);
padding: var(--button-margin);
height: var(--button-height);
display: flex;
cursor: pointer;
@ -148,12 +148,13 @@ indicator.active {
width: calc(var(--icon-button-height) - (var(--border-width) * 2));
height: calc(var(--icon-button-height) - (var(--border-width) * 2));
pointer-events: none !important;
border-radius: 0.6rem;
}
roomcontent {
display: flex;
flex-direction: column;
height: 100%;
flex-grow: 1;
border-right: var(--border-width) solid var(--light-border-color);
@ -161,7 +162,7 @@ roomcontent {
roomcontent2 {
display: flex;
flex-direction: column;
height: calc(100% - (var(--button-height) * 1.5));
width: 100%;
}
@ -169,7 +170,7 @@ roomcontent2 {
sidebar {
display: flex;
flex-direction: column;
height: 100%;
width: calc(var(--icon-button-height) + (var(--button-margin) * 2) + var(--border-width));
border-right: var(--border-width) solid var(--light-border-color);
@ -252,11 +253,11 @@ hr {
box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.4);
opacity: 0;
transform: translateY(-1rem);
transform: translateY(-2rem);
transition:
opacity 0.3s ease,
transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275),
background-color 0.2s ease;
opacity 0.2s ease,
transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275),
background-color 0.15s ease;
}
.notification.show {
opacity: 1;
@ -290,8 +291,8 @@ roomtopbar {
sidebar.second#roomdetailsbar {
height: 100%;
display: flex;
flex-direction: column;
border-left: var(--border-width) solid var(--light-border-color);
flex-direction: column;
border-left: var(--border-width) solid var(--light-border-color);
}
fullcontainer {
width: 100%;
@ -330,52 +331,7 @@ herotitle {
.bottom-element {
margin-top: auto;
}
@media (hover: hover) {
sidebarelement:hover indicator:not(.active) {
content: "";
position: absolute;
width: 0;
height: 1.2rem;
left: -0.08rem;
border: 0.15rem solid var(--text-color);
border-radius: 0.8rem;
transform: translateY(calc( ( var(--icon-button-height) + (var(--button-margin) * 2) ) / 2 - 0.6rem));
}
button:hover, input:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.submit-button:hover {
background-color: rgba(255, 255, 255, 0.5);
}
.add-action-button:hover {
background-color: rgba(255, 255, 255, 0.1);
color: var(--text-color);
}
}
button:active, button.is-pressed, input:active, input.is-pressed {
background-color: rgba(255, 255, 255, 0.1);
transform: var(--press-scale);
}
button.active, input.active {
background-color: rgba(255, 255, 255, 0.1);
}
.submit-button:active, .submit-button.is-pressed {
background-color: rgba(255, 255, 255, 0.5);
}
.add-action-button:active, .add-action-button.is-pressed {
background-color: rgba(255, 255, 255, 0.1);
color: var(--text-color);
}
sidebarelement:has(.icon-button:active) indicator:not(.active) {
content: "";
position: absolute;
width: 0;
height: 1.2rem;
left: -0.08rem;
border: 0.15rem solid var(--text-color);
border-radius: 0.8rem;
transform: translateY(calc( ( var(--icon-button-height) + (var(--button-margin) * 2) ) / 2 - 0.6rem));
}
blah, inherit, .inherit {
all: inherit;
padding: 0;
@ -419,9 +375,7 @@ space{
gap: 0.5rem;
font-weight: 600;
}
.context-menu button:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.mobile-nav-btn {
display: none;
background: transparent;
@ -466,7 +420,7 @@ space{
sidebar, sidebar.second, roomcontent {
position: absolute !important;
height: 100%;
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
transition: all 0.25s cubic-bezier(0.25, 1, 0.5, 1);
}
sidebar {
@ -492,13 +446,13 @@ space{
}
#roomdetailsbar {
left: auto !important;
left: auto !important;
right: 0;
width: 90vw !important;
width: 90vw !important;
min-width: unset !important;
max-width: none !important;
transform: translateX(100vw);
z-index: 10;
transform: translateX(100vw);
z-index: 10;
background-color: var(--main-bg-color);
}
@ -537,7 +491,7 @@ space{
opacity: 0;
pointer-events: none;
transition: opacity 0.3s cubic-bezier(0.25, 1, 0.5, 1);
transition: all 0.25s cubic-bezier(0.25, 1, 0.5, 1);
}
main.mobile-details roomcontent::after {
@ -553,4 +507,115 @@ space{
.submit-button.tab-inactive {
background-color: transparent;
color: var(--text-color);
}
.list-container {
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem;
overflow-y: auto;
gap: 0.5rem;
}
.list-container.empty {
justify-content: center;
text-align: center;
opacity: 0.5;
}
.invite-entry {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.8rem 1rem;
border: var(--border-width) solid var(--light-border-color);
border-radius: 0.8rem;
width: 100%;
background: rgba(255, 255, 255, 0.02);
flex-shrink: 0;
}
.invite-info {
display: flex;
align-items: center;
gap: 1rem;
}
.invite-pfp {
width: 2.8rem;
height: 2.8rem;
border-radius: 0.8rem;
object-fit: cover;
}
.invite-details {
display: flex;
flex-direction: column;
text-align: left;
}
.invite-name {
font-weight: 700;
font-size: 1.05rem;
}
.invite-desc {
font-size: 0.85rem;
opacity: 0.6;
}
.invite-actions {
display: flex;
gap: 0.5rem;
}
.invite-actions button {
margin: 0;
}
@media (hover: hover) {
sidebarelement:hover indicator:not(.active) {
content: "";
position: absolute;
width: 0;
height: 1.2rem;
left: -0.08rem;
border: 0.15rem solid var(--text-color);
border-radius: 0.8rem;
transform: translateY(calc( ( var(--icon-button-height) + (var(--button-margin) * 2) ) / 2 - 0.6rem));
}
button:hover, input:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.submit-button:hover {
background-color: rgba(255, 255, 255, 0.5);
}
.add-action-button:hover {
background-color: rgba(255, 255, 255, 0.1);
color: var(--text-color);
}
.context-menu button:hover {
background-color: rgba(255, 255, 255, 0.08);
}
}
button:active, button.is-pressed, input:active, input.is-pressed {
background-color: rgba(255, 255, 255, 0.1);
transform: var(--press-scale);
}
button.active, input.active {
background-color: rgba(255, 255, 255, 0.1);
}
.submit-button:active, .submit-button.is-pressed {
background-color: rgba(255, 255, 255, 0.5);
}
.add-action-button:active, .add-action-button.is-pressed {
background-color: rgba(255, 255, 255, 0.1);
color: var(--text-color);
}
.context-menu button:active, .context-menu button.is-pressed {
background-color: rgba(255, 255, 255, 0.08);
}
sidebarelement:has(.icon-button:active) indicator:not(.active) {
content: "";
position: absolute;
width: 0;
height: 1.2rem;
left: -0.08rem;
border: 0.15rem solid var(--text-color);
border-radius: 0.8rem;
transform: translateY(calc( ( var(--icon-button-height) + (var(--button-margin) * 2) ) / 2 - 0.6rem));
}