3.0 Beta 2
82
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
BIN
public/Kinn-Heavy.ttf
Normal file
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/images/homepage/new_customi_dark.png
Normal file
|
After Width: | Height: | Size: 414 KiB |
BIN
public/images/homepage/new_customi_light.png
Normal file
|
After Width: | Height: | Size: 444 KiB |
BIN
public/images/homepage/new_dm_dark.png
Normal file
|
After Width: | Height: | Size: 391 KiB |
BIN
public/images/homepage/new_dm_light.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
public/images/homepage/new_network_dark.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
public/images/homepage/new_network_discovery_dark.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/images/homepage/new_network_discovery_light.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/images/homepage/new_network_light.png
Normal file
|
After Width: | Height: | Size: 274 KiB |
BIN
public/images/homepage/new_picture_stats_dark.png
Normal file
|
After Width: | Height: | Size: 4.4 MiB |
BIN
public/images/homepage/new_picture_stats_light.png
Normal file
|
After Width: | Height: | Size: 4.4 MiB |
BIN
public/images/homepage/new_pictures_dark.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
public/images/homepage/new_pictures_light.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
public/images/logo/logo.png
Executable file
|
After Width: | Height: | Size: 49 KiB |
BIN
public/images/logo/logo_beta.png
Executable file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/images/logo/logo_short.png
Executable file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/images/logo/logo_short_dark.png
Executable file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/images/logo/logo_short_dark_1024_1024.png
Executable file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/images/logo/logo_short_dark_120_120.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/images/logo/logo_short_old.png
Executable file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/images/logo/logoold.png
Executable file
|
After Width: | Height: | Size: 46 KiB |
@@ -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},
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"/>
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
<div style="background: var(--tui-background-neutral-1); height: 50px; display: flex; align-items: center; padding: 0 10px">
|
||||
<span>{{ file.name }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px; display: flex; gap: 10px; width: 100%">
|
||||
<tui-textfield style="width: 100%">
|
||||
@@ -37,6 +49,25 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<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 id="close" tuiButton appearance="flat" (click)="viewModel().editingMessage.set(null); viewModel().message.set('');">
|
||||
<tui-icon icon="@tui.x"/>
|
||||
</button>
|
||||
</div>
|
||||
<div id="message-box" [style]="'border-radius:'+messageBoxRadius+'px;'">
|
||||
<div class="items-left">
|
||||
<button tuiButton appearance="flat" (click)="uplInput.click()">
|
||||
@@ -44,18 +75,19 @@
|
||||
<input #uplInput type="file" (change)="handleFileInput($event)" multiple hidden/>
|
||||
</button>
|
||||
|
||||
<button tuiButton appearance="flat">
|
||||
<button tuiButton appearance="flat" disabled>
|
||||
<tui-icon icon="@tui.cloud-sync"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="items-middle">
|
||||
<textarea [style]="'height:'+textareaHeight+'px;'" #message (input)="onTextAreaInput(message)" [(ngModel)]="viewModel().message"></textarea>
|
||||
<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 tuiButton appearance="flat" (click)="viewModel().onMessageSend(message.value, null)">
|
||||
<button [disabled]="message.value.trim() == ''" tuiButton appearance="flat" (click)="viewModel().onMessageSend(message.value, null)">
|
||||
<tui-icon icon="@tui.send"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -48,7 +48,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 26px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
&.shown {
|
||||
transform: translateY(0);
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
#message-box {
|
||||
z-index: 5;
|
||||
transition: all 0.2s ease-in-out;
|
||||
width: 60%;
|
||||
background: var(--tui-background-base-alt);
|
||||
@@ -117,3 +159,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -10,23 +10,57 @@
|
||||
<div class="above">
|
||||
<span>{{ message.sent_at.T * 1000 | date: 'HH:mm' }}</span>
|
||||
</div>
|
||||
<div class="bubble">
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
max-width: 50%;
|
||||
min-width: 250px;
|
||||
min-height: 40px;
|
||||
max-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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="">
|
||||
|
||||
@@ -15,4 +15,5 @@ export class Oimg {
|
||||
width = input("")
|
||||
src = input("")
|
||||
radius = input(15)
|
||||
objectFit = input("cover")
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
<div id="buttons">
|
||||
<button tuiButton appearance="icon"
|
||||
(click)="videoPlaying ? video.pause() : video.play();">
|
||||
(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>
|
||||
<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="right">
|
||||
<button
|
||||
type="button"
|
||||
tuiButton appearance="icon"
|
||||
tuiDropdown
|
||||
tuiDropdownAppearance="neutral"
|
||||
tuiDropdownAuto
|
||||
tuiDropdownDirection="top"
|
||||
tuiDropdownLimitWidth="fixed"
|
||||
tuiIconButton
|
||||
@@ -50,19 +43,16 @@
|
||||
} @else {
|
||||
<tui-icon icon="@tui.volume"></tui-icon>
|
||||
}
|
||||
|
||||
</button>
|
||||
<input
|
||||
*tuiDropdown
|
||||
tuiSlider
|
||||
type="range"
|
||||
value="100"
|
||||
(input)="setVolume(volume)"
|
||||
[(ngModel)]="volume"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button tuiButton appearance="icon"
|
||||
(mouseover)="showVolRange = false"
|
||||
(click)="videoFullscreen ? exitFullScreen() : player.requestFullscreen(); videoFullscreen = !videoFullscreen">
|
||||
@if (videoFullscreen) {
|
||||
<tui-icon icon="@tui.minimize"></tui-icon>
|
||||
@@ -72,6 +62,13 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
} @else {
|
||||
@@ -79,7 +76,8 @@
|
||||
@if (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>
|
||||
|
||||
@@ -36,15 +36,30 @@
|
||||
|
||||
#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: flex;
|
||||
flex-direction: column;
|
||||
|
||||
#info {
|
||||
display: grid;
|
||||
grid-template-columns: 50px 50px 1fr 50px 50px;
|
||||
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;
|
||||
@@ -66,6 +81,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.player_preview {
|
||||
max-width: 400px;
|
||||
@@ -92,6 +108,7 @@ tui-dropdown[data-appearance] {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
backdrop-filter: blur(1rem);
|
||||
position: fixed;
|
||||
|
||||
[tuiSlider] {
|
||||
position: absolute;
|
||||
|
||||
@@ -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
|
||||
],
|
||||
|
||||
17
src/app/guards/auth-needed-guard.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
24
src/app/guards/auth-needed-guard.ts
Normal 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;
|
||||
};
|
||||
17
src/app/guards/no-auth-guard.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
24
src/app/guards/no-auth-guard.ts
Normal 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;
|
||||
};
|
||||
268
src/app/homepage/homepage.html
Normal 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 ©</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>
|
||||
167
src/app/homepage/homepage.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
22
src/app/homepage/homepage.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
153
src/app/homepage/homepage.ts
Normal 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;
|
||||
}
|
||||
131
src/app/privacy/privacy.html
Normal 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 ©</span>
|
||||
</section>
|
||||
</div>
|
||||
3
src/app/privacy/privacy.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
section {
|
||||
margin: 10px;
|
||||
}
|
||||
22
src/app/privacy/privacy.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
18
src/app/privacy/privacy.ts
Normal 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);
|
||||
}
|
||||
@@ -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
@@ -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 ©</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
3
src/app/tos/tos.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
section {
|
||||
margin: 10px;
|
||||
}
|
||||
22
src/app/tos/tos.spec.ts
Normal 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
@@ -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);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||