Compare commits

...
Sign in to create a new pull request.

19 commits

Author SHA1 Message Date
802333a7aa Add fetching older messages 2026-06-03 12:46:36 +02:00
2e2c080df4 Forgot to fix keyboard 2026-06-03 01:03:02 +02:00
005d2e2820 Fix ui bugs:
- keyboard screen glitch
- multi-line messages
- missing styling
2026-06-03 00:56:29 +02:00
92edb123f3 working messages!!@!@ 2026-06-02 12:53:41 +02:00
b24b36adec Fix race conditions 2026-06-02 11:58:18 +02:00
aa0351fb2d Add key mismatch error handling 2026-05-30 11:05:10 +02:00
bba80a19df Change send message icon & slightly tweak border radius 2026-05-30 10:31:17 +02:00
c5004dd52b test installer (fix taksbar icon) 2026-05-29 22:38:05 +02:00
5c91c341a0 improved ui and added missing blahs 2026-05-29 10:13:58 +02:00
f957a081f3 add message reading 2026-05-29 09:32:24 +02:00
be26703908 add DM screen 2026-05-29 08:54:18 +02:00
a666cb9915 blahs update 2026-05-28 13:19:39 +02:00
8f28879f18 DM keys!!! 2026-05-27 23:45:38 +02:00
a660ba32bd Switch to X25519 + ML-KEM-768 encryption 2026-05-27 20:40:24 +02:00
9e6d128839 Make most requests use id instead of username 2026-05-27 19:41:30 +02:00
2979881908 Add test dm accepting, revoking and declining 2026-05-27 08:53:24 +02:00
81e222250d Add fetching invites 2026-05-21 11:02:02 +02:00
81145968a1 Add blahs in actions 2026-05-21 08:28:06 +02:00
5a9875ea01 Merge pull request 'blahy (teraz mam wiecej blahow niz mumin :3)' (#1) from sugary/LarpixClient:main into main
Reviewed-on: olcxjas-softworks/LarpixClient#1
2026-05-20 13:16:39 +02:00
21 changed files with 3159 additions and 419 deletions

View file

@ -33,7 +33,7 @@ jobs:
run: npx cap copy electron run: npx cap copy electron
- name: Build - name: Build
run: mkdir -p build && cd build && npx electron-packager ../electron miarven --platform=linux --arch=x64 --icon=../icons/icon.png --overwrite --asar && cp ../icons/icon.png ./miarven-linux-x64/icon.png && cp ../assets/Miarven.desktop ./miarven-linux-x64/Miarven.desktop run: mkdir -p build && cd build && npx electron-packager ../electron miarven --platform=linux --arch=x64 --icon=../icons/icon.png --overwrite --asar && cp ../icons/icon.png ./miarven-linux-x64/icon.png && cp ../assets/olcxja.miarven.desktop ./miarven-linux-x64/olcxja.miarven.desktop && cp ../assets/install.sh ./miarven-linux-x64/install.sh && chmod +x ./miarven-linux-x64/install.sh
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3

View file

@ -0,0 +1,132 @@
{
"user.not.found": "meowser not found :c",
"dm.already.exists": "direct meowchat already exists :3",
"cant.invite.urself": "you can't invite yourself, silly :3",
"user.already.invited": "you have already invited this user meow",
"user.invited": "user invited successfully :3",
"cant.create.dm.without.invitation": "you can't create a dm without invitation ><",
"dm.begin.notice": "at the beginning of a meowchat, you should always verify that the person you're talking to is the intended recipient",
"failed.accept.dm": "failed to accept direct meowchat",
"dm.accepted": "direct meowchat accepted",
"no.invite.found": "you can't create a dm without invitation *hiss*",
"unknown.error": "unknown error :o",
"forbidden": "you do not have pawmission to perform this action *hiss*",
"dm.key.too.large": "meowchat key too large :o",
"dm.key.not.found": "meowchat key not found :c",
"dm.key.updated": "meowchat key updated successfully :3",
"body.too.large": "request body too large ><",
"invite.revoked": "invite revoked successfully",
"invite.declined": "invite declined successfully",
"account.creation.request.expired": "cat creation request expired. try again",
"invalid.username": "invalid cat name: {0}",
"invalid.password": "invalid meow word: {0}",
"incorrect.captcha": "incorrect captcha. try meowgain",
"username.taken": "this cat name is already taken",
"accounts.slots.full": "you can't create a new cat because all collars are used. try again later",
"registration.disabled": "registration disabled",
"account.created": "cat created successfully",
"password.changed": "meow word changed successfully",
"keys.updated": "nametag updated successfully",
"unknown.request": "unknown request: {0}",
"login.successful": "pawgin successful :3",
"username.length": "cat name must be {0} characters long",
"username.conditions.allowed": "cat name can only include {all}",
"password.not.hashed.properly": "meow word is not hashed properly",
"invalid.username.or.password": "invalid cat name or meow word",
"account.not.exist": "cat with this name doesn't exist",
"invalid.nonce": "invalid nonce. try again",
"username.changed": "cat name changed successfully",
"auth.failed.redirect.to.login": "failed to find owner. redirecting to pawgin...",
"dm.refresh.failed": "failed to refresh direct meowchats",
"chat.add.failed": "failed to add meowchat",
"something.wrong.mayb.pass": "something went wrong... :c (probably wrong meow word)",
"something.wrong": "something went wrong... :c",
"passwords.not.match": "meow word do not match",
"password.cant.empty": "meow word cannot be empty",
"username.cant.empty": "cat name cannot be empty",
"bad.request": "bad request!!!",
"letters": "letters",
"numbers": "numbers",
"underscores": "underscores",
"loading.connecting": "connecting...",
"loading.loading": "loading...",
"loading.done": "ready! :3",
"title.home": "bed",
"title.dms": "direct meowchats",
"title.groups": "clowder",
"title.welcome.splash": "welpaw to Meowven",
"desc.welcome.splash": "first Larpix client",
"title.splash": "splash",
"title.create.dm": "invite to direct meowchat",
"title.create.space": "create house",
"title.create.group": "create clowder",
"title.add.chat": "add meowchat",
"desc.add.chat": "add a private, encrypted meowchat by entering a cat name",
"desc.create.group": "create a private, encrypted clowder",
"placeholder.create.group.input": "my clowder",
"placeholder.create.space.input": "my house",
"title.name": "name",
"desc.create.space": "create a house for your community",
"desc.join.space": "join a community that fits you",
"title.join.space": "join house",
"title.space.id": "house id",
"title.inbox": "inbox",
"title.invites": "invites",
"title.notifications": "notifications",
"title.received": "received",
"title.sent": "sent",
"title.all": "all",
"title.unread": "unread",
"desc.no.invites": "no invites found :c",
"desc.no.notifications": "no notifications found :c",
"desc.fetching.invites": "fetching invites...",
"desc.fetching.notifications": "fetching notifications...",
"desc.invite.dm.received": "{0} ({1}) invited you to meowchat",
"desc.invite.dm.sent": "you invited {0} ({1}) to meowchat",
"desc.invite.group.received": "{0} ({1}) invited you to a clowder",
"desc.invite.group.sent": "you invited {0} ({1}) to a clowder",
"action.fetching.invites.sent": "fetching sent invites...",
"action.fetching.invites.recv": "fetching received invites...",
"action.dm.fetch": "fetching direct meowchats...",
"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",
"title.sign.in.to": "sign in to your larpix instance",
"title.server.host": "server host",
"title.username": "meow name",
"title.password": "meow word",
"title.new.here.?": "new here?",
"title.create.an.account": "create a cat",
"join.larpix": "join Larpix",
"title.create.new.account": "create new larpix cat account",
"notice.use.strong.pass": "remember to use a strong meow word! tt will be used to lock your collar",
"title.confirm.password": "confirm meow word",
"title.already.registered.?": "already a kitty?",
"title.back.to.login": "back to pawgin",
"title.verify": "verify",
"title.captcha.desc": "prove you are a kitty",
"title.captcha.code": "captcha code",
"title.invitation.code": "invitation code",
"title.back.to.register": "back to registration",
"placeholder.username": "cat name",
"placeholder.captcha.code": "captcha code",
"placeholder.message.input": "meow...",
"desc.messages.loading": "loading meows...",
"desc.no.dms": "no direct meowchats :c",
"action.dm.opening": "opening meowchat...",
"dm.open.failed": "failed to open meowchat :c",
"keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on",
"keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c",
"dm.messages.fetch.failed": "failed to fetch meows :c",
"messages.decrypt.failed": "failed to decrypt meow :c"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -6,6 +6,7 @@
<title>Miarven</title> <title>Miarven</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="icon" type="image/svg+xml" href="favicon.svg"> <link rel="icon" type="image/svg+xml" href="favicon.svg">
<script src="crypto-pq.js"></script>
</head> </head>
<body> <body>
<loading> <loading>
@ -85,7 +86,6 @@
async function start() { async function start() {
updateLoadingStatus("loading.loading"); updateLoadingStatus("loading.loading");
try { try {
gotoHome();
if (host != null) if (host != null)
{ {
await updateProtocolAndUrl(host); await updateProtocolAndUrl(host);
@ -94,13 +94,21 @@
{ {
await updateProtocolAndUrl(window.location.hostname); await updateProtocolAndUrl(window.location.hostname);
} }
sidebarPfp.src = await getAvatarUrl(username); let localIdRes = await fetchAsync(`${url}/nametoid?u=${username}`);
let localId = localIdRes.trim();
sidebarPfp.src = await getAvatarUrl(localId, `${username}:${host}`);
showAction("Authenticating...", "startauth"); showAction("action.auth", "startauth");
let res = await Auth(username, password); let res = await Auth(username, password);
clearAction("startauth"); clearAction("startauth");
if (res.startsWith("success:")) { if (res.startsWith("success:")) {
await refreshDms(); try {
await ensureUserKeys();
} catch (e) {
//if fails continue loading encryption may be broken
console.error(e);
}
gotoHome();
} else { } else {
showBlahNotification("error:auth.failed.redirect.to.login"); showBlahNotification("error:auth.failed.redirect.to.login");
await delay(2000); await delay(2000);
@ -116,13 +124,18 @@
} }
} }
async function refreshDms() { async function refreshDms(waittime = 0) {
try { try {
showAction("action.dm.fetch", "dmrefresh");
await delay(waittime);
showAction("Refreshing dms...", "dmrefresh"); if (window.cachedDms && typeof renderDms === 'function') {
let res = await fetchEncrypted("user/dm/list", ""); await renderDms(window.cachedDms);
console.log(res); }
let res = await fetchEncrypted("user/dm/list");
window.cachedDms = res;
if (typeof renderDms === 'function') {
await renderDms(res);
}
clearAction("dmrefresh"); clearAction("dmrefresh");
} }
catch (e) { catch (e) {
@ -133,9 +146,17 @@
async function addDm() { async function addDm() {
try { try {
showAction("Adding...", "dmadd"); showAction("action.dm.adding", "dmadd");
let res = await fetchEncrypted("user/dm/invite", document.getElementById("addchat-username").value); let username = document.getElementById("addchat-username").value;
console.log(res); let idRes = await fetchAsync(`${url}/nametoid?u=${username}`);
if (idRes.startsWith("error") || idRes.trim() === "0" || idRes.trim() === "") {
clearAction("dmadd");
showBlahNotification("error:user.not.found");
return;
}
let targetId = idRes.trim();
let res = await fetchEncrypted("user/dm/invite", targetId);
clearAction("dmadd"); clearAction("dmadd");
showBlahNotification(res); showBlahNotification(res);
} }

View file

@ -6,6 +6,7 @@
<title>Miarven - Login</title> <title>Miarven - Login</title>
<link rel="stylesheet" href="../style.css"> <link rel="stylesheet" href="../style.css">
<link rel="icon" type="image/svg+xml" href="../favicon.svg"> <link rel="icon" type="image/svg+xml" href="../favicon.svg">
<script src="../crypto-pq.js"></script>
<style> <style>
html { html {
font-size: max(17px, calc(100vw / 100)); font-size: max(17px, calc(100vw / 100));
@ -321,16 +322,20 @@
formLogin.addEventListener('submit', async (e) => { formLogin.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
await updateProtocolAndUrl(loginHost.value);
let res = await Auth(loginUsername.value, loginPassword.value); let res = await Auth(loginUsername.value, loginPassword.value);
if (res.startsWith("success:")) if (res.startsWith("success:"))
{ {
showBlahNotification(res); let parts = res.split("|");
showBlahNotification(parts[0]);
await delay(800); await delay(800);
localStorage.setItem("username", loginUsername.value); localStorage.setItem("username", loginUsername.value);
localStorage.setItem("password", loginPassword.value); localStorage.setItem("password", loginPassword.value);
localStorage.setItem("host", loginHost.value); localStorage.setItem("host", loginHost.value);
if (parts.length > 1) {
localStorage.setItem("id", parts[1]);
}
await loadingFadeIn(); await loadingFadeIn();
@ -371,14 +376,18 @@
container.className = 'auth-container show-register'; container.className = 'auth-container show-register';
return; return;
} }
await updateProtocolAndUrl(registerHost.value);
let dataarray = keyDataFromServerJson(await fetchAsync(`${url}/createaccount?step=init`)); let dataarray = keyDataFromServerJson(await fetchAsync(`${url}/createaccount?step=init`));
var sharedkey = await calcCommunicationKeyClient(dataarray[0], dataarray[1], dataarray[2]); var sharedkey = await calcHybridSharedKeyClient(dataarray[0], dataarray[1]);
sharedpvkey = sharedkey[1]; sharedpvkey = sharedkey[2];
createId = dataarray[3]; createId = dataarray[2];
const captchaimage = await fetch(`${url}/createaccount?step=register`, { const captchaimage = await fetch(`${url}/createaccount?step=register`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
pubClient: sharedkey[0], pubX25519: sharedkey[0],
ciphertextMlKem: sharedkey[1],
idKey: createId, idKey: createId,
username: await encrypt(registerUsername.value, sharedpvkey), username: await encrypt(registerUsername.value, sharedpvkey),
password: await encrypt(await hashSHA3_512(registerPassword.value), sharedpvkey) password: await encrypt(await hashSHA3_512(registerPassword.value), sharedpvkey)
@ -428,6 +437,12 @@
if (res.startsWith("success")) { if (res.startsWith("success")) {
showBlahNotification(res); showBlahNotification(res);
await delay(1000); await delay(1000);
let resolvedId = await fetchAsync(`${url}/nametoid?u=${registerUsername.value}`);
if (resolvedId && !resolvedId.startsWith("error:")) {
localStorage.setItem("id", resolvedId.split(":")[0]);
}
localStorage.setItem("username", registerUsername.value); localStorage.setItem("username", registerUsername.value);
localStorage.setItem("password", registerPassword.value); localStorage.setItem("password", registerPassword.value);
localStorage.setItem("host", registerHost.value); localStorage.setItem("host", registerHost.value);

File diff suppressed because it is too large Load diff

View file

@ -73,30 +73,73 @@ var joinSpaceScreen = `
</div> </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 = ` var invitesScreen = `
<div style="display: flex; flex-direction: column; height: 100%;"> <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;"> <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-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> <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>
<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;"> <div id="invites-container" class="list-container empty">
<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> ${invitesLoadingState}
<br>
<p><blah>desc.no.invites</blah></p>
</div> </div>
</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 = ` var notifisScreen = `
<div style="display: flex; flex-direction: column; height: 100%;"> <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;"> <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-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> <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>
<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;"> <div id="notifis-container" class="list-container empty">
<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> ${notifisEmptyState}
<br> </div>
<p><blah>desc.no.notifications</blah></p> </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>
`;
var chatScreen = `
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
<div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
</div>
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
<textarea id="chat-input" class="forminput" style="flex-grow: 1; margin: 0; resize: none; min-height: 2.5rem; max-height: 8rem; padding: 0.5rem;" placeholder="{blah(placeholder.message.input)}" onkeydown="handleChatInputKey(event)" oninput="handleChatInputResize(this)"></textarea>
<button class="submit-button" onclick="sendMessage()" style="margin: 0; padding: 0; aspect-ratio: 1;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.9 -0.5 15.6 16" fill="none" width="1.5rem">
<path stroke="var(--main-bg-color)" stroke-linecap="round" stroke-linejoin="round" d="m3.75 7.5 -1.875 5.625 11.25 -5.625L1.875 1.875l1.875 5.625zm0 0h3.75" stroke-width="1.1"></path>
</svg>
</button>
</div> </div>
</div> </div>
`; `;
@ -119,6 +162,11 @@ var homeRoomBar = `
</svg> </svg>
</button> </button>
</div> </div>
<div id="dms-wrapper" class="collapsible-wrapper">
<div class="collapsible-inner">
<div id="dms-list" class="list-container no-flex-grow empty" style="padding: 0 0.5rem;"></div>
</div>
</div>
<div class="sidebar-section-header"> <div class="sidebar-section-header">
<button class="collapse-text-button" id="collapse-groups" onclick="clickCollapseGroups()"> <button class="collapse-text-button" id="collapse-groups" onclick="clickCollapseGroups()">
@ -136,6 +184,11 @@ var homeRoomBar = `
</svg> </svg>
</button> </button>
</div> </div>
<div id="groups-wrapper" class="collapsible-wrapper">
<div class="collapsible-inner">
<div id="groups-list" class="list-container no-flex-grow empty" style="padding: 0 0.5rem;"></div>
</div>
</div>
`; `;
var inboxRoomBar = ` var inboxRoomBar = `

View file

@ -14,9 +14,9 @@
--button-height: 2.4rem; --button-height: 2.4rem;
--border-width: 0.09rem; --border-width: 0.09rem;
--big-default: rgb(207 207 207); --big-default: rgb(207, 207, 207);
--big-red: rgb(202 0 0); --big-red: hsl(0, 60%, 55%);
--big-green: rgb(25 189 0); --big-green: hsl(120, 60%, 55%);
} }
*:focus { *:focus {
outline: none; outline: none;
@ -71,7 +71,7 @@ loading {
user-select: none !important; user-select: none !important;
-webkit-user-drag: none !important; -webkit-user-drag: none !important;
} }
button, input { button, input, textarea {
border-radius: 0.8rem; border-radius: 0.8rem;
background-color: rgba(255, 255, 255, 0.05); background-color: rgba(255, 255, 255, 0.05);
margin: var(--button-margin); margin: var(--button-margin);
@ -92,7 +92,7 @@ button, input {
touch-action: manipulation; touch-action: manipulation;
-webkit-touch-callout: none; -webkit-touch-callout: none;
} }
input { input, textarea {
padding-left: calc(var(--button-margin) * 2); padding-left: calc(var(--button-margin) * 2);
-webkit-touch-callout: default; -webkit-touch-callout: default;
user-select: text !important; user-select: text !important;
@ -148,6 +148,7 @@ indicator.active {
width: calc(var(--icon-button-height) - (var(--border-width) * 2)); width: calc(var(--icon-button-height) - (var(--border-width) * 2));
height: calc(var(--icon-button-height) - (var(--border-width) * 2)); height: calc(var(--icon-button-height) - (var(--border-width) * 2));
pointer-events: none !important; pointer-events: none !important;
border-radius: 0.65rem;
} }
roomcontent { roomcontent {
@ -252,11 +253,11 @@ hr {
box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.4); box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.4);
opacity: 0; opacity: 0;
transform: translateY(-1rem); transform: translateY(-2rem);
transition: transition:
opacity 0.3s ease, opacity 0.2s ease,
transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275),
background-color 0.2s ease; background-color 0.15s ease;
} }
.notification.show { .notification.show {
opacity: 1; opacity: 1;
@ -330,52 +331,7 @@ herotitle {
.bottom-element { .bottom-element {
margin-top: auto; 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 { blah, inherit, .inherit {
all: inherit; all: inherit;
padding: 0; padding: 0;
@ -419,9 +375,7 @@ space{
gap: 0.5rem; gap: 0.5rem;
font-weight: 600; font-weight: 600;
} }
.context-menu button:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.mobile-nav-btn { .mobile-nav-btn {
display: none; display: none;
background: transparent; background: transparent;
@ -466,7 +420,7 @@ space{
sidebar, sidebar.second, roomcontent { sidebar, sidebar.second, roomcontent {
position: absolute !important; position: absolute !important;
height: 100%; 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 { sidebar {
@ -537,7 +491,7 @@ space{
opacity: 0; opacity: 0;
pointer-events: none; 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 { main.mobile-details roomcontent::after {
@ -554,3 +508,217 @@ space{
background-color: transparent; background-color: transparent;
color: var(--text-color); 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;
}
.list-container.no-flex-grow {
flex-grow: 0;
}
.collapsible-wrapper {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows 0.25s cubic-bezier(0.25, 1, 0.5, 1);
}
.collapsible-wrapper.collapsed {
grid-template-rows: 0fr;
}
.collapsible-inner {
overflow: hidden;
}
.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;
border: var(--border-width) solid rgba(255, 255, 255, 0.2);
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;
border-color: rgba(255, 255, 255, 0.2);
width: 3.1rem;
height: 3.1rem;
}
.room-entry {
display: flex;
align-items: center;
gap: 0.1rem;
padding: 1.5rem 0.6rem;
width: 100%;
border: none;
background: transparent;
border-radius: 0.8rem;
flex-shrink: 0;
cursor: pointer;
margin: 0;
}
.room-pfp {
width: 2.2rem;
height: 2.2rem;
border-radius: 0.65rem;
border: var(--border-width) solid rgba(255, 255, 255, 0.2);
object-fit: cover;
pointer-events: none;
}
.room-name {
font-weight: 600;
font-size: 1.05rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
}
.chat-message {
display: flex;
gap: 1rem;
padding: 0.2rem 0;
}
.chat-message.with-avatar {
margin-top: 0.8rem;
}
.chat-message-pfp {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.65rem;
object-fit: cover;
flex-shrink: 0;
border: var(--border-width) solid rgba(255, 255, 255, 0.2);
}
.chat-message-content {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.chat-message-header {
display: flex;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.2rem;
display: none; /* hidden for consecutive */
}
.chat-message.with-avatar .chat-message-header {
display: flex;
}
.chat-message-author {
font-weight: bold;
font-size: 1.05rem;
}
.chat-message-timestamp {
font-size: 0.75rem;
opacity: 0.5;
}
.chat-message-text {
line-height: 1.4;
word-break: break-word;
white-space: pre-wrap;
}
@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, textarea: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);
}
.invite-actions button:hover {
filter: brightness(0.93);
}
}
button:active, button.is-pressed, input:active, input.is-pressed, textarea:active, textarea.is-pressed {
background-color: rgba(255, 255, 255, 0.1);
transform: var(--press-scale);
}
.invite-actions button:active {
filter: brightness(0.85);
}
button.active, input.active, textarea.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));
}

View file

@ -1,8 +0,0 @@
[Desktop Entry]
Name=Miarven
Exec=bash -c 'DIR="$(dirname "%k")"; mkdir -p ~/.local/share/icons; cp "$DIR/icon.png" ~/.local/share/icons/olcxjamiarven.png; cd "$DIR" && ./miarven'
Icon=olcxjamiarven
Type=Application
Categories=Internet
StartupNotify=true
StartupWMClass=olcxja.miarven

40
assets/install.sh Normal file
View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -e
echo "WARNING: Make sure you don't run this script in a folder that contains anything more than Miarven"
echo "otherwise it may result in data loss"
read -r -p "Press Enter to continue (or Ctrl+C, to cancel)..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BIN_DIR="$HOME/.local/bin/olcxja.miarven"
APP_DIR="$HOME/.local/share/applications"
echo "1. Copying files..."
mkdir -p "$BIN_DIR"
mkdir -p "$APP_DIR"
cp -r "$SCRIPT_DIR/"* "$BIN_DIR/"
echo "2. Creating shortcut..."
mv "$BIN_DIR/olcxja.miarven.desktop" "$APP_DIR/olcxja.miarven.desktop"
echo "3. Adding executable permissions..."
chmod +x "$APP_DIR/olcxja.miarven.desktop"
chmod +x "$BIN_DIR/miarven"
echo "4. Cleaning installation..."
rm -f "$BIN_DIR/olcxja.miarven.desktop"
rm -f "$BIN_DIR/install.sh"
echo "5. Installation completed. Cleaning up and launching..."
rm -rf "$SCRIPT_DIR"/*
"$BIN_DIR/miarven" &
exit 0

View file

@ -0,0 +1,8 @@
[Desktop Entry]
Name=Miarven
Exec=~/.local/bin/olcxja.miarven/miarven
Icon=olcxja.miarven
Type=Application
Categories=Internet
StartupNotify=true
StartupWMClass=olcxja.miarven

View file

@ -2,4 +2,6 @@ mkdir build
cd build cd build
npx electron-packager ../electron miarven --platform=linux --arch=x64 --icon=../electron/assets/icon.png --overwrite --asar npx electron-packager ../electron miarven --platform=linux --arch=x64 --icon=../electron/assets/icon.png --overwrite --asar
cp ../icons/icon.png ./miarven-linux-x64/icon.png cp ../icons/icon.png ./miarven-linux-x64/icon.png
cp ../assets/Miarven.desktop ./miarven-linux-x64/Miarven.desktop cp ../assets/olcxja.miarven.desktop ./miarven-linux-x64/olcxja.miarven.desktop
cp ../assets/install.sh ./miarven-linux-x64/install.sh
chmod +x ./miarven-linux-x64/install.sh

View file

@ -1,5 +1,6 @@
{ {
"user.not.found": "meowser not found :c", "user.not.found": "meowser not found :c",
"dm.already.exists": "direct meowchat already exists :3",
"cant.invite.urself": "you can't invite yourself, silly :3", "cant.invite.urself": "you can't invite yourself, silly :3",
"user.already.invited": "you have already invited this user meow", "user.already.invited": "you have already invited this user meow",
"user.invited": "user invited successfully :3", "user.invited": "user invited successfully :3",
@ -9,6 +10,13 @@
"dm.accepted": "direct meowchat accepted", "dm.accepted": "direct meowchat accepted",
"no.invite.found": "you can't create a dm without invitation *hiss*", "no.invite.found": "you can't create a dm without invitation *hiss*",
"unknown.error": "unknown error :o", "unknown.error": "unknown error :o",
"forbidden": "you do not have pawmission to perform this action *hiss*",
"dm.key.too.large": "meowchat key too large :o",
"dm.key.not.found": "meowchat key not found :c",
"dm.key.updated": "meowchat key updated successfully :3",
"body.too.large": "request body too large ><",
"invite.revoked": "invite revoked successfully",
"invite.declined": "invite declined successfully",
"account.creation.request.expired": "cat creation request expired. try again", "account.creation.request.expired": "cat creation request expired. try again",
"invalid.username": "invalid cat name: {0}", "invalid.username": "invalid cat name: {0}",
"invalid.password": "invalid meow word: {0}", "invalid.password": "invalid meow word: {0}",
@ -73,6 +81,24 @@
"title.all": "all", "title.all": "all",
"title.unread": "unread", "title.unread": "unread",
"desc.no.invites": "no invites found :c",
"desc.no.notifications": "no notifications found :c",
"desc.fetching.invites": "fetching invites...",
"desc.fetching.notifications": "fetching notifications...",
"desc.invite.dm.received": "{0} ({1}) invited you to meowchat",
"desc.invite.dm.sent": "you invited {0} ({1}) to meowchat",
"desc.invite.group.received": "{0} ({1}) invited you to a clowder",
"desc.invite.group.sent": "you invited {0} ({1}) to a clowder",
"action.fetching.invites.sent": "fetching sent invites...",
"action.fetching.invites.recv": "fetching received invites...",
"action.dm.fetch": "fetching direct meowchats...",
"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.up": "sign up",
"title.sign.in": "sign in", "title.sign.in": "sign in",
"title.sign.in.to": "sign in to your larpix instance", "title.sign.in.to": "sign in to your larpix instance",
@ -93,6 +119,14 @@
"title.invitation.code": "invitation code", "title.invitation.code": "invitation code",
"title.back.to.register": "back to registration", "title.back.to.register": "back to registration",
"placeholder.username": "cat name", "placeholder.username": "cat name",
"placeholder.invitation.code": "invitation code", "placeholder.captcha.code": "captcha code",
"placeholder.captcha.code": "captcha code" "placeholder.message.input": "meow...",
"desc.messages.loading": "loading meows...",
"desc.no.dms": "no direct meowchats :c",
"action.dm.opening": "opening meowchat...",
"dm.open.failed": "failed to open meowchat :c",
"keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on",
"keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c",
"dm.messages.fetch.failed": "failed to fetch meows :c",
"messages.decrypt.failed": "failed to decrypt meow :c"
} }

View file

@ -1,5 +1,6 @@
{ {
"user.not.found": "User not found", "user.not.found": "User not found",
"dm.already.exists": "Direct message already exists",
"cant.invite.urself": "You can't invite yourself", "cant.invite.urself": "You can't invite yourself",
"user.already.invited": "You have already invited this user", "user.already.invited": "You have already invited this user",
"user.invited": "User invited successfully", "user.invited": "User invited successfully",
@ -9,6 +10,13 @@
"dm.accepted": "DM accepted", "dm.accepted": "DM accepted",
"no.invite.found": "You can't create a dm without invitation", "no.invite.found": "You can't create a dm without invitation",
"unknown.error": "Unknown error", "unknown.error": "Unknown error",
"forbidden": "You do not have permission to perform this action",
"dm.key.too.large": "DM key too large",
"dm.key.not.found": "DM key not found",
"dm.key.updated": "DM key updated successfully",
"body.too.large": "Request body too large",
"invite.revoked": "Invite revoked successfully",
"invite.declined": "Invite declined successfully",
"account.creation.request.expired": "Account creation request expired. Try again", "account.creation.request.expired": "Account creation request expired. Try again",
"invalid.username": "Invalid username: {0}", "invalid.username": "Invalid username: {0}",
"invalid.password": "Invalid password: {0}", "invalid.password": "Invalid password: {0}",
@ -72,6 +80,23 @@
"title.sent": "Sent", "title.sent": "Sent",
"title.all": "All", "title.all": "All",
"title.unread": "Unread", "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.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.up": "Sign Up",
"title.sign.in": "Sign In", "title.sign.in": "Sign In",
@ -93,6 +118,14 @@
"title.invitation.code": "Invitation code", "title.invitation.code": "Invitation code",
"title.back.to.register": "Back to registration", "title.back.to.register": "Back to registration",
"placeholder.username": "username", "placeholder.username": "username",
"placeholder.invitation.code": "invitation code", "placeholder.captcha.code": "captcha code",
"placeholder.captcha.code": "captcha code" "placeholder.message.input": "Message...",
"desc.messages.loading": "Fetching messages...",
"desc.no.dms": "No direct messages",
"action.dm.opening": "Opening DM...",
"dm.open.failed": "Failed to open DM",
"keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.",
"keys.server.decrypt.failed": "Could not decrypt your account keys with this password.",
"dm.messages.fetch.failed": "Failed to fetch messages",
"messages.decrypt.failed": "Failed to decrypt message"
} }

14
webroot/crypto-pq.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -6,6 +6,7 @@
<title>Miarven</title> <title>Miarven</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="icon" type="image/svg+xml" href="favicon.svg"> <link rel="icon" type="image/svg+xml" href="favicon.svg">
<script src="crypto-pq.js"></script>
</head> </head>
<body> <body>
<loading> <loading>
@ -85,7 +86,6 @@
async function start() { async function start() {
updateLoadingStatus("loading.loading"); updateLoadingStatus("loading.loading");
try { try {
gotoHome();
if (host != null) if (host != null)
{ {
await updateProtocolAndUrl(host); await updateProtocolAndUrl(host);
@ -94,13 +94,21 @@
{ {
await updateProtocolAndUrl(window.location.hostname); await updateProtocolAndUrl(window.location.hostname);
} }
sidebarPfp.src = await getAvatarUrl(username); let localIdRes = await fetchAsync(`${url}/nametoid?u=${username}`);
let localId = localIdRes.trim();
sidebarPfp.src = await getAvatarUrl(localId, `${username}:${host}`);
showAction("Authenticating...", "startauth"); showAction("action.auth", "startauth");
let res = await Auth(username, password); let res = await Auth(username, password);
clearAction("startauth"); clearAction("startauth");
if (res.startsWith("success:")) { if (res.startsWith("success:")) {
await refreshDms(); try {
await ensureUserKeys();
} catch (e) {
//if fails continue loading encryption may be broken
console.error(e);
}
gotoHome();
} else { } else {
showBlahNotification("error:auth.failed.redirect.to.login"); showBlahNotification("error:auth.failed.redirect.to.login");
await delay(2000); await delay(2000);
@ -116,13 +124,18 @@
} }
} }
async function refreshDms() { async function refreshDms(waittime = 0) {
try { try {
showAction("action.dm.fetch", "dmrefresh");
await delay(waittime);
showAction("Refreshing dms...", "dmrefresh"); if (window.cachedDms && typeof renderDms === 'function') {
let res = await fetchEncrypted("user/dm/list", ""); await renderDms(window.cachedDms);
console.log(res); }
let res = await fetchEncrypted("user/dm/list");
window.cachedDms = res;
if (typeof renderDms === 'function') {
await renderDms(res);
}
clearAction("dmrefresh"); clearAction("dmrefresh");
} }
catch (e) { catch (e) {
@ -133,9 +146,17 @@
async function addDm() { async function addDm() {
try { try {
showAction("Adding...", "dmadd"); showAction("action.dm.adding", "dmadd");
let res = await fetchEncrypted("user/dm/invite", document.getElementById("addchat-username").value); let username = document.getElementById("addchat-username").value;
console.log(res); let idRes = await fetchAsync(`${url}/nametoid?u=${username}`);
if (idRes.startsWith("error") || idRes.trim() === "0" || idRes.trim() === "") {
clearAction("dmadd");
showBlahNotification("error:user.not.found");
return;
}
let targetId = idRes.trim();
let res = await fetchEncrypted("user/dm/invite", targetId);
clearAction("dmadd"); clearAction("dmadd");
showBlahNotification(res); showBlahNotification(res);
} }

View file

@ -6,6 +6,7 @@
<title>Miarven - Login</title> <title>Miarven - Login</title>
<link rel="stylesheet" href="../style.css"> <link rel="stylesheet" href="../style.css">
<link rel="icon" type="image/svg+xml" href="../favicon.svg"> <link rel="icon" type="image/svg+xml" href="../favicon.svg">
<script src="../crypto-pq.js"></script>
<style> <style>
html { html {
font-size: max(17px, calc(100vw / 100)); font-size: max(17px, calc(100vw / 100));
@ -321,16 +322,20 @@
formLogin.addEventListener('submit', async (e) => { formLogin.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
await updateProtocolAndUrl(loginHost.value);
let res = await Auth(loginUsername.value, loginPassword.value); let res = await Auth(loginUsername.value, loginPassword.value);
if (res.startsWith("success:")) if (res.startsWith("success:"))
{ {
showBlahNotification(res); let parts = res.split("|");
showBlahNotification(parts[0]);
await delay(800); await delay(800);
localStorage.setItem("username", loginUsername.value); localStorage.setItem("username", loginUsername.value);
localStorage.setItem("password", loginPassword.value); localStorage.setItem("password", loginPassword.value);
localStorage.setItem("host", loginHost.value); localStorage.setItem("host", loginHost.value);
if (parts.length > 1) {
localStorage.setItem("id", parts[1]);
}
await loadingFadeIn(); await loadingFadeIn();
@ -371,14 +376,18 @@
container.className = 'auth-container show-register'; container.className = 'auth-container show-register';
return; return;
} }
await updateProtocolAndUrl(registerHost.value);
let dataarray = keyDataFromServerJson(await fetchAsync(`${url}/createaccount?step=init`)); let dataarray = keyDataFromServerJson(await fetchAsync(`${url}/createaccount?step=init`));
var sharedkey = await calcCommunicationKeyClient(dataarray[0], dataarray[1], dataarray[2]); var sharedkey = await calcHybridSharedKeyClient(dataarray[0], dataarray[1]);
sharedpvkey = sharedkey[1]; sharedpvkey = sharedkey[2];
createId = dataarray[3]; createId = dataarray[2];
const captchaimage = await fetch(`${url}/createaccount?step=register`, { const captchaimage = await fetch(`${url}/createaccount?step=register`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
pubClient: sharedkey[0], pubX25519: sharedkey[0],
ciphertextMlKem: sharedkey[1],
idKey: createId, idKey: createId,
username: await encrypt(registerUsername.value, sharedpvkey), username: await encrypt(registerUsername.value, sharedpvkey),
password: await encrypt(await hashSHA3_512(registerPassword.value), sharedpvkey) password: await encrypt(await hashSHA3_512(registerPassword.value), sharedpvkey)
@ -428,6 +437,12 @@
if (res.startsWith("success")) { if (res.startsWith("success")) {
showBlahNotification(res); showBlahNotification(res);
await delay(1000); await delay(1000);
let resolvedId = await fetchAsync(`${url}/nametoid?u=${registerUsername.value}`);
if (resolvedId && !resolvedId.startsWith("error:")) {
localStorage.setItem("id", resolvedId.split(":")[0]);
}
localStorage.setItem("username", registerUsername.value); localStorage.setItem("username", registerUsername.value);
localStorage.setItem("password", registerPassword.value); localStorage.setItem("password", registerPassword.value);
localStorage.setItem("host", registerHost.value); localStorage.setItem("host", registerHost.value);

File diff suppressed because it is too large Load diff

View file

@ -73,30 +73,73 @@ var joinSpaceScreen = `
</div> </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 = ` var invitesScreen = `
<div style="display: flex; flex-direction: column; height: 100%;"> <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;"> <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-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> <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>
<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;"> <div id="invites-container" class="list-container empty">
<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> ${invitesLoadingState}
<br>
<p><blah>desc.no.invites</blah></p>
</div> </div>
</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 = ` var notifisScreen = `
<div style="display: flex; flex-direction: column; height: 100%;"> <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;"> <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-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> <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>
<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;"> <div id="notifis-container" class="list-container empty">
<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> ${notifisEmptyState}
<br> </div>
<p><blah>desc.no.notifications</blah></p> </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>
`;
var chatScreen = `
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
<div id="chat-messages" style="flex-grow: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
</div>
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
<textarea id="chat-input" class="forminput" style="flex-grow: 1; margin: 0; resize: none; min-height: 2.5rem; max-height: 8rem; padding: 0.5rem;" placeholder="{blah(placeholder.message.input)}" onkeydown="handleChatInputKey(event)" oninput="handleChatInputResize(this)"></textarea>
<button class="submit-button" onclick="sendMessage()" style="margin: 0; padding: 0; aspect-ratio: 1;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.9 -0.5 15.6 16" fill="none" width="1.5rem">
<path stroke="var(--main-bg-color)" stroke-linecap="round" stroke-linejoin="round" d="m3.75 7.5 -1.875 5.625 11.25 -5.625L1.875 1.875l1.875 5.625zm0 0h3.75" stroke-width="1.1"></path>
</svg>
</button>
</div> </div>
</div> </div>
`; `;
@ -119,6 +162,11 @@ var homeRoomBar = `
</svg> </svg>
</button> </button>
</div> </div>
<div id="dms-wrapper" class="collapsible-wrapper">
<div class="collapsible-inner">
<div id="dms-list" class="list-container no-flex-grow empty" style="padding: 0 0.5rem;"></div>
</div>
</div>
<div class="sidebar-section-header"> <div class="sidebar-section-header">
<button class="collapse-text-button" id="collapse-groups" onclick="clickCollapseGroups()"> <button class="collapse-text-button" id="collapse-groups" onclick="clickCollapseGroups()">
@ -136,6 +184,11 @@ var homeRoomBar = `
</svg> </svg>
</button> </button>
</div> </div>
<div id="groups-wrapper" class="collapsible-wrapper">
<div class="collapsible-inner">
<div id="groups-list" class="list-container no-flex-grow empty" style="padding: 0 0.5rem;"></div>
</div>
</div>
`; `;
var inboxRoomBar = ` var inboxRoomBar = `

View file

@ -14,9 +14,9 @@
--button-height: 2.4rem; --button-height: 2.4rem;
--border-width: 0.09rem; --border-width: 0.09rem;
--big-default: rgb(207 207 207); --big-default: rgb(207, 207, 207);
--big-red: rgb(202 0 0); --big-red: hsl(0, 60%, 55%);
--big-green: rgb(25 189 0); --big-green: hsl(120, 60%, 55%);
} }
*:focus { *:focus {
outline: none; outline: none;
@ -71,7 +71,7 @@ loading {
user-select: none !important; user-select: none !important;
-webkit-user-drag: none !important; -webkit-user-drag: none !important;
} }
button, input { button, input, textarea {
border-radius: 0.8rem; border-radius: 0.8rem;
background-color: rgba(255, 255, 255, 0.05); background-color: rgba(255, 255, 255, 0.05);
margin: var(--button-margin); margin: var(--button-margin);
@ -92,7 +92,7 @@ button, input {
touch-action: manipulation; touch-action: manipulation;
-webkit-touch-callout: none; -webkit-touch-callout: none;
} }
input { input, textarea {
padding-left: calc(var(--button-margin) * 2); padding-left: calc(var(--button-margin) * 2);
-webkit-touch-callout: default; -webkit-touch-callout: default;
user-select: text !important; user-select: text !important;
@ -148,6 +148,7 @@ indicator.active {
width: calc(var(--icon-button-height) - (var(--border-width) * 2)); width: calc(var(--icon-button-height) - (var(--border-width) * 2));
height: calc(var(--icon-button-height) - (var(--border-width) * 2)); height: calc(var(--icon-button-height) - (var(--border-width) * 2));
pointer-events: none !important; pointer-events: none !important;
border-radius: 0.65rem;
} }
roomcontent { roomcontent {
@ -252,11 +253,11 @@ hr {
box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.4); box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.4);
opacity: 0; opacity: 0;
transform: translateY(-1rem); transform: translateY(-2rem);
transition: transition:
opacity 0.3s ease, opacity 0.2s ease,
transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275),
background-color 0.2s ease; background-color 0.15s ease;
} }
.notification.show { .notification.show {
opacity: 1; opacity: 1;
@ -330,52 +331,7 @@ herotitle {
.bottom-element { .bottom-element {
margin-top: auto; 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 { blah, inherit, .inherit {
all: inherit; all: inherit;
padding: 0; padding: 0;
@ -419,9 +375,7 @@ space{
gap: 0.5rem; gap: 0.5rem;
font-weight: 600; font-weight: 600;
} }
.context-menu button:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.mobile-nav-btn { .mobile-nav-btn {
display: none; display: none;
background: transparent; background: transparent;
@ -466,7 +420,7 @@ space{
sidebar, sidebar.second, roomcontent { sidebar, sidebar.second, roomcontent {
position: absolute !important; position: absolute !important;
height: 100%; 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 { sidebar {
@ -537,7 +491,7 @@ space{
opacity: 0; opacity: 0;
pointer-events: none; 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 { main.mobile-details roomcontent::after {
@ -554,3 +508,217 @@ space{
background-color: transparent; background-color: transparent;
color: var(--text-color); 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;
}
.list-container.no-flex-grow {
flex-grow: 0;
}
.collapsible-wrapper {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows 0.25s cubic-bezier(0.25, 1, 0.5, 1);
}
.collapsible-wrapper.collapsed {
grid-template-rows: 0fr;
}
.collapsible-inner {
overflow: hidden;
}
.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;
border: var(--border-width) solid rgba(255, 255, 255, 0.2);
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;
border-color: rgba(255, 255, 255, 0.2);
width: 3.1rem;
height: 3.1rem;
}
.room-entry {
display: flex;
align-items: center;
gap: 0.1rem;
padding: 1.5rem 0.6rem;
width: 100%;
border: none;
background: transparent;
border-radius: 0.8rem;
flex-shrink: 0;
cursor: pointer;
margin: 0;
}
.room-pfp {
width: 2.2rem;
height: 2.2rem;
border-radius: 0.65rem;
border: var(--border-width) solid rgba(255, 255, 255, 0.2);
object-fit: cover;
pointer-events: none;
}
.room-name {
font-weight: 600;
font-size: 1.05rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
}
.chat-message {
display: flex;
gap: 1rem;
padding: 0.2rem 0;
}
.chat-message.with-avatar {
margin-top: 0.8rem;
}
.chat-message-pfp {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.65rem;
object-fit: cover;
flex-shrink: 0;
border: var(--border-width) solid rgba(255, 255, 255, 0.2);
}
.chat-message-content {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.chat-message-header {
display: flex;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.2rem;
display: none; /* hidden for consecutive */
}
.chat-message.with-avatar .chat-message-header {
display: flex;
}
.chat-message-author {
font-weight: bold;
font-size: 1.05rem;
}
.chat-message-timestamp {
font-size: 0.75rem;
opacity: 0.5;
}
.chat-message-text {
line-height: 1.4;
word-break: break-word;
white-space: pre-wrap;
}
@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, textarea: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);
}
.invite-actions button:hover {
filter: brightness(0.93);
}
}
button:active, button.is-pressed, input:active, input.is-pressed, textarea:active, textarea.is-pressed {
background-color: rgba(255, 255, 255, 0.1);
transform: var(--press-scale);
}
.invite-actions button:active {
filter: brightness(0.85);
}
button.active, input.active, textarea.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));
}