add DM screen
All checks were successful
Android Build / publish (push) Successful in 48s
Linux Build / publish (push) Successful in 51s

This commit is contained in:
olcxja 2026-05-29 08:54:18 +02:00
commit be26703908
8 changed files with 386 additions and 8 deletions

View file

@ -127,12 +127,12 @@
async function refreshDms() {
try {
showAction("action.dm.fetch", "dmrefresh");
let res = await fetchEncrypted("user/dm/list");
clearAction("dmrefresh");
if (typeof renderDms === 'function') {
await renderDms(res);
}
}
catch (e) {
clearAction("dmrefresh");

View file

@ -1234,8 +1234,11 @@ async function switchDetailsContent(title, content)
function clickCollapseDms()
{
var collapseDmsBtn = document.getElementById("collapse-dms");
collapseDmsBtn.classList.toggle("collapsed");
var dmsList = document.getElementById("dms-list");
if (dmsList) {
dmsList.style.display = collapseDmsBtn.classList.contains("collapsed") ? "none" : "";
}
}
function clickCollapseGroups()
{
@ -1471,6 +1474,7 @@ async function acceptInvite(targetId) { //TODO: Implement key generation
clearAction("invite.action");
showBlahNotification(res || "success:invite.accepted");
await loadInvites('received');
await refreshDms();
} catch (e) {
clearAction("invite.action");
showBlahNotification("error:something.wrong");
@ -1679,4 +1683,98 @@ function handleMobileSwipe() {
}
}
}
}
async function renderDms(res) {
let container = document.getElementById("dms-list");
if (!container) return;
if (res && res !== "") {
let parsed = JSON.parse(res);
let dms = parsed.dms || {};
let dmEntries = Object.entries(dms);
// Sort by timestamp descending
dmEntries.sort((a,b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0));
let html = "";
let count = 0;
for (let [dmId, dmData] of dmEntries) {
let parts = dmId.split('_');
let myId = `${id};${host}`;
let targetId = parts[0] === myId ? parts[1] : parts[0];
let targetUsername = await fetchAsync(`${url}/idtoname?id=${targetId}`);
if (!targetUsername || targetUsername.trim() === "" || targetUsername.startsWith("error:")) continue;
let pfp = await getAvatarUrl(targetId, targetUsername);
let displayName = targetUsername.split(':')[0];
html += `
<button class="room-entry" id="dm-btn-${dmId}" onclick="openDm('${dmId}', '${targetUsername}', '${targetId}')">
<img src="${pfp}" class="room-pfp">
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%;">
<span class="room-name">${displayName}</span>
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${targetUsername}</span>
</div>
</button>
`;
count++;
}
if (count > 0) {
container.innerHTML = html;
container.classList.remove("empty");
} else {
container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.dms</blah></p>`;
container.classList.add("empty");
let blahTags = container.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
}
} else {
container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.dms</blah></p>`;
container.classList.add("empty");
let blahTags = container.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
}
}
var currentDmId = null;
var currentDmKey = null; // AES key for encrypting/decrypting messages
async function openDm(dmId, username, targetId) {
try {
showAction("action.dm.opening", "dmopen");
currentDmKey = await ensureDmRoomKey(dmId);
if (!currentDmKey) {
throw new Error("Missing DM room key");
}
currentDmId = dmId;
let pfp = await getAvatarUrl(targetId, username);
let iconHtml = `<img src="${pfp}" style="width: 1.8rem; height: 1.8rem; border-radius: 0.4rem; margin-right: 0.5rem; object-fit: cover;">`;
await switchRoomContent(username.split(':')[0], chatScreen, false, iconHtml);
let msgContainer = document.getElementById("chat-messages");
if (msgContainer) {
msgContainer.innerHTML = `<div style="text-align: center; opacity: 0.5; padding: 1rem;"><blah>desc.messages.encryption.active</blah></div>`;
let blahTags = msgContainer.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
}
setActiveRoombarItem(`dm-btn-${dmId}`);
clearAction("dmopen");
} catch (e) {
clearAction("dmopen");
console.error(e);
showBlahNotification("error:dm.open.failed");
}
}

View file

@ -129,6 +129,20 @@ var invitesEntry = `
</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;">
<!-- messages go here -->
</div>
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
<input type="text" id="chat-input" class="forminput" style="flex-grow: 1; margin: 0;" placeholder="{blah(placeholder.message.input)}">
<button class="submit-button" onclick="sendMessage()" style="margin: 0; padding: 0 1.5rem;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--main-bg-color)" width="1.5rem"><path d="M120-160v-240l320-80-320-80v-240l760 320-760 320Z"/></svg>
</button>
</div>
</div>
`;
//roombars
var homeRoomBar = `
<div class="sidebar-section-header">
@ -147,6 +161,7 @@ var homeRoomBar = `
</svg>
</button>
</div>
<div id="dms-list" class="list-container empty" style="padding: 0 0.5rem;"></div>
<div class="sidebar-section-header">
<button class="collapse-text-button" id="collapse-groups" onclick="clickCollapseGroups()">

View file

@ -571,6 +571,82 @@ space{
height: 3.1rem;
}
.room-entry {
display: flex;
align-items: center;
gap: 0.8rem;
padding: 0.5rem 0.8rem;
width: 100%;
border: none;
background: transparent;
border-radius: 0.8rem;
flex-shrink: 0;
cursor: pointer;
}
.room-pfp {
width: 2.2rem;
height: 2.2rem;
border-radius: 0.6rem;
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.6rem;
object-fit: cover;
flex-shrink: 0;
opacity: 0; /* hidden by default for consecutive messages */
}
.chat-message.with-avatar .chat-message-pfp {
opacity: 1;
}
.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;
}
@media (hover: hover) {
sidebarelement:hover indicator:not(.active) {
content: "";

View file

@ -127,12 +127,12 @@
async function refreshDms() {
try {
showAction("action.dm.fetch", "dmrefresh");
let res = await fetchEncrypted("user/dm/list");
clearAction("dmrefresh");
if (typeof renderDms === 'function') {
await renderDms(res);
}
}
catch (e) {
clearAction("dmrefresh");

View file

@ -1234,8 +1234,11 @@ async function switchDetailsContent(title, content)
function clickCollapseDms()
{
var collapseDmsBtn = document.getElementById("collapse-dms");
collapseDmsBtn.classList.toggle("collapsed");
var dmsList = document.getElementById("dms-list");
if (dmsList) {
dmsList.style.display = collapseDmsBtn.classList.contains("collapsed") ? "none" : "";
}
}
function clickCollapseGroups()
{
@ -1471,6 +1474,7 @@ async function acceptInvite(targetId) { //TODO: Implement key generation
clearAction("invite.action");
showBlahNotification(res || "success:invite.accepted");
await loadInvites('received');
await refreshDms();
} catch (e) {
clearAction("invite.action");
showBlahNotification("error:something.wrong");
@ -1679,4 +1683,98 @@ function handleMobileSwipe() {
}
}
}
}
async function renderDms(res) {
let container = document.getElementById("dms-list");
if (!container) return;
if (res && res !== "") {
let parsed = JSON.parse(res);
let dms = parsed.dms || {};
let dmEntries = Object.entries(dms);
// Sort by timestamp descending
dmEntries.sort((a,b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0));
let html = "";
let count = 0;
for (let [dmId, dmData] of dmEntries) {
let parts = dmId.split('_');
let myId = `${id};${host}`;
let targetId = parts[0] === myId ? parts[1] : parts[0];
let targetUsername = await fetchAsync(`${url}/idtoname?id=${targetId}`);
if (!targetUsername || targetUsername.trim() === "" || targetUsername.startsWith("error:")) continue;
let pfp = await getAvatarUrl(targetId, targetUsername);
let displayName = targetUsername.split(':')[0];
html += `
<button class="room-entry" id="dm-btn-${dmId}" onclick="openDm('${dmId}', '${targetUsername}', '${targetId}')">
<img src="${pfp}" class="room-pfp">
<div style="display: flex; flex-direction: column; text-align: left; overflow: hidden; width: 100%;">
<span class="room-name">${displayName}</span>
<span class="room-id" style="font-size: 0.8rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${targetUsername}</span>
</div>
</button>
`;
count++;
}
if (count > 0) {
container.innerHTML = html;
container.classList.remove("empty");
} else {
container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.dms</blah></p>`;
container.classList.add("empty");
let blahTags = container.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
}
} else {
container.innerHTML = `<p style="opacity: 0.5;"><blah>desc.no.dms</blah></p>`;
container.classList.add("empty");
let blahTags = container.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
}
}
var currentDmId = null;
var currentDmKey = null; // AES key for encrypting/decrypting messages
async function openDm(dmId, username, targetId) {
try {
showAction("action.dm.opening", "dmopen");
currentDmKey = await ensureDmRoomKey(dmId);
if (!currentDmKey) {
throw new Error("Missing DM room key");
}
currentDmId = dmId;
let pfp = await getAvatarUrl(targetId, username);
let iconHtml = `<img src="${pfp}" style="width: 1.8rem; height: 1.8rem; border-radius: 0.4rem; margin-right: 0.5rem; object-fit: cover;">`;
await switchRoomContent(username.split(':')[0], chatScreen, false, iconHtml);
let msgContainer = document.getElementById("chat-messages");
if (msgContainer) {
msgContainer.innerHTML = `<div style="text-align: center; opacity: 0.5; padding: 1rem;"><blah>desc.messages.encryption.active</blah></div>`;
let blahTags = msgContainer.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
}
setActiveRoombarItem(`dm-btn-${dmId}`);
clearAction("dmopen");
} catch (e) {
clearAction("dmopen");
console.error(e);
showBlahNotification("error:dm.open.failed");
}
}

View file

@ -129,6 +129,20 @@ var invitesEntry = `
</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;">
<!-- messages go here -->
</div>
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
<input type="text" id="chat-input" class="forminput" style="flex-grow: 1; margin: 0;" placeholder="{blah(placeholder.message.input)}">
<button class="submit-button" onclick="sendMessage()" style="margin: 0; padding: 0 1.5rem;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--main-bg-color)" width="1.5rem"><path d="M120-160v-240l320-80-320-80v-240l760 320-760 320Z"/></svg>
</button>
</div>
</div>
`;
//roombars
var homeRoomBar = `
<div class="sidebar-section-header">
@ -147,6 +161,7 @@ var homeRoomBar = `
</svg>
</button>
</div>
<div id="dms-list" class="list-container empty" style="padding: 0 0.5rem;"></div>
<div class="sidebar-section-header">
<button class="collapse-text-button" id="collapse-groups" onclick="clickCollapseGroups()">

View file

@ -571,6 +571,82 @@ space{
height: 3.1rem;
}
.room-entry {
display: flex;
align-items: center;
gap: 0.8rem;
padding: 0.5rem 0.8rem;
width: 100%;
border: none;
background: transparent;
border-radius: 0.8rem;
flex-shrink: 0;
cursor: pointer;
}
.room-pfp {
width: 2.2rem;
height: 2.2rem;
border-radius: 0.6rem;
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.6rem;
object-fit: cover;
flex-shrink: 0;
opacity: 0; /* hidden by default for consecutive messages */
}
.chat-message.with-avatar .chat-message-pfp {
opacity: 1;
}
.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;
}
@media (hover: hover) {
sidebarelement:hover indicator:not(.active) {
content: "";