diff --git a/package-lock.json b/package-lock.json index a2673cf..d2f2314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", "@angular/service-worker": "^21.2.0", - "@chatenium/chatenium-sdk": "^1.2.0", + "@chatenium/chatenium-sdk": "^1.2.2", "@fortawesome/angular-fontawesome": "^4.0.0", "@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.1.0", @@ -1011,9 +1011,9 @@ } }, "node_modules/@chatenium/chatenium-sdk": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.2.0.tgz", - "integrity": "sha512-myNvjsMbxRji6MEHufhgEbCmltLL+Azb2UUA+ovUDuf5+LcdYiLYcIF4A4/NAxRVYx3IcPB4uOVF2f5SqW2sLA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.2.2.tgz", + "integrity": "sha512-LVaw46XAf3gUXRtTjvUPA15SG+iqmOT9XFdeyc77X24vqv0f2IkvepnFqb/o3SVJ+7JRpibUx4QwnOmLy1enEg==", "dependencies": { "@faker-js/faker": "^10.4.0", "axios": "^1.14.0", diff --git a/package.json b/package.json index a31fcd5..c752562 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", "@angular/service-worker": "^21.2.0", - "@chatenium/chatenium-sdk": "^1.2.0", + "@chatenium/chatenium-sdk": "^1.2.2", "@fortawesome/angular-fontawesome": "^4.0.0", "@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.1.0", diff --git a/public/i18n/en.json b/public/i18n/en.json index 610661c..4f107e0 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -97,6 +97,41 @@ } }, "chat": { + "userSettingsDialog": { + "label": "Settings", + "options": { + "security": "Privacy & Security", + "profile": "Profile", + "themes": "Themes", + "sessions": "Sessions", + "storage": "Data and Storage" + }, + "security": { + "changePasswordDialog": { + "label": "Change password", + "labelSet": "Set password", + "labelRemove": "Remove password", + "currentPassword": "Current password", + "newPassword": "New password", + "newPasswordRepeat": "Repeat new password" + }, + "label": "Keep your account safe by using as much sign in methods as possible. Also check your credentials regularly to keep them up to date.", + "password": "Password", + "set": "Set", + "notSet": "Not set", + "changePassword": "Change password", + "phoneNumber": "Phone number", + "email": "E-mail address", + "removePassword": "Remove password", + "changePhone": "Change phone number", + "changeMail": "Change e-mail address", + "removePhone": "Remove phone number", + "removeMail": "Remove e-mail address", + "setPassword": "Set password", + "setPhone": "Set phone number", + "setMail": "Set e-mail address" + } + }, "tabBar": { "tab1": "Chats", "tab2": "Networks", diff --git a/src/app/chat/chat.html b/src/app/chat/chat.html index f262ec7..9f4dc77 100644 --- a/src/app/chat/chat.html +++ b/src/app/chat/chat.html @@ -15,6 +15,10 @@ + + + + @if (serviceManager.currentSession() == null) {
diff --git a/src/app/chat/chat.scss b/src/app/chat/chat.scss index dd49e0c..2842523 100644 --- a/src/app/chat/chat.scss +++ b/src/app/chat/chat.scss @@ -85,3 +85,8 @@ } } } + +::ng-deep tui-dialog[data-appearance~=big] { + height: 90svh; + width: 90svw !important; +} diff --git a/src/app/chat/chat.ts b/src/app/chat/chat.ts index 5ff8f59..a66c8d3 100644 --- a/src/app/chat/chat.ts +++ b/src/app/chat/chat.ts @@ -13,6 +13,9 @@ import {environment} from '../../environments/environment'; import {TuiTabBarComponent, TuiTabBarItem} from '@taiga-ui/addon-mobile'; import {NetworkList} from './network-list/network-list'; import {PictureList} from './picture-list/picture-list'; +import {UserSettings} from './user-settings/user-settings'; +import {AuthService} from '@chatenium/chatenium-sdk/services/authService'; +import {UserService} from '@chatenium/chatenium-sdk/services/userService'; @Component({ selector: 'app-chat', @@ -31,7 +34,8 @@ import {PictureList} from './picture-list/picture-list'; TuiTabBarComponent, TuiTabBarItem, NetworkList, - PictureList + PictureList, + UserSettings ], templateUrl: './chat.html', styleUrl: './chat.scss', @@ -43,6 +47,7 @@ export class Chat implements OnInit { router = inject(Router) routerOutletActive = signal(false) + userSettingsOpen = signal(true) navigationActiveIndex = 0 // Mobile navigation // @@ -81,7 +86,19 @@ export class Chat implements OnInit { this.indexedDb.openDatabase().then(async () => { const session = await this.serviceManager.sessionManager.loadPreferredSession() this.serviceManager.currentSession.set(session) + this.serviceManager.currentSessionHandler = new UserService(session.userData.userid, session.token, this.indexedDb.getApi()) await WebSocketHandler.getInstance().connect(session.userData.userid, session.token) + try { + console.log("Updating sessions...") + const sessions = await this.serviceManager.sessionManager.loadSessions() + console.log("Updating sessions: saved sessions loaded...") + await this.serviceManager.sessionManager.updateSessions(sessions) + const session = await this.serviceManager.sessionManager.loadPreferredSession() + console.log("Updating sessions succeeded: updating sessions...") + this.serviceManager.currentSession.set(session) + } catch (e) { + console.warn("Session update failed, skipping...", e) + } }) setTimeout(() => { diff --git a/src/app/chat/user-settings/security/password/password.html b/src/app/chat/user-settings/security/password/password.html new file mode 100644 index 0000000..dfdbc4c --- /dev/null +++ b/src/app/chat/user-settings/security/password/password.html @@ -0,0 +1,80 @@ +
+ +
+ @if (serviceManager.currentSession()!.userData.passwordSet) { + + + + + + @if (changePasswordForm.controls['currentPassword'].dirty) { + @if (changePasswordForm.controls['currentPassword'].hasError("required")) { + + } + + @if (changePasswordForm.controls['currentPassword'].hasError("incorrect")) { + + } + } + } + + @if (!changePasswordRemoveMode()) { + + + + + + + + + + } +
+ +
+ +
+
+ +
+ + {{ "chat.userSettingsDialog.security.password"|translate }} + + @if (serviceManager.currentSession()!.userData.passwordSet) { +
{{ 'chat.userSettingsDialog.security.set'|translate }} +
+ } @else { +
{{ 'chat.userSettingsDialog.security.notSet'|translate }} +
+ } +
+ +
+ + + @if (serviceManager.currentSession()!.userData.passwordSet) { + + } +
+
diff --git a/src/app/chat/user-settings/security/password/password.scss b/src/app/chat/user-settings/security/password/password.scss new file mode 100644 index 0000000..b9bc65e --- /dev/null +++ b/src/app/chat/user-settings/security/password/password.scss @@ -0,0 +1,3 @@ +:host { + width: 100%; +} diff --git a/src/app/chat/user-settings/security/password/password.spec.ts b/src/app/chat/user-settings/security/password/password.spec.ts new file mode 100644 index 0000000..324f047 --- /dev/null +++ b/src/app/chat/user-settings/security/password/password.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Password } from './password'; + +describe('Password', () => { + let component: Password; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Password], + }).compileComponents(); + + fixture = TestBed.createComponent(Password); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/chat/user-settings/security/password/password.ts b/src/app/chat/user-settings/security/password/password.ts new file mode 100644 index 0000000..1982ccc --- /dev/null +++ b/src/app/chat/user-settings/security/password/password.ts @@ -0,0 +1,91 @@ +import {Component, inject, signal} from '@angular/core'; +import {ServiceManager} from '../../../../service-manager'; +import { + AbstractControl, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + Validators +} from '@angular/forms'; +import {TranslatePipe} from '@ngx-translate/core'; +import {TuiBadge, TuiButtonLoading} from '@taiga-ui/kit'; +import { + TuiButton, + TuiDialog, + TuiErrorComponent, + TuiIcon, + TuiInputDirective, + TuiLabel, + TuiTextfieldComponent +} from '@taiga-ui/core'; + +@Component({ + selector: 'user-settings-security-password', + imports: [ + FormsModule, + ReactiveFormsModule, + TranslatePipe, + TuiBadge, + TuiButton, + TuiButtonLoading, + TuiDialog, + TuiErrorComponent, + TuiIcon, + TuiInputDirective, + TuiLabel, + TuiTextfieldComponent + ], + templateUrl: './password.html', + styleUrl: './password.scss', +}) +export class Password { + serviceManager = inject(ServiceManager) + + changePasswordDialogOpen = signal(false) + changePasswordRemoveMode = signal(false) + changePasswordPending = signal(false) + + changePasswordForm = new FormGroup({ + currentPassword: new FormControl(""), + newPassword: new FormControl(""), + newPasswordRepeat: new FormControl("") + }, { + validators: [(group) => this.chkPassMatch(group)] + }) + + openChangePasswordDialog(modeRemove: boolean) { + this.changePasswordDialogOpen.set(true) + this.changePasswordRemoveMode.set(modeRemove) + this.changePasswordForm.controls["currentPassword"].clearValidators() + this.changePasswordForm.controls["newPassword"].clearValidators() + this.changePasswordForm.controls["newPasswordRepeat"].clearValidators() + if (!modeRemove) { + this.changePasswordForm.controls["newPassword"].setValidators([Validators.required]) + this.changePasswordForm.controls["newPasswordRepeat"].setValidators([Validators.required]) + this.changePasswordForm.controls["newPassword"].updateValueAndValidity() + this.changePasswordForm.controls["newPasswordRepeat"].updateValueAndValidity() + } + + this.changePasswordForm.controls["currentPassword"].setValidators([Validators.required]) + this.changePasswordForm.controls["currentPassword"].updateValueAndValidity() + } + + async changePassword(currentPassword: string | null, newPassword: string | null) { + this.changePasswordPending.set(true) + const service = this.serviceManager.currentSessionHandler + if (service) { + try { + await service.changePassword(newPassword ?? "", currentPassword ?? "") + } catch (e) { + this.changePasswordForm.controls["currentPassword"].setErrors({incorrect: true}) + } + } + } + + chkPassMatch(group: AbstractControl): ValidationErrors | null { + return this.changePasswordRemoveMode() ? null : group.value.newPassword == group.value.newPasswordRepeat + ? null : {passMatchError: true}; + } +} diff --git a/src/app/chat/user-settings/security/security.html b/src/app/chat/user-settings/security/security.html new file mode 100644 index 0000000..d523a05 --- /dev/null +++ b/src/app/chat/user-settings/security/security.html @@ -0,0 +1,76 @@ +

+ + {{ "chat.userSettingsDialog.security.label"|translate }} +

+
+ + +
+
+ + {{ "chat.userSettingsDialog.security.email"|translate }} + + @if (serviceManager.currentSession()!.userData.phoneSet) { +
{{ 'chat.userSettingsDialog.security.set'|translate }} +
+ } @else { +
{{ 'chat.userSettingsDialog.security.notSet'|translate }} +
+ } +
+ +
+ + + @if (serviceManager.currentSession()!.userData.emailSet) { + + } +
+
+
+
+ + {{ "chat.userSettingsDialog.security.phoneNumber"|translate }} + + @if (serviceManager.currentSession()!.userData.phoneSet) { +
{{ 'chat.userSettingsDialog.security.set'|translate }} +
+ } @else { +
{{ 'chat.userSettingsDialog.security.notSet'|translate }} +
+ } +
+ +
+ + + @if (serviceManager.currentSession()!.userData.phoneSet) { + + } +
+
+
diff --git a/src/app/chat/user-settings/security/security.scss b/src/app/chat/user-settings/security/security.scss new file mode 100644 index 0000000..69c3bc1 --- /dev/null +++ b/src/app/chat/user-settings/security/security.scss @@ -0,0 +1,33 @@ +#options { + display: flex; + flex-direction: column; + + ::ng-deep .option { + width: 100%; + height: 60px; + background: var(--tui-background-base-alt); + padding: 15px; + display: grid; + grid-template-columns: 1fr 1fr; + align-items: center; + gap: 10px; + + ::ng-deep header { + display: flex; + gap: 5px; + align-items: center; + } + + ::ng-deep main { + display: flex; + align-items: center; + justify-content: end; + display: flex; + gap: 5px; + + button { + height: 35px; + } + } + } +} diff --git a/src/app/chat/user-settings/security/security.spec.ts b/src/app/chat/user-settings/security/security.spec.ts new file mode 100644 index 0000000..a9eec9e --- /dev/null +++ b/src/app/chat/user-settings/security/security.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Security } from './security'; + +describe('Security', () => { + let component: Security; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Security], + }).compileComponents(); + + fixture = TestBed.createComponent(Security); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/chat/user-settings/security/security.ts b/src/app/chat/user-settings/security/security.ts new file mode 100644 index 0000000..5e56376 --- /dev/null +++ b/src/app/chat/user-settings/security/security.ts @@ -0,0 +1,39 @@ +import {Component, inject, signal} from '@angular/core'; +import {TuiButton, TuiDialog, TuiErrorComponent, TuiGroup, TuiIcon, TuiInputDirective} from '@taiga-ui/core'; +import {TranslatePipe} from '@ngx-translate/core'; +import {ServiceManager} from '../../../service-manager'; +import {JsonPipe} from '@angular/common'; +import {TuiBadge, TuiButtonLoading, TuiInputColor} from '@taiga-ui/kit'; +import { + AbstractControl, + FormControl, + FormGroup, + ReactiveFormsModule, + ValidationErrors, + Validators +} from '@angular/forms'; +import {Password} from './password/password'; + +@Component({ + selector: 'user-settings-security', + imports: [ + TuiGroup, + TranslatePipe, + TuiIcon, + JsonPipe, + TuiBadge, + TuiButton, + TuiDialog, + ReactiveFormsModule, + TuiInputColor, + TuiInputDirective, + TuiButtonLoading, + TuiErrorComponent, + Password + ], + templateUrl: './security.html', + styleUrl: './security.scss', +}) +export class Security { + serviceManager = inject(ServiceManager) +} diff --git a/src/app/chat/user-settings/user-settings.html b/src/app/chat/user-settings/user-settings.html new file mode 100644 index 0000000..ea17621 --- /dev/null +++ b/src/app/chat/user-settings/user-settings.html @@ -0,0 +1,24 @@ + + +
+ @switch (selectedOption) { + @case ("security") { + + } + } +
diff --git a/src/app/chat/user-settings/user-settings.scss b/src/app/chat/user-settings/user-settings.scss new file mode 100644 index 0000000..486ed67 --- /dev/null +++ b/src/app/chat/user-settings/user-settings.scss @@ -0,0 +1,50 @@ +:host { + height: 100%; + width: 100%; + display: grid; + grid-template-columns: 300px 1fr; + + aside { + background-color: var(--tui-background-base-alt); + height: 100%; + width: 100%; + border-radius: 1.5rem 0 0 1.5rem; + + header { + display: flex; + justify-content: center; + align-items: center; + gap: 5px; + position: relative; + + tui-icon { + font-size: 35px; + } + + button { + position: absolute; + right: 25px; + } + } + + #options { + display: flex; + flex-direction: column; + gap: 1px; + padding: 15px; + + button { + width: 100%; + display: flex; + justify-content: start; + } + } + } + + main { + background: var(--tui-background-neutral-2); + width: 100%; + border-radius: 0 1.5rem 1.5rem 0; + padding: 15px; + } +} diff --git a/src/app/chat/user-settings/user-settings.spec.ts b/src/app/chat/user-settings/user-settings.spec.ts new file mode 100644 index 0000000..eba8fb5 --- /dev/null +++ b/src/app/chat/user-settings/user-settings.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserSettings } from './user-settings'; + +describe('UserSettings', () => { + let component: UserSettings; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserSettings], + }).compileComponents(); + + fixture = TestBed.createComponent(UserSettings); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/chat/user-settings/user-settings.ts b/src/app/chat/user-settings/user-settings.ts new file mode 100644 index 0000000..9a495f5 --- /dev/null +++ b/src/app/chat/user-settings/user-settings.ts @@ -0,0 +1,42 @@ +import { Component } from '@angular/core'; +import {TranslatePipe} from '@ngx-translate/core'; +import {TuiButton, TuiButtonX, TuiIcon} from '@taiga-ui/core'; +import {Security} from './security/security'; + +@Component({ + selector: 'user-settings', + imports: [ + TranslatePipe, + TuiIcon, + TuiButtonX, + TuiButton, + Security + ], + templateUrl: './user-settings.html', + styleUrl: './user-settings.scss', +}) +export class UserSettings { + selectedOption = "security" + options = [ + { + icon: "shield", + name: "security", + }, + { + icon: "users", + name: "profile", + }, + { + icon: "paint-roller", + name: "themes", + }, + { + icon: "computer", + name: "sessions" + }, + { + icon: "hard-drive", + name: "storage" + } + ] +} diff --git a/src/app/service-manager.ts b/src/app/service-manager.ts index 0aaf9a1..eef7fd8 100644 --- a/src/app/service-manager.ts +++ b/src/app/service-manager.ts @@ -17,6 +17,8 @@ import {PictureService} from '@chatenium/chatenium-sdk/services/pictureService'; import {Album} from '@chatenium/chatenium-sdk/domain/pictureService.schema'; import {PublicUserData} from '@chatenium/chatenium-sdk/domain/common.schema'; import {MessagesViewModel} from './chat/elements/messages/messages-viewmodel'; +import {AuthService} from '@chatenium/chatenium-sdk/services/authService'; +import {UserService} from '@chatenium/chatenium-sdk/services/userService'; @Injectable({ providedIn: 'root', @@ -28,6 +30,7 @@ export class ServiceManager { sessionManager = new SessionManager(this.database.getApi(), this.keyring.getApi(), this.keyValue.getApi()) currentSession = signal(null) + currentSessionHandler: UserService | null = null chatService: ChatService | null = null // Initialized in picture-list.ts chatsStatus = signal(LoadStatus.loading) diff --git a/src/app/storage/indexed-db.ts b/src/app/storage/indexed-db.ts index 1e6f9de..a4686e8 100644 --- a/src/app/storage/indexed-db.ts +++ b/src/app/storage/indexed-db.ts @@ -147,7 +147,7 @@ export class IndexedDB { delete(storeName: string, key: string): Promise { return new Promise((resolve, reject) => { if (this.db) { - const transaction = this.db.transaction([storeName], "readonly"); + const transaction = this.db.transaction([storeName], "readwrite"); const store = transaction.objectStore(storeName); const request = store.delete(key);