1 Commits

Author SHA1 Message Date
a01b5347d6 3.0 Beta 2 2026-04-10 21:57:29 +02:00
65 changed files with 1619 additions and 175 deletions

82
package-lock.json generated
View File

@@ -15,7 +15,10 @@
"@angular/forms": "^21.2.0",
"@angular/platform-browser": "^21.2.0",
"@angular/router": "^21.2.0",
"@chatenium/chatenium-sdk": "^1.1.3",
"@chatenium/chatenium-sdk": "^1.1.5",
"@fortawesome/angular-fontawesome": "^4.0.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@ngx-translate/core": "^17.0.0",
"@ngx-translate/http-loader": "^17.0.0",
"@taiga-ui/addon-charts": "^5.1.0",
@@ -988,13 +991,12 @@
}
},
"node_modules/@chatenium/chatenium-sdk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.1.3.tgz",
"integrity": "sha512-y1+ls4MnMu9/t0vWtQEjIw1QPwk0peQbdEx738xu8OgxD/0nEBn6SGhhnesPHcQmdnkhZe/xiRVrIlDmZxGUHg==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.1.5.tgz",
"integrity": "sha512-seEsxlRk96WHsE4h6oPPa5nGZioePIZpB5qk0Xs3CaKNbVJKH02/Q5HW2PIi6DHw+djAENWu5zVXIic+9DrqZw==",
"dependencies": {
"@faker-js/faker": "^10.4.0",
"axios": "^1.14.0",
"dotenv": "^17.4.0",
"msw": "^2.12.14",
"uuid": "^13.0.0"
}
@@ -1652,6 +1654,64 @@
"npm": ">=10"
}
},
"node_modules/@fortawesome/angular-fontawesome": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-4.0.0.tgz",
"integrity": "sha512-TCqHqT5ovFY1A4RgMpoBUgS+RX3OVs39+CzHFgzDhbCPAopOa26J748TZJcuZwJAvGAk9tbWeVEmWuLByINAeg==",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"tslib": "^2.8.1"
},
"peerDependencies": {
"@angular/core": "^21.0.0"
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz",
"integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz",
"integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-common-types": "7.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-brands-svg-icons": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.2.0.tgz",
"integrity": "sha512-VNG8xqOip1JuJcC3zsVsKRQ60oXG9+oYNDCosjoU/H9pgYmLTEwWw8pE0jhPz/JWdHeUuK6+NQ3qsM4gIbdbYQ==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "7.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.2.0.tgz",
"integrity": "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "7.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@gar/promise-retry": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz",
@@ -5616,18 +5676,6 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": {
"version": "17.4.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
"integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",

View File

@@ -18,7 +18,10 @@
"@angular/forms": "^21.2.0",
"@angular/platform-browser": "^21.2.0",
"@angular/router": "^21.2.0",
"@chatenium/chatenium-sdk": "^1.1.3",
"@chatenium/chatenium-sdk": "^1.1.5",
"@fortawesome/angular-fontawesome": "^4.0.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@ngx-translate/core": "^17.0.0",
"@ngx-translate/http-loader": "^17.0.0",
"@taiga-ui/addon-charts": "^5.1.0",

BIN
public/Kinn-Book.ttf Normal file

Binary file not shown.

BIN
public/Kinn-Heavy.ttf Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,4 +1,80 @@
{
"version": "3.0 Beta 2 (2026.04.10)",
"ok": "Ok",
"aChatProgram": "A messaging platform that you can trust.",
"fast": "Fast",
"secure": "Secure",
"independent": "Independent",
"scrollDownForMore": "Scroll down for more",
"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.",
"home": {
"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.",
"chtnOnWeb": "Chatenium On Web",
"chtnOnWebDesc": "Built for maximum compatibility. The Web will work on every device you have.",
"chtnOnAndroid": "Chatenium On Android™",
"chtnOnAndroidDesc": "Built to work beautifully with your Android™ device. Blends in beautifully with your Material You theme.",
"chtnOnApple": "Chatenium On Apple®",
"chtnOnAppleDesc": "Built with the latest Liquid Glass design to blend in and work flawlessly across all your Apple® devices.",
"chtnOnWindows": "Chatenium On Windows®",
"chtnOnWindowsDesc": "We are also expanding our native apps to Microsoft Windows®, because using a web app is resource intensive and inferior to a native application. So we built a fully native Windows® application using WinUI3 + C#.",
"chtnOnLinux": "Chatenium On Linux",
"chtnOnLinuxDesc": "Built on the latest GNOME Adwaita design language to beautifully blend into the GNOME desktop environment on Linux.",
"chtnEcho": "Chatenium Echo",
"chtnEchoDesc": "Built on the GNOME GTK design language to beautifully blend into any GTK based desktop environment on Linux.",
"chtnReson": "Chatenium Reson",
"chtnResonDesc": "Built on the KDE Kirigami design language to beautifully blend into KDE or any other Qt based desktop environment on Linux.",
"tba": "To be announced",
"secure": "secure",
"secureDesc": "Chatenium uses the latest algorithms to protect your data from any third-parties.",
"secureCdn": "Encrypted CDN",
"secureCdnDesc": "All uploaded media is encrypted and never accessed by anyone outside your chat. Not even by us. All deleted media are permanently removed from our servers and databases.",
"encryptedText": "Encrypted messages",
"encryptedTextDesc": "Any message that is sent to any DM chat or channel is encrypted and stored securely in our databases. We do not share your messages with any third-parties.",
"zeroDataCollection": "Zero data collection",
"zeroDataCollectionDesc": "Our policies allow zero data collection. We do not collect any data from our clients.",
"secureCalling": "Secure calls",
"secureCallingDesc": "When using Chatenium Call, all audio and video tracks are sent to our servers for secure distribution. No audio or video is ever recorded by our servers under any circumstances.",
"zeroDigitalFootprint": "Zero digital footprint",
"zeroDigitalFootprintDesc": "We make it easy to delete your account from our services and we make sure to remove all data made by you.",
"underDevelopment": "Under development",
"devPreview": "Developer Preview",
"weAreExcitedFor": "We are excited for ",
"you": "you",
"weAreExcitedForYouDesc": "Our community awaits you with love. We are independent, so you can express your true feelings without getting censored.",
"enterChtnOnWeb": "Enter Chatenium On Web",
"enterChtnOnWebDesc": "Register your account here are access the latest experimental features we can offer. The web is mobile-friendly and offers offline mode.",
"downloadChtnOnAndroid": "Get Chatenium On Android",
"downloadChtnOnAndroidDesc": "We are preparing a Google Play release right now. Don't worry, you can get access to .apk releases in the official 'Chatenium' network.",
"downloadChtnOnApple": "Get Chatenium On Apple",
"downloadChtnOnAppleDesc": "We are preparing an App Store release right now. Don't worry, you can get access to .ipa (.dmg soon) releases in the official 'Chatenium' network.",
"storeReleaseSoon": "Store release coming soon...",
"closedAlpha": "Closed alpha",
"openBeta": "Beta",
"openBetaDesc": "Participate in public beta testing to help us create the best app experience!",
"closedAlphaDesc": "Great news! You are eligible to participate in the testing program. All we need is your e-mail address.",
"alphaRequestedSuccessfully": "Request sent successfully! We will get back to you later.",
"alphaRequestAlreadySent": "You already requested an alpha access! We will get back to you later.",
"alphaRequestError": "Something went wrong. Please try again later.",
"cookies": "Cookies",
"cookiesDesc": "Chatenium is using cookies to securely store your session token. No cookies are used for tracking purposes",
"understood": "Understood",
"privacyPolicy": "Privacy policy",
"tos": "Terms of services",
"requestAlphaAccess": "Start beta testing",
"joinGoogleGroup": "Our Google Group",
"joinGoogleGroupWhy": "All you need to do is join our Google Group and you can join the beta testing program via the link provided in the group.",
"testFlight": "Apple TestFlight",
"testFlightDesc": "Start testing the application on your Apple device via TestFlight!",
"downloadOnWindows": "Get Chatenium On Windows",
"microsoftStore": "Microsoft Store",
"microsoftStoreDesc": "Start testing the application on your Windows computer via the Microsoft Store!"
},
"signIn": {
"formTitle": "Sign in",
"mainInput": {
@@ -18,6 +94,13 @@
}
},
"chat": {
"changeLogDialog": {
"label": "Chatenium has been updated",
"changeLog": {
"1": "UI changes and bug fixes in the video player",
"2": "UI overflow fix in chat navigation bar"
}
},
"chatnav": {
"dmList": {
"newChat": "Start new chat",
@@ -30,6 +113,8 @@
},
"elements": {
"messageBox": {
"editMessageLabel": "Editing message: ",
"attachments": "Attachment(s)",
"placeholder": "Type a message...",
"message": "Message",
"uplDrag": {
@@ -39,6 +124,12 @@
"fileUploadDialog": {
"label": "Upload files"
}
},
"messages": {
"contextMenu": {
"edit": "Edit",
"delete": "Delete"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
public/images/logo/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
public/images/logo/logo_beta.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/images/logo/logo_short.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/images/logo/logoold.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -2,11 +2,19 @@ import {Routes} from '@angular/router';
import {SignIn} from './signin/signin';
import {Chat} from './chat/chat';
import {Dm} from './chat/dm/dm';
import {noAuthGuard} from './guards/no-auth-guard';
import {authNeededGuard} from './guards/auth-needed-guard';
import {Homepage} from './homepage/homepage';
import {Privacy} from './privacy/privacy';
import {TOS} from './tos/tos';
export const routes: Routes = [
{path: 'signin', component: SignIn},
{path: '', component: Homepage},
{path: 'privacy', component: Privacy},
{path: 'tos', component: TOS},
{path: 'signin', component: SignIn, canActivate: [noAuthGuard]},
{
path: 'chat', component: Chat, children: [
path: 'chat', component: Chat, canActivate: [authNeededGuard], children: [
{path: 'dm/:chatid', component: Dm},
]
},

View File

@@ -1,3 +1,13 @@
<ng-template [(tuiDialog)]="changeLogOpen" [tuiDialogOptions]="{closable: false, dismissible: false, label: 'chat.changeLogDialog.label'|translate}">
<h3>{{"version"|translate}}</h3>
<ul>
<li>{{"chat.changeLogDialog.changeLog.1"|translate}}</li>
<li>{{"chat.changeLogDialog.changeLog.2"|translate}}</li>
</ul>
<button tuiButton iconStart="@tui.check" (click)="localStorage.setItem('changeLogLastRead', environment.version); changeLogOpen.set(false)">{{"ok"|translate}}</button>
</ng-template>
@if (serviceManager.currentSession() == null) {
<main style="width: 100%; height: 100svh; display: flex; justify-content: center; align-items: center;">
<tui-loader size="xl"/>
@@ -10,15 +20,15 @@
<button>
<tui-icon icon="@tui.message-circle"/>
</button>
<button>
<button disabled style="pointer-events: none; opacity: 0.5">
<tui-icon icon="@tui.network"/>
</button>
<button>
<button disabled style="pointer-events: none; opacity: 0.5">
<tui-icon icon="@tui.image"/>
</button>
</tui-segmented>
<button id="bottom_btn" tuiButton appearance="flat">
<button id="bottom_btn" tuiButton appearance="flat" disabled>
<tui-icon icon="@tui.cog"/>
</button>
</aside>

View File

@@ -7,6 +7,7 @@
#chatnav {
display: grid;
grid-template-columns: 70px minmax(0, 1fr);
height: 100svh;
aside {
padding: 15px;
@@ -25,6 +26,12 @@
button {
height: 50px;
tui-icon {
&::before {
color: var(--tui-background-accent-1);
}
}
}
}
@@ -41,11 +48,13 @@
border-radius: 20px 0 0 20px;
margin: 10px 0 10px 10px;
padding: 15px;
overflow-y: scroll;
}
}
#content {
width: 100%;
height: 100svh;
padding: 10px 10px 10px 0;
#content_tint {

View File

@@ -1,13 +1,15 @@
import {Component, inject, OnInit} from '@angular/core';
import {Component, inject, OnInit, signal} from '@angular/core';
import {RouterOutlet} from '@angular/router';
import {TuiSegmented} from '@taiga-ui/kit';
import {TuiAppearance, TuiButton, TuiGroup, TuiIcon, TuiLoader} from '@taiga-ui/core';
import {TuiAppearance, TuiButton, TuiDialog, TuiGroup, TuiIcon, TuiLoader} from '@taiga-ui/core';
import {SessionManager} from '@chatenium/chatenium-sdk/services/sessionManager';
import {LoadStatus, ServiceManager} from '../service-manager';
import {IndexedDB} from '../storage/indexed-db';
import {DmList} from './dm-list/dm-list';
import {JsonPipe} from '@angular/common';
import {WebSocketHandler} from '@chatenium/chatenium-sdk/core/webSocketHandler';
import {TranslatePipe} from '@ngx-translate/core';
import {environment} from '../../environments/environment';
@Component({
selector: 'app-chat',
@@ -20,7 +22,9 @@ import {WebSocketHandler} from '@chatenium/chatenium-sdk/core/webSocketHandler';
DmList,
JsonPipe,
TuiAppearance,
TuiGroup
TuiGroup,
TuiDialog,
TranslatePipe
],
templateUrl: './chat.html',
styleUrl: './chat.scss',
@@ -29,13 +33,24 @@ export class Chat implements OnInit {
serviceManager = inject(ServiceManager)
indexedDb = inject(IndexedDB)
changeLogOpen = signal(false)
async ngOnInit() {
this.indexedDb.openDatabase().then(async () => {
const session = await this.serviceManager.sessionManager.loadPreferredSession()
this.serviceManager.currentSession.set(session)
await WebSocketHandler.getInstance().connect(session.userData.userid, session.token)
})
setTimeout(() => {
const latestRead = localStorage.getItem("changeLogLastRead")
if (latestRead != environment.version) {
this.changeLogOpen.set(true)
}
}, 50)
}
protected readonly LoadStatus = LoadStatus;
protected readonly localStorage = localStorage;
protected readonly environment = environment;
}

View File

@@ -13,13 +13,13 @@
</div>
<div class="items-right">
<button tuiButton appearance="flat">
<button tuiButton appearance="flat" disabled>
<tui-icon icon="@tui.phone"/>
</button>
</div>
</navbar>
<messages [messages]="store.messages()"/>
<messages [messageBoxViewModel]="store.messageBox" [messages]="store.messages()" id="scrollContainer" (onDelete)="deleteMessage($event)"/>
<message-box [viewModel]="store.messageBox"/>
}

View File

@@ -40,8 +40,31 @@ export class Dm implements OnInit {
}
async sendMessage(message: string, files: FileDataWithPreview[] | null) {
if (!files && message.trim() == "") return
const session = this.serviceManager.currentSession();
if (session != null) {
const editedMessage = this.store.messageBox.editingMessage()
if (editedMessage) {
const storedMsg = this.store.messages().find(m => m.msgid == editedMessage.messageId)
const originalMessage: Message = JSON.parse(JSON.stringify(storedMsg))
if (storedMsg) {
storedMsg.message = message
}
try {
await this.store.service.editMessage(editedMessage.messageId, message)
} catch (e) {
if (storedMsg) {
storedMsg.message = originalMessage.message
}
}
this.store.messageBox.editingMessage.set(null)
this.store.messageBox.message.set("")
return
}
let attachments: Attachment[] = []
files?.forEach(file => {
const extraMetaData: Record<string, string> = {}
@@ -74,12 +97,40 @@ export class Dm implements OnInit {
forwardedFromName: ""
}])
this.scrollToBottom("smooth")
await this.store.service.sendMessage("", message, null, null, files, <FileUploadProgressListener>{
fileProgressUpdate: (tempMsgId, fileId, allChunks, chunksDone) => {
this.uploadProgressUpdate(tempMsgId, fileId, allChunks, chunksDone)
}
})
}
this.store.messageBox.message.set("")
}
async deleteMessage(messageId: string) {
const i = this.store.messages().findIndex(m => m.msgid == messageId)
if (i != -1) {
const originalMessage: Message = JSON.parse(JSON.stringify(this.store.messages()[i]))
this.store.messages().splice(i, 1)
try {
await this.store.service.deleteMessages([messageId])
} catch (e) {
this.store.messages().splice(i, 0, originalMessage)
}
}
}
scrollToBottom(anim: 'instant' | 'smooth'): void {
setTimeout(() => {
const scrollContainer = <HTMLDivElement>document.querySelector("#scrollContainer")
scrollContainer.scroll({
top: scrollContainer.scrollHeight,
left: 0,
behavior: anim
});
}, 0)
}
uploadProgressUpdate(tempMsgId: string, fileId: string, allChunks: number, chunksDone: number) {
@@ -95,6 +146,7 @@ export class Dm implements OnInit {
switch (action) {
case "newMessage":
targetStore.messages.update(messages => [...messages, data]);
this.scrollToBottom("smooth")
break;
}
}
@@ -135,6 +187,7 @@ export class Dm implements OnInit {
const currentStore = this.serviceManager.dmServices()[chatid];
const history = await currentStore.service.get();
currentStore.messages.set(history);
this.scrollToBottom("instant")
await currentStore.service.joinWebSocketRoom();
}

View File

@@ -12,4 +12,5 @@ export class MessageBoxViewModel {
message = signal<string>("")
files = signal<FileDataWithPreview[]>([])
dialogOpen = signal<boolean>(false)
editingMessage = signal<{messageId: string, message: string} | null >(null)
}

View File

@@ -5,7 +5,7 @@
</div>
<span>{{ "chat.elements.messageBox.uplDrag.upload"|translate }}</span>
</div>
<div class="method">
<div class="method" style="opacity: 0.5">
<div class="icon-holder">
<tui-icon icon="@tui.cloud-sync"/>
</div>
@@ -14,13 +14,25 @@
</div>
<ng-template [(tuiDialog)]="viewModel().dialogOpen" [tuiDialogOptions]="{label: 'chat.elements.messageBox.fileUploadDialog.label'|translate}">
<masonry [maxColSize]="2" style="overflow-y: scroll; height: 500px">
@if (filterPictureVideo(viewModel().files()).length != 0) {
<masonry [maxColSize]="2" style="overflow-y: scroll; height: 200px">
@for (file of viewModel().files(); track file) {
@if (file.type == "image") {
<img [src]="file.blob" style="width: 100%; height: 100%; object-fit: fill;"/>
}
}
</masonry>
}
<div tuiGroup orientation="vertical" style="width: 100%">
@for (file of viewModel().files(); track file) {
@if (file.type == "image") {
<img [src]="file.blob" style="width: 100%; height: 100%; object-fit: fill;"/>
@if (file.type != "image") {
<div style="background: var(--tui-background-neutral-1); height: 50px; display: flex; align-items: center; padding: 0 10px">
<span>{{ file.name }}</span>
</div>
}
}
</masonry>
</div>
<div style="margin-top: 10px; display: flex; gap: 10px; width: 100%">
<tui-textfield style="width: 100%">
@@ -37,25 +49,45 @@
</div>
</ng-template>
<div id="message-box" [style]="'border-radius:'+messageBoxRadius+'px;'">
<div class="items-left">
<button tuiButton appearance="flat" (click)="uplInput.click()">
<tui-icon icon="@tui.file-up"/>
<input #uplInput type="file" (change)="handleFileInput($event)" multiple hidden/>
</button>
<main>
<div id="message-box-extension" tuiAppearance="floating" [class.shown]="viewModel().editingMessage()">
<div id="content">
@if (viewModel().editingMessage()) {
<tui-icon icon="@tui.pencil"></tui-icon>
<span>{{"chat.elements.messageBox.editMessageLabel"|translate}}</span>
@if (viewModel().editingMessage()!.message == "") {
<tui-icon icon="@tui.paperclip"></tui-icon>
<span>{{"chat.elements.messageBox.attachments"|translate}}</span>
} @else {
{{viewModel().editingMessage()!.message}}
}
}
</div>
<button tuiButton appearance="flat">
<tui-icon icon="@tui.cloud-sync"/>
<button id="close" tuiButton appearance="flat" (click)="viewModel().editingMessage.set(null); viewModel().message.set('');">
<tui-icon icon="@tui.x"/>
</button>
</div>
<div class="items-middle">
<textarea [style]="'height:'+textareaHeight+'px;'" #message (input)="onTextAreaInput(message)" [(ngModel)]="viewModel().message"></textarea>
<span class="placeholder"
[class.hidden]="message.value != ''">{{ "chat.elements.messageBox.placeholder"|translate }}</span>
<div id="message-box" [style]="'border-radius:'+messageBoxRadius+'px;'">
<div class="items-left">
<button tuiButton appearance="flat" (click)="uplInput.click()">
<tui-icon icon="@tui.file-up"/>
<input #uplInput type="file" (change)="handleFileInput($event)" multiple hidden/>
</button>
<button tuiButton appearance="flat" disabled>
<tui-icon icon="@tui.cloud-sync"/>
</button>
</div>
<div class="items-middle">
<textarea (keydown.enter)="handleEnterKeydown($event); viewModel().onMessageSend(message.value, null)" [style]="'height:'+textareaHeight+'px;'" #message (input)="onTextAreaInput(message)" [(ngModel)]="viewModel().message"></textarea>
<span class="placeholder"
[class.hidden]="message.value != ''">{{ "chat.elements.messageBox.placeholder"|translate }}</span>
</div>
<div class="items-right">
<button [disabled]="message.value.trim() == ''" tuiButton appearance="flat" (click)="viewModel().onMessageSend(message.value, null)">
<tui-icon icon="@tui.send"/>
</button>
</div>
</div>
<div class="items-right">
<button tuiButton appearance="flat" (click)="viewModel().onMessageSend(message.value, null)">
<tui-icon icon="@tui.send"/>
</button>
</div>
</div>
</main>

View File

@@ -48,72 +48,115 @@
}
}
#message-box {
transition: all 0.2s ease-in-out;
width: 60%;
background: var(--tui-background-base-alt);
min-height: 75px;
max-height: 200px;
border: 2px solid var(--tui-border-normal);
display: grid;
grid-template-columns: 85px 1fr 100px;
main {
width: 100%;
display: flex;
align-items: center;
padding: 0 10px;
flex-direction: column;
gap: 5px;
.items-left, .items-middle, .items-right {
display: flex;
align-items: center;
}
#message-box-extension {
transition: all 0.2s ease-in-out;
width: 50%;
border-radius: 20px;
background: var(--tui-background-base-alt);
border: 2px solid var(--tui-border-normal);
transform: translateY(50px);
display: grid;
grid-template-columns: 1fr 40px;
justify-content: center;
height: 0;
#content {
padding-left: 10px;
display: flex;
gap: 5px;
align-items: center;
}
tui-icon {
font-size: 13px;
}
.items-left, .items-right {
button {
height: 30px;
height: 26px;
width: 30px;
}
tui-icon {
font-size: 20px;
}
&.shown {
transform: translateY(0);
height: 30px;
}
}
.items-middle {
::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 10px;
#message-box {
z-index: 5;
transition: all 0.2s ease-in-out;
width: 60%;
background: var(--tui-background-base-alt);
min-height: 75px;
max-height: 200px;
border: 2px solid var(--tui-border-normal);
display: grid;
grid-template-columns: 85px 1fr 100px;
align-items: center;
padding: 0 10px;
.items-left, .items-middle, .items-right {
display: flex;
align-items: center;
}
position: relative;
.items-left, .items-right {
button {
height: 30px;
width: 30px;
textarea {
width: 100%;
background: transparent;
border: none;
outline: none;
resize: none;
color: var(--tui-text-01);
font-size: 16px;
z-index: 1;
}
.placeholder {
position: absolute;
font-size: 16px;
height: 25px;
top: 2px;
left: 2px;
color: gray;
transition: all 0.2s ease-in-out;
&.hidden {
margin-left: 10px;
opacity: 0;
tui-icon {
font-size: 20px;
}
}
}
}
.items-right {
display: flex;
justify-content: end;
.items-middle {
::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 10px;
}
position: relative;
textarea {
width: 100%;
background: transparent;
border: none;
outline: none;
resize: none;
color: var(--tui-text-01);
font-size: 16px;
z-index: 1;
}
.placeholder {
position: absolute;
font-size: 16px;
height: 25px;
top: 2px;
left: 2px;
color: gray;
transition: all 0.2s ease-in-out;
&.hidden {
margin-left: 10px;
opacity: 0;
}
}
}
.items-right {
display: flex;
justify-content: end;
}
}
}
}

View File

@@ -103,6 +103,15 @@ export class MessageBox {
this.viewModel().dialogOpen.set(false)
}
handleEnterKeydown(e: any) {
e.preventDefault()
return false
}
filterPictureVideo(files: FileDataWithPreview[]) {
return files.filter(f => f.type == "image")
}
async processFiles(fileList: FileList) {
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i];

View File

@@ -8,25 +8,59 @@
[class.chained_end]="isMessageEndOfChain(i)"
>
<div class="above">
<span>{{message.sent_at.T * 1000 | date: 'HH:mm'}}</span>
<span>{{ message.sent_at.T * 1000 | date: 'HH:mm' }}</span>
</div>
<div class="bubble">
<span class="message-text">{{message.message}}</span>
<div class="bubble" tuiDropdownContext [tuiDropdown]="messageContextMenu">
<span class="message-text">{{ message.message }}</span>
<masonry style="max-height: 300px">
@for (file of message.files; track file) {
@for (file of filterExpressedMedia(message.files); track file) {
@if (file.type == "image") {
<img [src]="file.path" style="width: 100%; height: 100%; max-height: 300px; object-fit: cover; border-radius: 25px"/>
<img [src]="file.path"
style="width: 100%; height: 100%; max-height: 300px; object-fit: cover; border-radius: 25px"/>
} @else if (file.type == "video") {
@if (file.extraMetaData && Object.keys(file.extraMetaData).length > 0) {
<video-player maxHeight="250px" maxWidth="250px" [src]="file.path" [thumbnailOverwrite]="file.extraMetaData['thumbnailMetaData']"></video-player>
<video-player maxHeight="300px" maxWidth="300px" [src]="file.path"
[thumbnailOverwrite]="file.extraMetaData['thumbnailMetaData']"></video-player>
} @else {
<video-player maxHeight="250px" maxWidth="250px" [src]="file.path"></video-player>
<video-player maxHeight="300px" maxWidth="300px" [src]="file.path"></video-player>
}
}
}
</masonry>
<div tuiGroup orientation="vertical" style="width: 100%">
@for (file of filterNonExpressedMedia(message.files); track file) {
<div style="width: 100%; height: 50px; display: flex; align-items: center; padding: 0 10px; background: var(--tui-background-accent-1); gap: 5px">
<tui-icon icon="@tui.file"/>
<span>{{ file.fileName }}</span>
</div>
}
</div>
</div>
<div class="below"></div>
<ng-template #messageContextMenu>
<tui-data-list>
<button
tuiOption
type="button"
iconEnd="@tui.pencil"
(click)="messageBoxViewModel().editingMessage.set({message: message.message, messageId: message.msgid}); messageBoxViewModel().message.set(message.message);"
>
{{ "chat.elements.messages.contextMenu.edit" | translate }}
</button>
<button
style="color: var(--tui-text-negative)"
tuiOption
type="button"
iconEnd="@tui.trash"
(click)="onDelete.emit(message.msgid)"
>
{{ "chat.elements.messages.contextMenu.delete" | translate }}
</button>
</tui-data-list>
</ng-template>
</div>
}
}

View File

@@ -56,6 +56,7 @@
max-width: 50%;
min-width: 250px;
min-height: 40px;
max-height: 400px;
display: flex;
flex-direction: column;
justify-content: center;

View File

@@ -1,17 +1,38 @@
import {Component, inject, input} from '@angular/core';
import {Component, EventEmitter, inject, input, Output} from '@angular/core';
import {Message} from '@chatenium/chatenium-sdk/domain/dmService.schema';
import {Message as NetworkMessage} from '@chatenium/chatenium-sdk/domain/textChannelService.schema'
import {ServiceManager} from '../../../service-manager';
import {DatePipe} from '@angular/common';
import {Masonry} from '../masonry/masonry';
import {VideoPlayer} from '../video-player/video-player';
import {
TuiButton,
TuiDataListComponent,
TuiDropdown,
TuiDropdownContext,
TuiGroup,
TuiIcon,
TuiOption
} from '@taiga-ui/core';
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';
@Component({
selector: 'messages',
imports: [
DatePipe,
Masonry,
VideoPlayer
VideoPlayer,
TuiDropdownContext,
TuiDropdown,
TuiButton,
TuiOption,
TranslatePipe,
TuiDataListComponent,
TuiGroup,
TuiIcon
],
templateUrl: './messages.html',
styleUrl: './messages.scss',
@@ -19,7 +40,10 @@ import {VideoPlayer} from '../video-player/video-player';
export class Messages {
serviceManager = inject(ServiceManager)
messages = input<Message[] | NetworkMessage[]>([])
messages = input.required<Message[] | NetworkMessage[]>()
messageBoxViewModel = input.required<MessageBoxViewModel>()
@Output() onDelete = new EventEmitter<string>()
/**
* Helps code readability by specifying what type of messages are being processed.
@@ -111,5 +135,13 @@ export class Messages {
return i + 1 === this.messages().length;
}
filterExpressedMedia(files: Attachment[]) {
return files.filter(f => f.type == "image" || f.type == "video")
}
filterNonExpressedMedia(files: Attachment[]) {
return files.filter(f => f.type != "image" && f.type != "video")
}
protected readonly Object = Object;
}

View File

@@ -1 +1 @@
<img [src]="src()" [style]="'width:'+width()+';height:'+height()+'; border-radius: '+radius()+'px;'" class="">
<img [src]="src()" [style]="'width:'+width()+';height:'+height()+'; border-radius: '+radius()+'px; object-fit:'+objectFit()" class="">

View File

@@ -15,4 +15,5 @@ export class Oimg {
width = input("")
src = input("")
radius = input(15)
objectFit = input("cover")
}

View File

@@ -1,7 +1,7 @@
@if (playerActive) {
<div id="player" [style]="'max-width:'+maxWidth+';max-height:'+maxHeight" (mouseover)="controlShowed = true"
(mouseleave)="controlShowed = false" #player>
<video (mouseout)="showVolRange = false" [class.upScale]="videoFullscreen"
<video (mouseout)="showVolRange = false" (mouseover)="showVolRange = false" [class.upScale]="videoFullscreen"
[style]="'max-width:'+maxWidth+';height:'+maxHeight+';border-radius: 15px'" (pause)="videoPlaying = false"
(play)="videoPlaying = true" #video (timeupdate)="watched = videoHMSFormat(video.currentTime)"
(loadedmetadata)="videoPlayer = video; video.style.display = 'block'; videoLoaded = true"
@@ -15,28 +15,21 @@
<div id="controlsHolder">
<div id="controls">
<button tuiButton appearance="icon"
(click)="videoPlaying ? video.pause() : video.play();">
@if (videoPlaying) {
<tui-icon icon="@tui.pause"></tui-icon>
} @else {
<tui-icon icon="@tui.play"></tui-icon>
}
</button>
<span>{{ watched }}</span>
<progress class="timeProgress" (click)="jump($event)" [max]="video.duration" [value]="video.currentTime"
tuiProgressBar size="xs"></progress>
<div>
<!-- @if (showVolRange) { -->
<!-- } -->
<div class="volumeSetter" (mouseover)="showVolRange = true">
<div id="buttons">
<button tuiButton appearance="icon"
(mouseover)="showVolRange = false"
(click)="videoPlaying ? video.pause() : video.play();" id="left">
@if (videoPlaying) {
<tui-icon icon="@tui.pause"></tui-icon>
} @else {
<tui-icon icon="@tui.play"></tui-icon>
}
</button>
<div id="right">
<button
type="button"
tuiButton appearance="icon"
tuiDropdown
tuiDropdownAppearance="neutral"
tuiDropdownAuto
tuiDropdownDirection="top"
tuiDropdownLimitWidth="fixed"
tuiIconButton
@@ -50,26 +43,30 @@
} @else {
<tui-icon icon="@tui.volume"></tui-icon>
}
<input
*tuiDropdown
tuiSlider
type="range"
value="100"
(input)="setVolume(volume)"
[(ngModel)]="volume"
/>
</button>
<input
tuiSlider
type="range"
value="100"
(input)="setVolume(volume)"
[(ngModel)]="volume"
/>
<button tuiButton appearance="icon"
(mouseover)="showVolRange = false"
(click)="videoFullscreen ? exitFullScreen() : player.requestFullscreen(); videoFullscreen = !videoFullscreen">
@if (videoFullscreen) {
<tui-icon icon="@tui.minimize"></tui-icon>
} @else {
<tui-icon icon="@tui.maximize"></tui-icon>
}
</button>
</div>
</div>
<button tuiButton appearance="icon"
(click)="videoFullscreen ? exitFullScreen() : player.requestFullscreen(); videoFullscreen = !videoFullscreen">
@if (videoFullscreen) {
<tui-icon icon="@tui.minimize"></tui-icon>
} @else {
<tui-icon icon="@tui.maximize"></tui-icon>
}
</button>
<div id="info">
<span>{{ watched }}</span>
<progress class="timeProgress" (click)="jump($event)" [max]="video.duration" [value]="video.currentTime"
tuiProgressBar size="xs"></progress>
</div>
</div>
</div>
</div>
@@ -77,9 +74,10 @@
} @else {
<div class="player_preview">
@if (thumbnailOverwrite) {
<img [style]="'max-width:'+maxWidth+';max-height:'+maxHeight+';border-radius: 15px'" [src]="thumbnailOverwrite" />
<img [style]="'max-width:'+maxWidth+';max-height:'+maxHeight+';border-radius: 15px'" [src]="thumbnailOverwrite"/>
} @else {
<oimg [height]="maxHeight" [width]="maxWidth" style="border-radius: 15px" [src]="src+'_thumbnail.png'"></oimg>
<oimg [height]="maxHeight" [width]="maxWidth" style="border-radius: 15px; object-fit: cover"
[src]="src+'_thumbnail.png'"></oimg>
}
<button tuiButton (click)="playVideo()">
<tui-icon icon="@tui.play"></tui-icon>

View File

@@ -36,33 +36,49 @@
#controls {
width: 95%;
height: 50px;
height: 60px;
background: var(--tui-background-base-alt);
border: 2px solid var(--tui-border-normal);
padding: 10px;
border-radius: 20px;
gap: 5px;
display: grid;
grid-template-columns: 50px 50px 1fr 50px 50px;
align-items: center;
display: flex;
flex-direction: column;
button {
height: 25px;
width: 50px;
}
.time {
font-size: 15px;
display: flex;
flex-direction: column;
gap: 0;
}
.volumeSetter {
display: flex;
gap: 5px;
#info {
display: grid;
grid-template-columns: 50px 1fr;
align-items: center;
}
#buttons {
display: grid;
grid-template-columns: 50px 1fr;
align-items: center;
#right {
display: flex;
align-items: center;
justify-content: end;
}
button {
height: 25px;
width: 50px;
}
.time {
font-size: 15px;
display: flex;
flex-direction: column;
gap: 0;
}
.volumeSetter {
display: flex;
gap: 5px;
align-items: center;
}
}
}
}
}
@@ -92,6 +108,7 @@ tui-dropdown[data-appearance] {
box-shadow: none;
border: none;
backdrop-filter: blur(1rem);
position: fixed;
[tuiSlider] {
position: absolute;

View File

@@ -1,7 +1,7 @@
import {Component, Input, ViewEncapsulation} from '@angular/core';
import {
TuiButton,
TuiDropdownDirective,
TuiDropdown,
TuiDropdownOpen,
TuiDropdownOptionsDirective,
TuiIcon,
@@ -21,7 +21,7 @@ import {TuiInputDateRange, TuiProgressBar} from '@taiga-ui/kit';
TuiSlider,
TuiProgressBar,
TuiDropdownOptionsDirective,
TuiDropdownDirective,
TuiDropdown,
TuiDropdownOpen,
TuiInputDateRange
],

View File

@@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { CanActivateFn } from '@angular/router';
import { authNeededGuard } from './auth-needed-guard';
describe('authNeededGuard', () => {
const executeGuard: CanActivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => authNeededGuard(...guardParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeGuard).toBeTruthy();
});
});

View File

@@ -0,0 +1,24 @@
import {CanActivateFn, Router} from '@angular/router';
import {inject} from '@angular/core';
import {IndexedDB} from '../storage/indexed-db';
import {Keyring} from '../storage/keyring';
import {KeyValue} from '../storage/key-value';
import {SessionManager} from '@chatenium/chatenium-sdk/services/sessionManager';
export const authNeededGuard: CanActivateFn = (route, state) => {
const indexedDb = inject(IndexedDB)
const keyring = inject(Keyring)
const keyValue = inject(KeyValue)
const router = inject(Router)
indexedDb.openDatabase().then(async () => {
const sessionManager = new SessionManager(indexedDb.getApi(), keyring.getApi(), keyValue.getApi());
const sessions = await sessionManager.loadSessions()
if (sessions.length == 0) {
router.navigate(['/signin'])
}
})
return true;
};

View File

@@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { CanActivateFn } from '@angular/router';
import { noAuthGuard } from './no-auth-guard';
describe('noAuthGuard', () => {
const executeGuard: CanActivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => noAuthGuard(...guardParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeGuard).toBeTruthy();
});
});

View File

@@ -0,0 +1,24 @@
import {CanActivateFn, Router} from '@angular/router';
import {SessionManager} from '@chatenium/chatenium-sdk/services/sessionManager';
import {inject} from '@angular/core';
import {IndexedDB} from '../storage/indexed-db';
import {Keyring} from '../storage/keyring';
import {KeyValue} from '../storage/key-value';
export const noAuthGuard: CanActivateFn = async (route, state) => {
const indexedDb = inject(IndexedDB)
const keyring = inject(Keyring)
const keyValue = inject(KeyValue)
const router = inject(Router)
indexedDb.openDatabase().then(async () => {
const sessionManager = new SessionManager(indexedDb.getApi(), keyring.getApi(), keyValue.getApi());
const sessions = await sessionManager.loadSessions()
if (sessions.length != 0) {
router.navigate(['/chat'])
}
})
return true;
};

View File

@@ -0,0 +1,268 @@
@if (cookieConsentOpen) {
<div id="cookieConsent" tuiCardLarge>
<h1>
<tui-icon icon="@tui.cookie"></tui-icon>
{{'home.cookies'|translate}}
</h1>
<p>{{'home.cookiesDesc'|translate}}</p>
<button (click)="consent()" tuiButton iconStart="@tui.check">
{{'home.understood'|translate}}
</button>
</div>
}
<ng-template [(tuiDialog)]="androidBetaTestDialogOpen"
[tuiDialogOptions]="{label: translateService.instant('home.requestAlphaAccess'), size: 's'}">
<span>{{'home.joinGoogleGroupWhy'|translate}}</span>
<button (click)="enterGGroup()" style="width: 100%; margin-top: 10px" tuiButton iconStart="@tui.users">{{'home.joinGoogleGroup'|translate}}</button>
</ng-template>
<ng-template [(tuiDialog)]="appleBetaTestDialogOpen"
[tuiDialogOptions]="{label: translateService.instant('home.requestAlphaAccess'), size: 's'}">
<span>{{'home.testFlightDesc'|translate}}</span>
<button (click)="openTestFlight()" style="width: 100%; margin-top: 10px" tuiButton >
<i class="fa-brands fa-apple" style="font-size: 25px"></i>
{{'home.testFlight'|translate}}
</button>
</ng-template>
<ng-template [(tuiDialog)]="msBetaTestDialogOpen"
[tuiDialogOptions]="{label: translateService.instant('home.requestAlphaAccess'), size: 's'}">
<span>{{'home.microsoftStoreDesc'|translate}}</span>
<button (click)="openMsStore()" style="width: 100%; margin-top: 10px" tuiButton >
<i class="fa-brands fa-microsoft" style="font-size: 25px"></i>
{{'home.microsoftStore'|translate}}
</button>
</ng-template>
<main>
<section id="hero">
<div id="imgScroller" [style]="'opacity:'+scrollerOpacity">
<img [src]="'/images/homepage/new_customi_'+theme+'.png'" alt="ARYA_BLUE_IMAGES">
<img [src]="'/images/homepage/new_dm_'+theme+'.png'" alt="ARYA_BLUE_IMAGES">
<img [src]="'/images/homepage/new_network_'+theme+'.png'" alt="ARYA_BLUE_IMAGES">
<img [src]="'/images/homepage/new_network_discovery_'+theme+'.png'" alt="ARYA_BLUE_IMAGES">
<img [src]="'/images/homepage/new_picture_stats_'+theme+'.png'" alt="ARYA_BLUE_IMAGES">
<img [src]="'/images/homepage/new_pictures_'+theme+'.png'" alt="ARYA_BLUE_IMAGES">
</div>
<div id="introducer">
<svg viewBox="0 0 350 60" width="1000px" height="200px">
<text class="logo" x="50%" y="80%" text-anchor="middle">Chatenium</text>
</svg>
<div id="quickFeatures">
<h1>
{{ 'aChatProgram'|translate }}
</h1>
<p class="chtn_desc">
{{ 'whatIsChtn'|translate }}
</p>
<button style="width: 350px" routerLink="/chat" tuiButton iconStart="@tui.globe">
{{'home.enterChtnOnWeb'|translate}}
</button>
<button style="width: 350px" appearance="secondary" (click)="openRoadmap()" tuiButton iconStart="@tui.chart-no-axes-gantt">
Roadmap
</button>
<p style="display: flex; align-items: center; gap: 5px;">
<tui-icon icon="@tui.mouse"></tui-icon>
{{ 'scrollDownForMore'|translate }}
</p>
</div>
</div>
</section>
<section class="detailedFeature">
<div class="style">
<h1>{{ 'home.chtn'|translate }} <span style="color: var(--tui-status-negative)"><tui-icon icon="@tui.circle-gauge"></tui-icon>
{{ 'home.adaptsToYou'|translate }}</span></h1>
<p>{{ 'home.adaptsToYouDesc'|translate }}</p>
<div class="cardList">
<div tuiCardLarge>
<tui-icon icon="@tui.globe" style="font-size: 80px"></tui-icon>
<h2>{{ 'home.chtnOnWeb'|translate }}</h2>
<p>{{ 'home.chtnOnWebDesc'|translate }}</p>
</div>
<div tuiCardLarge>
<fa-icon [icon]="faAndroid"></fa-icon>
<h2>{{ 'home.chtnOnAndroid'|translate }}</h2>
<div tuiBadge>{{'home.openBeta'|translate}}</div>
<p>{{ 'home.chtnOnAndroidDesc'|translate }}</p>
</div>
<div tuiCardLarge>
<fa-icon [icon]="faApple"></fa-icon>
<h2>{{ 'home.chtnOnApple'|translate }}</h2>
<div tuiBadge>{{'home.openBeta'|translate}}</div>
<p>{{ 'home.chtnOnAppleDesc'|translate }}</p>
</div>
<div tuiCardLarge>
<fa-icon [icon]="faWindows"></fa-icon>
<h2>{{ 'home.chtnOnWindows'|translate }}</h2>
<div tuiBadge>{{'home.openBeta'|translate}}</div>
<p>{{ 'home.chtnOnWindowsDesc'|translate }}</p>
</div>
<div tuiCardLarge>
<fa-icon [icon]="faLinux"></fa-icon>
<h2>{{ 'home.chtnOnLinux'|translate }}</h2>
<div tuiBadge>{{'home.openBeta'|translate}}</div>
<p>{{ 'home.chtnOnLinuxDesc'|translate }}</p>
</div>
<div tuiCardLarge>
<fa-icon [icon]="faRecordVinyl"></fa-icon>
<h2>{{ 'home.chtnEcho'|translate }}</h2>
<div tuiBadge>{{'home.openBeta'|translate}}</div>
<p>{{ 'home.chtnEchoDesc'|translate }}</p>
</div>
<div tuiCardLarge>
<fa-icon [icon]="faDesktop"></fa-icon>
<h2>{{ 'home.chtnReson'|translate }}</h2>
<div tuiBadge>{{'home.openBeta'|translate}}</div>
<p>{{ 'home.chtnResonDesc'|translate }}</p>
</div>
</div>
</div>
</section>
<section class="detailedFeature">
<div class="style">
<h1>{{ 'home.chtnIs'|translate }} <span style="color: var(--tui-status-positive)"><tui-icon icon="@tui.lock"></tui-icon>
{{ 'home.secure'|translate }}</span></h1>
<p>{{ 'home.secureDesc'|translate }}</p>
<div class="cardList">
<div tuiCardLarge>
<tui-icon icon="@tui.file"></tui-icon>
<h2>{{ 'home.secureCdn'|translate }}</h2>
<p>{{ 'home.secureCdnDesc'|translate }}</p>
</div>
<div tuiCardLarge>
<tui-icon icon="@tui.message-circle"></tui-icon>
<h2>{{ 'home.encryptedText'|translate }}</h2>
<p>{{ 'home.encryptedTextDesc'|translate }}</p>
</div>
<div tuiCardLarge>
<tui-icon icon="@tui.shredder"></tui-icon>
<h2>{{ 'home.zeroDataCollection'|translate }}</h2>
<p>{{ 'home.zeroDataCollectionDesc'|translate }}</p>
</div>
<div tuiCardLarge>
<tui-icon icon="@tui.phone"></tui-icon>
<h2>{{ 'home.secureCalling'|translate }}</h2>
<p>{{ 'home.secureCallingDesc'|translate }}</p>
</div>
<div tuiCardLarge>
<tui-icon icon="@tui.footprints"></tui-icon>
<h2>{{ 'home.zeroDigitalFootprint'|translate }}</h2>
<p>{{ 'home.zeroDigitalFootprintDesc'|translate }}</p>
</div>
</div>
</div>
</section>
<section class="detailedFeature" style="margin-bottom: 5%;">
<div class="style">
<h1>{{ 'home.weAreExcitedFor'|translate }} <span style="color: var(--tui-status-negative)"><tui-icon icon="@tui.heart"></tui-icon>
{{ 'home.you'|translate }}</span></h1>
<p>{{ 'home.weAreExcitedForYouDesc'|translate }}</p>
<div class="cardList">
<div tuiCardLarge>
<tui-icon icon="@tui.globe" style="font-size: 80px"></tui-icon>
<h2><a routerLink="/chat">{{ 'home.enterChtnOnWeb'|translate }}</a></h2>
<p>{{ 'home.enterChtnOnWebDesc'|translate }}</p>
</div>
<div tuiCardLarge>
<fa-icon [icon]="faGooglePlay"></fa-icon>
<h2>{{ 'home.downloadChtnOnAndroid'|translate }}</h2>
<div tuiBadge>{{'home.openBeta'|translate}}</div>
<p>{{'home.openBetaDesc'|translate}}</p>
<button (click)="androidBetaTestDialogOpen = true" tuiButton iconStart="@tui.door-open">{{'home.requestAlphaAccess'|translate}}</button>
</div>
<div tuiCardLarge>
<fa-icon [icon]="faAppStoreIos"></fa-icon>
<h2>{{ 'home.downloadChtnOnApple'|translate }}</h2>
<div tuiBadge>{{'home.openBeta'|translate}}</div>
<p>{{'home.openBetaDesc'|translate}}</p>
<button (click)="appleBetaTestDialogOpen = true" tuiButton iconStart="@tui.door-open">{{'home.requestAlphaAccess'|translate}}</button>
</div>
<div tuiCardLarge>
<fa-icon [icon]="faMicrosoft"></fa-icon>
<h2>{{ 'home.downloadOnWindows'|translate }}</h2>
<div tuiBadge>{{'home.openBeta'|translate}}</div>
<p>{{'home.openBetaDesc'|translate}}</p>
<button (click)="msBetaTestDialogOpen = true" tuiButton iconStart="@tui.door-open">{{'home.requestAlphaAccess'|translate}}</button>
</div>
</div>
</div>
</section>
</main>
<footer>
<h1>Chatenium</h1>
<span>2026 &copy;</span>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<button routerLink="/privacy" tuiButton iconStart="@tui.lock">
{{'home.privacyPolicy'|translate}}
</button>
<button routerLink="/tos" tuiButton iconStart="@tui.scroll">
{{'home.tos'|translate}}
</button>
<button (click)="openHelp()" tuiButton iconStart="@tui.badge-question-mark">
{{'home.help'|translate}}
</button>
<button (click)="openBlog()" tuiButton iconStart="@tui.scroll-text">
{{'home.blog'|translate}}
</button>
<button (click)="openAPISpecs()" tuiButton iconStart="@tui.braces">
{{'home.apiSpecs'|translate}}
</button>
</div>
</footer>

View File

@@ -0,0 +1,167 @@
* {
font-family: kinnBook, serif !important;
}
main {
#hero {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100vh;
#imgScroller {
position: fixed;
top: -500px;
left: 0;
transform: rotateX(35deg);
animation: scrollerAnim 30s infinite ease-in-out;
filter: blur(15px);
opacity: 0.5;
pointer-events: none;
img {
width: 100%;
}
}
#introducer {
z-index: 9999;
font-family: kinnBook, serif;
#quickFeatures {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
flex-direction: column;
h1 {
font-size: 25px;
}
p {
font-size: 18px;
}
.chtn_desc {
width: 500px;
}
}
}
@keyframes scrollerAnim {
0% {
transform: translateY(450px);
}
50% {
transform: translateY(-450px);
}
100% {
transform: translateY(450px);
}
}
}
.detailedFeature {
z-index: 9999;
padding: 50px 15% 50px 15%;
.style {
background: var(--tui-background-base-alt);
width: 100%;
padding: 20px 20px 20px 50px;
border-radius: 50px;
.cardList {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 10px;
div {
p {
font-size: 15px;
}
h2 {
display: flex;
align-items: center;
gap: 5px;
margin: 0;
}
i, fa-icon {
font-size: 75px;
}
a {
color: var(--tui-background-accent-1);
text-decoration: none;
}
}
}
}
h1 {
font-size: 50px;
font-weight: bolder;
}
tui-icon {
font-size: 50px;
}
p {
font-size: 20px;
}
}
}
.logo {
font-size: 55px;
stroke: var(--tui-background-accent-1);
fill: transparent;
font-family: kinnBook, serif !important;
font-weight: bold;
stroke-width: 2;
stroke-dasharray: 50 20; /* Dashes and gaps */
stroke-dashoffset: 0;
animation: moveStroke 2s linear infinite;
}
@keyframes moveStroke {
to {
stroke-dashoffset: -70; /* Negative offset moves the dashes endlessly */
}
}
footer {
height: 25svh;
width: 100vw;
background: var(--tui-background-base-alt);
padding: 50px;
p {
font-size: 25px;
font-family: kinnBook, serif;
font-weight: bold;
}
}
#cookieConsent {
position: fixed;
right: 25px;
bottom: 25px;
z-index: 9999;
width: 350px;
height: 300px;
h1 {
display: flex;
align-items: center;
gap: 10px;
margin: 0;
}
}

View File

@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Homepage } from './homepage';
describe('Homepage', () => {
let component: Homepage;
let fixture: ComponentFixture<Homepage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Homepage],
}).compileComponents();
fixture = TestBed.createComponent(Homepage);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,153 @@
import {Component, HostListener, inject} from '@angular/core';
import {Router, RouterLink} from '@angular/router';
import {TranslatePipe, TranslateService} from '@ngx-translate/core';
import {TUI_DARK_MODE, TuiAppearance, TuiButton, TuiDialog, TuiIcon} from '@taiga-ui/core';
import {TuiCardLarge} from '@taiga-ui/layout';
import {TuiBadge} from '@taiga-ui/kit';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {
faAndroid,
faApple,
faAppStoreIos,
faGooglePlay,
faLinux,
faMicrosoft,
faWindows
} from '@fortawesome/free-brands-svg-icons';
import {faDesktop, faRecordVinyl} from '@fortawesome/free-solid-svg-icons';
@Component({
selector: 'app-homepage',
imports: [
TranslatePipe,
TuiButton,
RouterLink,
TuiIcon,
TuiCardLarge,
TuiDialog,
TuiAppearance,
TuiBadge,
FaIconComponent
],
templateUrl: './homepage.html',
styleUrl: './homepage.scss',
})
export class Homepage {
public router = inject(Router);
translateService = inject(TranslateService)
darkMode = inject(TUI_DARK_MODE)
get theme(): string {
return this.darkMode() ? "dark" : "light"
}
androidBetaTestDialogOpen = false;
appleBetaTestDialogOpen = false;
msBetaTestDialogOpen = false;
cookieConsentOpen = false;
mainFeatureIndex = 0
features: { icon: string, title: string, color: string }[] = [
{
icon: "circle-gauge",
title: "fast",
color: "#ff3d32"
},
{
icon: "lock",
title: "secure",
color: "#22c55e"
},
{
icon: "speech",
title: "independent",
color: "#326fd1"
}
]
scrollerOpacity = 1;
@HostListener('window:scroll', ['$event'])
onScroll(event: Event) {
const scrollTop = window.scrollY;
const docHeight = document.getElementById("hero")?.clientHeight ?? 0;
let scrollPercent = scrollTop / docHeight;
if (scrollPercent > 1) scrollPercent = 1; // clamp at max
this.scrollerOpacity = 1 - scrollPercent; // invert
}
contact() {
const mail = 'mailto:personal@alms.hu';
const a = document.createElement('a');
a.href = mail;
a.click();
}
enterGGroup() {
window.open('https://groups.google.com/g/chatenium-closed-alpha-testing', '_blank');
}
openTestFlight() {
window.open('https://testflight.apple.com/join/ATGmZ8mx', '_blank')
}
openRoadmap() {
window.open('https://help.chatenium.hu/s/169154db-df3e-44cb-980d-2db1915ecdf8', '_blank')
}
openMsStore() {
window.open('https://apps.microsoft.com/detail/9p1xq5vb62b0?ocid=webpdpshare', '_blank')
}
openBlog() {
window.open('https://blog.chatenium.hu', '_blank')
}
openHelp() {
window.open('https://help.chatenium.hu/s/06a56637-0050-4332-b397-ea3ca8fa91a5', '_blank')
}
openAPISpecs() {
window.open('https://apispecs.chatenium.hu', '_blank')
}
gotoFunctions() {
location.href = "#functions"
}
stepFeature = () => {
console.log(this.features, this.mainFeatureIndex + 1, this.mainFeatureIndex);
if (this.features && this.features[this.mainFeatureIndex + 1] !== undefined) {
this.mainFeatureIndex++;
} else {
this.mainFeatureIndex = 0;
}
};
consent() {
localStorage.setItem("cookieConsentRead", "true")
this.cookieConsentOpen = false;
}
ngOnInit(): void {
const consentOk = localStorage.getItem("cookieConsentRead")
if (!consentOk) {
this.cookieConsentOpen = true
}
}
protected readonly window = window;
protected readonly scroll = scroll;
protected readonly localStorage = localStorage;
protected readonly faApple = faApple;
protected readonly faAndroid = faAndroid;
protected readonly faWindows = faWindows;
protected readonly faLinux = faLinux;
protected readonly faRecordVinyl = faRecordVinyl;
protected readonly faDesktop = faDesktop;
protected readonly faGooglePlay = faGooglePlay;
protected readonly faAppStoreIos = faAppStoreIos;
protected readonly faMicrosoft = faMicrosoft;
}

View File

@@ -0,0 +1,131 @@
<div style="padding: 15px;">
<button tuiButton iconStart="@tui.arrow-left" (click)="router.navigate(['/'])">Back to homepage</button>
<h1>
<tui-icon icon="@tui.lock"></tui-icon>
Chatenium's Privacy Policy
</h1>
<section tuiCardLarge style="background: var(--tui-background-base-alt)">
<h2>Welcome!</h2>
<span>This Privacy Policy shows how we store and protect your personal information through our services.</span><br>
<ul>
<li><b>We care about your privacy.</b> We are committed to creating an extra secure message platform where people's
data is safe and not shared.
</li>
<li><b>We do not sell your personal data.</b> We are a non-profit group. We do not share your personal data with
other companies.
</li>
<li><b>We collect the bare minimum of data we need at login.</b> When you log into your account we collect your browser's
version,
operating system's name and your selected language inside Chatenium for account security. (Logging out will clear stored data from our database)
</li>
</ul>
</section>
<section tuiCardLarge style="background: var(--tui-background-base-alt)">
<h2>All the information we collect</h2>
<h3><em>Information that you provide</em></h3>
<ul>
<li><b>Account information.</b> When you register to Chatenium you can either use social logins, or provide a
username, display name, password and an e-mail address. When using social logins, we collect your e-mail address
to manage your account and prevent duplicates.
</li>
<li><b>Content you upload or send.</b> This includes everything you upload to our services (media, message,
etc...).
</li>
</ul>
<h3><em>Information we collect automatically</em></h3>
<ul>
<li><b>Information about your device.</b> As mentioned earlier, we collect your browser's information, operating
system's information and the current selected language for security reasons.
</li>
</ul>
</section>
<section tuiCardLarge style="background: var(--tui-background-base-alt)">
<h2>How we handle client-side storage</h2>
<ul>
<li>
<b>Cookies.</b>
<ul>
<li>Your authorization token is stored here.</li>
<li>We do <strong>not</strong> use cookies for analytics or to track you.</li>
</ul>
</li>
<br>
<li>
<b>LocalStorage.</b> Used to enhance user experience by saving settings made by the user like:
<ul>
<li>Selected language</li>
<li>Selected microphone</li>
<li>Selected camera</li>
<li>Selected theme</li>
</ul>
</li>
<br>
<li>
<b>IndexedDB.</b> Used to boost performance and enable offline support via PWA.
<ul>
<li>Messages</li>
<li>Files</li>
</ul>
</li>
</ul>
</section>
<section tuiCardLarge style="background: var(--tui-background-base-alt)">
<h2>How we use your data</h2>
<ul>
<li><b>To provide you with services.</b> For example when you start a call, WebRTC collects your audio / camera /
screen information to make it work.
</li>
<li><b>To contact you.</b> We use your e-mail address to contact you whenever needed. E-mail can also be used for
account recovery.
</li>
<li><b>Data sharing.</b> We do <strong>not</strong> share <strong>any</strong> data with third parties.
</li>
</ul>
</section>
<section tuiCardLarge style="background: var(--tui-background-base-alt)">
<h2>How we protect your information.</h2>
<ul>
<li><b>Secure communication.</b> All data between the client and the server is encrypted using Transport Layer
Security (TLS).
</li>
<li><b>Secure data storage.</b> After communication is finished, the messages and the files are encrypted before
storing it.
</li>
</ul>
</section>
<section tuiCardLarge style="background: var(--tui-background-base-alt)">
<span>When editing or deleting a messages or a file we will permanently delete them from our database and client side caches.</span><br>
<br>
<em>Last modified: December 15. 2025</em><br>
<span>Chatenium &copy;</span>
</section>
</div>

View File

@@ -0,0 +1,3 @@
section {
margin: 10px;
}

View File

@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Privacy } from './privacy';
describe('Privacy', () => {
let component: Privacy;
let fixture: ComponentFixture<Privacy>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Privacy],
}).compileComponents();
fixture = TestBed.createComponent(Privacy);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,18 @@
import {Component, inject} from '@angular/core';
import {Router} from '@angular/router';
import {TuiCardLarge} from '@taiga-ui/layout';
import {TuiButton, TuiIcon} from '@taiga-ui/core';
@Component({
selector: 'app-privacy',
imports: [
TuiCardLarge,
TuiIcon,
TuiButton
],
templateUrl: './privacy.html',
styleUrl: './privacy.scss',
})
export class Privacy {
public router = inject(Router);
}

View File

@@ -9,6 +9,7 @@ import {SessionManager} from '@chatenium/chatenium-sdk/services/sessionManager';
import {ServiceManager} from '../service-manager';
import {IndexedDB} from '../storage/indexed-db';
import {PublicUserData} from '@chatenium/chatenium-sdk/domain/common.schema';
import {Router} from '@angular/router';
@Component({
selector: 'app-signin',
@@ -30,6 +31,7 @@ import {PublicUserData} from '@chatenium/chatenium-sdk/domain/common.schema';
})
export class SignIn implements OnInit {
indexedDb = inject(IndexedDB)
router = inject(Router)
service = new AuthService()
serviceManager = new ServiceManager()
@@ -68,6 +70,7 @@ export class SignIn implements OnInit {
displayName: resp.displayName,
}, resp.token)
console.log("Logged in")
this.router.navigate(["/chat"])
} catch (e) {
console.error(e)
this.signInForm.controls['password'].setErrors({incorrect: true})

112
src/app/tos/tos.html Normal file
View File

@@ -0,0 +1,112 @@
<div style="padding: 15px;">
<button tuiButton routerLink="/" iconStart="@tui.chevron-left">Back to homepage</button>
<h1>Terms Of Service</h1>
<section tuiCardLarge style="background: var(--tui-background-base-alt)">
<div>
<span>Welcome to Chatenium! These Terms of Service govern your use of our services. By registering an account, you
accept the Terms Of Service.</span><br>
<br>
<b>When we say "Chatenium", "we", "us", and "our" in these terms, we mean Chatenium.</b><br>
<br>
<b>"services" include our Website, API and native apps.</b><br>
</div>
</section>
<!-- ? Age -->
<section tuiCardLarge style="background: var(--tui-background-base-alt)">
<h2>Age requirements</h2>
<span>By accessing our services, you confirm that you are at least 13 years old. (May be different in your country).
If your underage, a legal guardian must agree our terms of services on your behalf.</span>
<h2>Your Chatenium account</h2>
<span>To access our services, you must register an account. To register you need to provide a username, display name
and an email address. If you use a social login, then providing a password is not required, but otherwise its
required.</span><br>
<br>
<span>You agree not to sell your account without our approval.</span>
</section>
<!-- ? Account -->
<!-- ? Uploaded content -->
<section tuiCardLarge style="background: var(--tui-background-base-alt)">
<h2>Uploaded content</h2>
<span>Everything you upload to our services (Including GIFs, videos, pictures, or other media), you retain ownership
and responsibility. We are not responsible for anything you upload.</span><br>
<br>
<span>Every file is encrypted and we do not edit or share it with others. You can delete them anytime by deleting the
message.</span><br>
<br>
<span>Users have an ability to keep all files on local machines (Including you and anyone you sent an attachment).
This option can be toggled in the Chat settings.</span>
<br>
<span>All media must not violate the rights of others, and must not contain any harmful or malicious content
including but not limited to:</span>
<ul>
<li>Malicious malware</li>
<li>Pornographic content</li>
</ul>
</section>
<section tuiCardLarge style="background: var(--tui-background-base-alt)">
<h2>Messages</h2>
<span>You agree to use our services in a lawful and respectful manner. You may not engage in any conduct that is harmful,
offensive, or violates the rights of others. Prohibited activities include but are not limited to:</span>
<ul>
<li>Harassment, bullying, or hate speech</li>
<li>Distribution of illegal or inappropriate content</li>
<li>Spamming, phishing, or other forms of unauthorized solicitation</li>
<li>Impersonation of others or misrepresentation of your identity</li>
</ul>
<br>
<span>We reserve the right to block or delete any message at any time.</span>
</section>
<section tuiCardLarge style="background: var(--tui-background-base-alt)">
<h2>Content on Chatenium</h2>
<span>Our services (Including the Website, API, Designs, Native apps, and anything writtin by us) is prohibited to
clone / copy.</span>
<h2>Other people's content</h2>
<span>Our services might provide you access to other people's content. You may not use it without the owner's
consent.</span>
<h2>Our software</h2>
<span>You may not distribute, sell, lease or sublicense any of our software and you also may not reverse engineer or
decompile any of our software without written consent or if applicable law permits it.</span>
<h2>Namings</h2>
<span>The following features:</span>
<ul>
<li>
Network name
<ul>
<li>Channel name</li>
<li>Category names</li>
</ul>
</li>
<li>Username</li>
<li>Display name</li>
<li>Public picture's title and description</li>
</ul>
<span>cannot include names that include</span>
<li>NFSW content</li>
<li>Swear words</li>
<h2>Termination</h2>
<span>If you want to terminate your account, you can do it anytime in the settings. We may terminate your account
anytime if you violate or terms of services.</span><br>
<br>
</section>
<section tuiCardLarge style="background: var(--tui-background-base-alt)">
<div>
<em>Last modified: December 15. 2025</em><br>
<span>Chatenium &copy;</span>
</div>
</section>
</div>

3
src/app/tos/tos.scss Normal file
View File

@@ -0,0 +1,3 @@
section {
margin: 10px;
}

22
src/app/tos/tos.spec.ts Normal file
View File

@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Tos } from './tos';
describe('Tos', () => {
let component: Tos;
let fixture: ComponentFixture<Tos>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Tos],
}).compileComponents();
fixture = TestBed.createComponent(Tos);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

19
src/app/tos/tos.ts Normal file
View File

@@ -0,0 +1,19 @@
import {Component, inject} from '@angular/core';
import {Router, RouterLink} from '@angular/router';
import {TuiCardLarge} from '@taiga-ui/layout';
import {TuiButton, TuiIcon} from '@taiga-ui/core';
@Component({
selector: 'app-tos',
imports: [
TuiCardLarge,
TuiIcon,
TuiButton,
RouterLink
],
templateUrl: './tos.html',
styleUrl: './tos.scss',
})
export class TOS {
public router = inject(Router);
}

View File

@@ -1,4 +1,5 @@
export const environment = {
version: "3.0-beta2",
api_url: "http://localhost:3000",
cdn_url: "http://localhost:4000",
ws_url: "ws://localhost:3000",

View File

@@ -1,4 +1,5 @@
export const environment = {
version: "3.0-beta2",
api_url: "https://api.chatenium.hu",
cdn_url: "https://cdn.chatenium.hu",
ws_url: "wss://cdn.chatenium.hu",

View File

@@ -2,9 +2,10 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>ChateniumOnWeb</title>
<title>Chatenium</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>

View File

@@ -44,3 +44,14 @@ body {
src: url("/Onest-SemiBold.ttf");
font-weight: 600;
}
@font-face {
font-family: "kinnBook";
src: url("/Kinn-Book.ttf");
}
@font-face {
font-family: "kinnBook";
src: url("/Kinn-Heavy.ttf");
font-weight: bold;
}