diff --git a/README.md b/README.md
index 2bcef04..251edb8 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,2 @@
-# Chatenium On Web
-A modern web application for Chatenium compatible with a wide range of devices.
+# Chatenium Nexum
+The next generation Web application for Chatenium, built on modern standards powered by Chatenium SDK (TypeScript).
diff --git a/package-lock.json b/package-lock.json
index fe274ad..6b542bb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,7 @@
"@angular/platform-browser": "^21.2.0",
"@angular/router": "^21.2.0",
"@angular/service-worker": "^21.2.0",
- "@chatenium/chatenium-sdk": "^1.1.10",
+ "@chatenium/chatenium-sdk": "^1.1.11",
"@fortawesome/angular-fontawesome": "^4.0.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
@@ -1011,9 +1011,9 @@
}
},
"node_modules/@chatenium/chatenium-sdk": {
- "version": "1.1.10",
- "resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.1.10.tgz",
- "integrity": "sha512-FRVKyOzkKQ5wWFL/m3G731VXhvqo3IHpKFcseWfpX5TxBq6Kh9GxpjN8/JQgQ5X+KDES1Nrm4FqOHkVw5CVHyA==",
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.1.11.tgz",
+ "integrity": "sha512-iwYHyED1AnGcWtyeVo+R1JfxVauuIC5FCX8Rk6RwF+ls/oUIg1aExFgvif2wc0qUyT9mla8ztBt+wBB/cTy4hA==",
"dependencies": {
"@faker-js/faker": "^10.4.0",
"axios": "^1.14.0",
diff --git a/package.json b/package.json
index d6b4df0..c71fb6c 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"@angular/platform-browser": "^21.2.0",
"@angular/router": "^21.2.0",
"@angular/service-worker": "^21.2.0",
- "@chatenium/chatenium-sdk": "^1.1.10",
+ "@chatenium/chatenium-sdk": "^1.1.11",
"@fortawesome/angular-fontawesome": "^4.0.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
diff --git a/public/i18n/en.json b/public/i18n/en.json
index a7bab73..e33d5c0 100644
--- a/public/i18n/en.json
+++ b/public/i18n/en.json
@@ -1,5 +1,5 @@
{
- "version": "3.0 Beta 6 (April 14, 2026)",
+ "version": "Chatenium Nexum 3.0 Beta 7 (April 15, 2026)",
"ok": "Ok",
"back": "Back",
"aChatProgram": "A messaging platform that you can trust.",
@@ -10,13 +10,14 @@
"whatIsChtn": "Chatenium is a chat platform aiming to provide a secure, well integrated fast chatting experience across any devices. You can create a new account for free and start chatting and broadcasting messages.",
"updating": "Updating...",
"home": {
+ "soon": "Coming soon...",
"chtn": "Chatenium ",
"chtnIs": "Chatenium is ",
"help": "Help",
"blog": "Blog",
"apiSpecs": "API specifications",
- "adaptsToYou": "adapts to you",
- "adaptsToYouDesc": "We actively work on bringing Chatenium to every device, but not the fast way, the good way by developing a native application for each platform.",
+ "adaptsTo": "Chatenium adapts to ",
+ "adaptsToYouDesc": "Choose your platform:",
"chtnOnWeb": "Chatenium On Web",
"chtnOnWebDesc": "Built for maximum compatibility. The Web will work on every device you have.",
"chtnOnAndroid": "Chatenium On Androidâ„¢",
@@ -105,8 +106,9 @@
"changeLogDialog": {
"label": "Chatenium has been updated",
"changeLog": {
- "1": "UI issue fixes in the message box",
- "2": "Added caching to improve performance"
+ "1": "Added progress bar to files when uploading attachments",
+ "2": "Bug fixes related to sending messages",
+ "3": "Fixed scrolling issues when switching chats. Also now the scroll position is now saved"
}
},
"chatnav": {
diff --git a/src/app/chat/chat.html b/src/app/chat/chat.html
index a068b51..9406d90 100644
--- a/src/app/chat/chat.html
+++ b/src/app/chat/chat.html
@@ -4,6 +4,7 @@
{{ "chat.changeLogDialog.changeLog.1"|translate }}
{{ "chat.changeLogDialog.changeLog.2"|translate }}
+ {{ "chat.changeLogDialog.changeLog.3"|translate }}
-
+
}
diff --git a/src/app/chat/dm/dm.ts b/src/app/chat/dm/dm.ts
index 8837d28..e82dce4 100644
--- a/src/app/chat/dm/dm.ts
+++ b/src/app/chat/dm/dm.ts
@@ -14,6 +14,8 @@ import {MessageBoxViewModel} from '../elements/message-box/message-box-viewmodel
import {WebSocketHandler} from '@chatenium/chatenium-sdk/core/webSocketHandler';
import {FileData, FileUploadProgressListener} from '@chatenium/chatenium-sdk/domain/fileUploadService.schema';
import {Attachment} from '@chatenium/chatenium-sdk/domain/common.schema';
+import {MessagesViewModel} from '../elements/messages/messages-viewmodel';
+import {v4 as uuidv4} from 'uuid';
@Component({
selector: 'app-dm',
@@ -42,6 +44,7 @@ export class Dm implements OnInit {
async sendMessage(message: string, files: FileDataWithPreview[] | null) {
if (!files && message.trim() == "") return
+ this.scrollToBottom("smooth")
const session = this.serviceManager.currentSession();
if (session != null) {
@@ -66,8 +69,11 @@ export class Dm implements OnInit {
let attachments: Attachment[] = []
files?.forEach(file => {
- const extraMetaData: Record = {}
+ const extraMetaData: Record = {}
extraMetaData["thumbnailMetaData"] = file.videoThumbnail ?? ""
+ extraMetaData["progressShown"] = true
+ extraMetaData["totalChunks"] = 0
+ extraMetaData["uploadedChunks"] = 0
attachments.push({
fileName: file.name,
@@ -81,6 +87,7 @@ export class Dm implements OnInit {
})
})
+ const tempMsgId = uuidv4()
this.store.messages.update(value => [...value, {
message: message,
chatid: this.chatid,
@@ -88,7 +95,7 @@ export class Dm implements OnInit {
replyTo: "",
author: session.userData.userid,
seen: false,
- msgid: "",
+ msgid: tempMsgId,
forwardedFrom: "",
isEdited: false,
sent_at: {T: 0, I: 0},
@@ -98,14 +105,13 @@ export class Dm implements OnInit {
this.scrollToBottom("smooth")
- await this.store.service.sendMessage("", message, null, null, files, {
+ const respMessage = await this.store.service.sendMessage(tempMsgId, message, null, null, files, {
fileProgressUpdate: (tempMsgId, fileId, allChunks, chunksDone) => {
this.uploadProgressUpdate(tempMsgId, fileId, allChunks, chunksDone)
}
})
+ this.updateTempMessage(tempMsgId, respMessage)
}
-
- this.store.messageBox.message.set("")
}
async deleteMessage(messageId: string) {
@@ -125,15 +131,61 @@ export class Dm implements OnInit {
setTimeout(() => {
const scrollContainer = document.querySelector("#scrollContainer")
scrollContainer.scroll({
- top: scrollContainer.scrollHeight,
+ top: this.store.messagesVm.scrollBarStatus() == -1 ? scrollContainer.scrollHeight : this.store.messagesVm.scrollBarStatus(),
left: 0,
behavior: anim
});
+
+ if (this.store.messagesVm.scrollBarStatus() == -1) {
+ this.store.messagesVm.scrollBarStatus.set(scrollContainer.scrollHeight)
+ }
}, 0)
}
+ handleMessagesScroll(e: any) {
+ this.store.messagesVm.scrollBarStatus.set(e.target.scrollTop)
+ }
+
uploadProgressUpdate(tempMsgId: string, fileId: string, allChunks: number, chunksDone: number) {
- console.log(fileId, allChunks, chunksDone)
+ console.log(tempMsgId, fileId, allChunks, chunksDone)
+ this.store.messages.update(messages =>
+ messages.map(m => {
+ if (m.msgid !== tempMsgId) return m;
+
+ return {
+ ...m,
+ files: m.files.map(f => {
+ if (f.fileId !== fileId) return f;
+ return {
+ ...f,
+ extraMetaData: {
+ ...f.extraMetaData,
+ totalChunks: allChunks,
+ uploadedChunks: chunksDone
+ }
+ };
+ })
+ };
+ })
+ );
+ }
+
+ updateTempMessage(tempMsgId: string, message: Message) {
+ this.store.messages.update(messages =>
+ messages.map(m => {
+ if (m.msgid !== tempMsgId) return m;
+
+ return {
+ ...m,
+ msgid: message.msgid,
+ sent_at: message.sent_at,
+ files: m.files.map(f => {
+ f.extraMetaData["progressShown"] = false
+ return f
+ })
+ };
+ })
+ );
}
// The chatid parameter ensures isolation
@@ -170,6 +222,7 @@ export class Dm implements OnInit {
chatData: signal(chatData),
messages: signal([]),
messageBox: new MessageBoxViewModel((msg, files) => this.sendMessage(msg, files)),
+ messagesVm: new MessagesViewModel(),
wsListener: (action, data) => this.onWsListen(action, data, chatid),
} as DmStorage;
@@ -187,6 +240,7 @@ export class Dm implements OnInit {
try {
const messagesCache = await currentStore.service.getQuick();
currentStore.messages.set(messagesCache);
+ this.scrollToBottom("instant")
} catch (e) {
console.warn(`Cache load failed: ${e}. Skipping cache load...`)
}
@@ -195,6 +249,8 @@ export class Dm implements OnInit {
this.scrollToBottom("instant")
await currentStore.service.joinWebSocketRoom();
+ } else {
+ this.scrollToBottom("instant")
}
});
}
diff --git a/src/app/chat/elements/message-box/message-box.html b/src/app/chat/elements/message-box/message-box.html
index 8a56106..534d408 100644
--- a/src/app/chat/elements/message-box/message-box.html
+++ b/src/app/chat/elements/message-box/message-box.html
@@ -80,12 +80,12 @@
-
+
{{ "chat.elements.messageBox.placeholder"|translate }}
-
+
diff --git a/src/app/chat/elements/messages/messages-viewmodel.ts b/src/app/chat/elements/messages/messages-viewmodel.ts
new file mode 100644
index 0000000..26e04dd
--- /dev/null
+++ b/src/app/chat/elements/messages/messages-viewmodel.ts
@@ -0,0 +1,6 @@
+import {signal} from '@angular/core';
+
+export class MessagesViewModel {
+ // Saves scrolling state. First value initialized when scrolling to bottom on the message load
+ scrollBarStatus = signal(-1)
+}
diff --git a/src/app/chat/elements/messages/messages.html b/src/app/chat/elements/messages/messages.html
index 31c5867..1302599 100644
--- a/src/app/chat/elements/messages/messages.html
+++ b/src/app/chat/elements/messages/messages.html
@@ -17,24 +17,47 @@
{{ message.message }}
@for (file of filterExpressedMedia(message.files); track file) {
- @if (file.type == "image") {
-
- } @else if (file.type == "video") {
+
@if (file.extraMetaData && Object.keys(file.extraMetaData).length > 0) {
-
- } @else {
-
+ @if (file.extraMetaData['progressShown']) {
+
+ }
}
- }
+ @if (file.type == "image") {
+
+ } @else if (file.type == "video") {
+ @if (file.extraMetaData && Object.keys(file.extraMetaData).length > 0) {
+
+ } @else {
+
+ }
+ }
+
}
@for (file of filterNonExpressedMedia(message.files); track file) {
-
-
+
+ @if (file.extraMetaData && Object.keys(file.extraMetaData).length > 0) {
+ @if (file.extraMetaData['progressShown']) {
+
+ } @else {
+
+ }
+ } @else {
+
+ }
{{ file.fileName }}
}
diff --git a/src/app/chat/elements/messages/messages.scss b/src/app/chat/elements/messages/messages.scss
index ba4111b..92109c1 100644
--- a/src/app/chat/elements/messages/messages.scss
+++ b/src/app/chat/elements/messages/messages.scss
@@ -31,7 +31,7 @@
}
.bubble {
- border-radius: 25px 10px 10px 10px !important;
+ border-radius: 10px 25px 10px 10px !important;
}
}
@@ -43,7 +43,7 @@
&.chained_end {
.bubble {
- border-radius: 10px 10px 10px 25px !important;
+ border-radius: 10px 10px 25px 10px !important;
}
}
diff --git a/src/app/chat/elements/messages/messages.ts b/src/app/chat/elements/messages/messages.ts
index c93e55d..104a663 100644
--- a/src/app/chat/elements/messages/messages.ts
+++ b/src/app/chat/elements/messages/messages.ts
@@ -18,6 +18,7 @@ import {TranslatePipe} from '@ngx-translate/core';
import {MessageBoxViewModel} from '../message-box/message-box-viewmodel';
import {FileDataWithPreview} from '../message-box/message-box';
import {Attachment} from '@chatenium/chatenium-sdk/domain/common.schema';
+import {TuiProgressCircle} from '@taiga-ui/kit';
@Component({
selector: 'messages',
@@ -32,7 +33,8 @@ import {Attachment} from '@chatenium/chatenium-sdk/domain/common.schema';
TranslatePipe,
TuiDataListComponent,
TuiGroup,
- TuiIcon
+ TuiIcon,
+ TuiProgressCircle
],
templateUrl: './messages.html',
styleUrl: './messages.scss',
diff --git a/src/app/chat/network/channel/text/text.html b/src/app/chat/network/channel/text/text.html
index c14871f..451bf13 100644
--- a/src/app/chat/network/channel/text/text.html
+++ b/src/app/chat/network/channel/text/text.html
@@ -25,7 +25,7 @@
-
diff --git a/src/app/chat/network/channel/text/text.ts b/src/app/chat/network/channel/text/text.ts
index 32b88f0..0d5e282 100644
--- a/src/app/chat/network/channel/text/text.ts
+++ b/src/app/chat/network/channel/text/text.ts
@@ -12,6 +12,8 @@ import {Message} from '@chatenium/chatenium-sdk/domain/textChannelService.schema
import {Messages} from '../../../elements/messages/messages';
import {Navbar} from '../../../elements/navbar/navbar';
import {Oimg} from '../../../elements/oimg/oimg';
+import {MessagesViewModel} from '../../../elements/messages/messages-viewmodel';
+import {v4 as uuidv4} from 'uuid';
@Component({
selector: 'app-text',
@@ -65,15 +67,16 @@ export class Text {
}
}
- this.store.messageBox.editingMessage.set(null)
- this.store.messageBox.message.set("")
return
}
let attachments: Attachment[] = []
files?.forEach(file => {
- const extraMetaData: Record
= {}
+ const extraMetaData: Record = {}
extraMetaData["thumbnailMetaData"] = file.videoThumbnail ?? ""
+ extraMetaData["progressShown"] = true
+ extraMetaData["totalChunks"] = 0
+ extraMetaData["uploadedChunks"] = 0
attachments.push({
fileName: file.name,
@@ -87,6 +90,7 @@ export class Text {
})
})
+ const tempMsgId = uuidv4()
this.store.messages.update(value => [...value, {
author: {
userid: session.userData.userid,
@@ -94,7 +98,7 @@ export class Text {
username: session.userData.username,
displayName: session.userData.displayName
},
- msgid: "",
+ msgid: tempMsgId,
message: message,
sent_at: {
T: 0,
@@ -104,7 +108,7 @@ export class Text {
channelId: "",
networkId: "",
categoryId: "",
- files: [],
+ files: attachments,
seen: false,
replyTo: "",
replyToId: "",
@@ -114,11 +118,12 @@ export class Text {
this.scrollToBottom("smooth")
- await this.store.service.sendMessage("", message, null, null, files, {
+ const respMessage = await this.store.service.sendMessage(tempMsgId, message, null, null, files, {
fileProgressUpdate: (tempMsgId, fileId, allChunks, chunksDone) => {
this.uploadProgressUpdate(tempMsgId, fileId, allChunks, chunksDone)
}
})
+ this.updateTempMessage(tempMsgId, respMessage)
}
}
@@ -138,19 +143,69 @@ export class Text {
scrollToBottom(anim: 'instant' | 'smooth'): void {
setTimeout(() => {
+ if (!this.store) {
+ return
+ }
const scrollContainer = document.querySelector("#scrollContainer")
scrollContainer.scroll({
- top: scrollContainer.scrollHeight,
+ top: this.store.messagesVm.scrollBarStatus() == -1 ? scrollContainer.scrollHeight : this.store.messagesVm.scrollBarStatus(),
left: 0,
behavior: anim
});
+
+ if (this.store.messagesVm.scrollBarStatus() == -1) {
+ this.store.messagesVm.scrollBarStatus.set(scrollContainer.scrollHeight)
+ }
}, 0)
}
- uploadProgressUpdate(tempMsgId: string, fileId: string, allChunks: number, chunksDone: number) {
- console.log(fileId, allChunks, chunksDone)
+ handleMessagesScroll(e: any) {
+ if (!this.store) return
+ this.store.messagesVm.scrollBarStatus.set(e.target.scrollTop)
}
+ uploadProgressUpdate(tempMsgId: string, fileId: string, allChunks: number, chunksDone: number) {
+ if (!this.store) return
+ this.store.messages.update(messages =>
+ messages.map(m => {
+ if (m.msgid !== tempMsgId) return m;
+
+ return {
+ ...m,
+ files: m.files.map(f => {
+ if (f.fileId !== fileId) return f;
+ return {
+ ...f,
+ extraMetaData: {
+ ...f.extraMetaData,
+ totalChunks: allChunks,
+ uploadedChunks: chunksDone
+ }
+ };
+ })
+ };
+ })
+ );
+ }
+
+ updateTempMessage(tempMsgId: string, message: Message) {
+ if (!this.store) return
+ this.store.messages.update(messages =>
+ messages.map(m => {
+ if (m.msgid !== tempMsgId) return m;
+
+ return {
+ ...m,
+ msgid: message.msgid,
+ sent_at: message.sent_at,
+ files: m.files.map(f => {
+ f.extraMetaData["progressShown"] = false
+ return f
+ })
+ };
+ })
+ );
+ }
// The chatid parameter ensures isolation
onWsListen(action: string, message: string, networkId: string, channelId: string) {
const data = JSON.parse(message);
@@ -205,6 +260,7 @@ export class Text {
categoryData: signal(categoryData),
channelData: signal(channelData),
messages: signal([]),
+ messagesVm: new MessagesViewModel(),
messageBox: new MessageBoxViewModel((msg, files) => this.sendMessage(msg, files)),
wsListener: (action, data) => this.onWsListen(action, data, networkId, categoryId),
} as TextChannelStorage;
@@ -225,13 +281,17 @@ export class Text {
try {
const messagesCache = await currentStore.service.getQuick();
currentStore.messages.set(messagesCache);
+ this.scrollToBottom("instant")
} catch (e) {
console.warn(`Cache load failed: ${e}. Skipping cache load...`)
}
const messages = await currentStore.service.get();
currentStore.messages.set(messages);
+ this.scrollToBottom("instant")
await currentStore.service.joinWebSocketRoom();
+ } else {
+ this.scrollToBottom("instant")
}
});
}
diff --git a/src/app/homepage/homepage.html b/src/app/homepage/homepage.html
index 800d15e..e3e1d6d 100644
--- a/src/app/homepage/homepage.html
+++ b/src/app/homepage/homepage.html
@@ -70,6 +70,11 @@
{{'home.enterChtnOnWeb'|translate}}
+
+
+ Git
+
+
Roadmap
@@ -82,71 +87,6 @@
-
-
-
{{ 'home.chtn'|translate }}
- {{ 'home.adaptsToYou'|translate }}
-
{{ 'home.adaptsToYouDesc'|translate }}
-
-
-
-
-
-
{{ 'home.chtnOnWeb'|translate }}
-
{{ 'home.chtnOnWebDesc'|translate }}
-
-
-
-
-
-
{{ 'home.chtnOnAndroid'|translate }}
-
{{'home.openBeta'|translate}}
-
{{ 'home.chtnOnAndroidDesc'|translate }}
-
-
-
-
-
-
{{ 'home.chtnOnApple'|translate }}
-
{{'home.openBeta'|translate}}
-
{{ 'home.chtnOnAppleDesc'|translate }}
-
-
-
-
-
-
{{ 'home.chtnOnWindows'|translate }}
-
{{'home.openBeta'|translate}}
-
{{ 'home.chtnOnWindowsDesc'|translate }}
-
-
-
-
-
-
{{ 'home.chtnOnLinux'|translate }}
-
{{'home.openBeta'|translate}}
-
{{ 'home.chtnOnLinuxDesc'|translate }}
-
-
-
-
-
-
{{ 'home.chtnEcho'|translate }}
-
{{'home.openBeta'|translate}}
-
{{ 'home.chtnEchoDesc'|translate }}
-
-
-
-
-
-
{{ 'home.chtnReson'|translate }}
-
{{'home.openBeta'|translate}}
-
{{ 'home.chtnResonDesc'|translate }}
-
-
-
-
-
{{ 'home.chtnIs'|translate }}
@@ -192,48 +132,35 @@
-