diff --git a/package-lock.json b/package-lock.json index d2f2314..41c0ed6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,9 +27,11 @@ "@taiga-ui/addon-table": "^5.1.0", "@taiga-ui/cdk": "^5.1.0", "@taiga-ui/core": "^5.1.0", + "@taiga-ui/i18n": "^5.2.0", "@taiga-ui/icons": "^5.1.0", "@taiga-ui/kit": "^5.1.0", "@taiga-ui/layout": "^5.1.0", + "libphonenumber-js": "^1.12.41", "ngx-cookie-service": "^21.3.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", @@ -4384,11 +4386,10 @@ "peer": true }, "node_modules/@taiga-ui/i18n": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-5.1.0.tgz", - "integrity": "sha512-NqCo1fK95w6aXHkvIZ3aqZOA2z+CnvD/eEEXZjbCs/Ik6QfWmGP8GbS5uwb7SzqNZz9QiVaJi5RNZtoInHf/tQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-5.2.0.tgz", + "integrity": "sha512-dyjp5hqDR272EPX+aQVtCXKiFKEUggdf1Y/Gcdt4JGZ0WqYR5n5CjP1EoKRG3GQcgd1mCSXnAt2BQJpujGcaHw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": ">=2.8.1" }, @@ -7061,8 +7062,7 @@ "version": "1.12.41", "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.41.tgz", "integrity": "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/listr2": { "version": "9.0.5", diff --git a/package.json b/package.json index c752562..6c32e8d 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,11 @@ "@taiga-ui/addon-table": "^5.1.0", "@taiga-ui/cdk": "^5.1.0", "@taiga-ui/core": "^5.1.0", + "@taiga-ui/i18n": "^5.2.0", "@taiga-ui/icons": "^5.1.0", "@taiga-ui/kit": "^5.1.0", "@taiga-ui/layout": "^5.1.0", + "libphonenumber-js": "^1.12.41", "ngx-cookie-service": "^21.3.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", diff --git a/public/i18n/en.json b/public/i18n/en.json index 3762487..c5ebb78 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -1,5 +1,5 @@ { - "version": "Chatenium Nexum 3.0 Beta 8 (April 17, 2026)", + "version": "Chatenium Nexum 3.0 Beta 9 (April 19, 2026)", "ok": "Ok", "back": "Back", "aChatProgram": "A messaging platform that you can trust.", @@ -111,6 +111,7 @@ "label": "Change password", "labelSet": "Set password", "labelRemove": "Remove password", + "warn": "After you change your password, you will be logged out of all your sessions.", "currentPassword": "Current password", "newPassword": "New password", "newPasswordRepeat": "Repeat new password" @@ -119,11 +120,22 @@ "label": "Change e-mail address", "labelSet": "Set e-mail address", "labelRemove": "Remove e-mail address", + "warn": "After you change your e-mail address, you will be logged out of all your sessions.", "newMail": "New e-mail address", "currentPassword": "Current password", "oldCode": "Code sent to the old e-mail address", "newCode": "Code sent to the new e-mail address" }, + "changePhoneDialog": { + "label": "Change phone number", + "labelSet": "Set phone number", + "labelRemove": "Remove phone number", + "warn": "After you change your phone number, you will be logged out of all your sessions.", + "newPhone": "New phone number", + "currentPassword": "Current password", + "oldCode": "Code sent to the old phone number", + "newCode": "Code sent to the new phone number" + }, "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", @@ -206,12 +218,8 @@ "changeLogDialog": { "label": "Chatenium has been updated", "changeLog": { - "1": "Optimized the loading of chats and networks", - "2": "Added skeleton loaders for messages inside DMs and channels", - "3": "Added tabs in networks", - "4": "Added option to edit network name", - "5": "Added option to change network visibility", - "6": "Fixed overflow issues in the mobile UI" + "1": "Started implementing user settings, Privacy & Security is now available.", + "2": "Enabled Chatenium Pictures on mobile devices." } }, "chatnav": { diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 90601d0..05a61f8 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -6,6 +6,8 @@ import {routes} from './app.routes'; import {provideTranslateService} from '@ngx-translate/core'; import {provideTranslateHttpLoader} from '@ngx-translate/http-loader'; import {provideServiceWorker} from '@angular/service-worker'; +import {tuiInputPhoneInternationalOptionsProvider} from '@taiga-ui/kit'; +import {defer} from 'rxjs'; export const appConfig: ApplicationConfig = { providers: [ @@ -25,6 +27,11 @@ export const appConfig: ApplicationConfig = { provideServiceWorker('ngsw-worker.js', { enabled: !isDevMode(), registrationStrategy: 'registerWhenStable:30000' - }) + }), + tuiInputPhoneInternationalOptionsProvider({ + metadata: defer(async () => + import('libphonenumber-js/max/metadata').then((m) => m.default), + ), + }), ], }; diff --git a/src/app/chat/chat.html b/src/app/chat/chat.html index 9f4dc77..55723bf 100644 --- a/src/app/chat/chat.html +++ b/src/app/chat/chat.html @@ -4,10 +4,6 @@ - @@ -94,6 +90,13 @@ } + @case (2) { + + } + @case (3) { + + } } @@ -114,10 +117,10 @@ } - @if (router.url.startsWith("/chat/dm")) { + @if (router.url.startsWith("/chat/dm") || router.url.startsWith("/chat/picture")) { @defer (when serviceManager.chatsStatus() != LoadStatus.loading) { - + } } @else if (router.url.startsWith("/chat/network")) { @defer (when serviceManager.networksStatus() != LoadStatus.loading) { diff --git a/src/app/chat/chat.ts b/src/app/chat/chat.ts index a66c8d3..9a5c735 100644 --- a/src/app/chat/chat.ts +++ b/src/app/chat/chat.ts @@ -47,7 +47,7 @@ export class Chat implements OnInit { router = inject(Router) routerOutletActive = signal(false) - userSettingsOpen = signal(true) + userSettingsOpen = signal(false) navigationActiveIndex = 0 // Mobile navigation // @@ -65,12 +65,12 @@ export class Chat implements OnInit { { text: "chat.tabBar.tab3", icon: '@tui.image', - implemented: false, + implemented: true, }, { text: "chat.tabBar.tab4", icon: '@tui.cog', - implemented: false, + implemented: true, } ]; diff --git a/src/app/chat/user-settings/security/email/email.html b/src/app/chat/user-settings/security/email/email.html index 42b23a5..9882de7 100644 --- a/src/app/chat/user-settings/security/email/email.html +++ b/src/app/chat/user-settings/security/email/email.html @@ -1,13 +1,14 @@ -
+
+

{{ "chat.userSettingsDialog.security.changeEmailDialog.warn"|translate }}

@if (step() == 0) {
@if (!changeEmailRemoveMode()) { - + } @@ -32,23 +33,25 @@
} @else { - @if (!changeEmailRemoveMode()) { + @if (serviceManager.currentSession()!.userData.emailSet) { - + } - - - - + @if (!changeEmailRemoveMode()) { + + + + + }
@@ -87,7 +90,7 @@ @if (serviceManager.currentSession()!.userData.emailSet) { - @if (serviceManager.currentSession()!.userData.passwordSet) { - diff --git a/src/app/chat/user-settings/security/password/password.ts b/src/app/chat/user-settings/security/password/password.ts index 0c670bd..564b3b1 100644 --- a/src/app/chat/user-settings/security/password/password.ts +++ b/src/app/chat/user-settings/security/password/password.ts @@ -12,6 +12,7 @@ import { import {TranslatePipe} from '@ngx-translate/core'; import {TuiBadge, TuiButtonLoading} from '@taiga-ui/kit'; import { + TUI_BREAKPOINT, TuiButton, TuiDialog, TuiErrorComponent, @@ -42,6 +43,7 @@ import { }) export class Password { serviceManager = inject(ServiceManager) + breakpoint = inject(TUI_BREAKPOINT) changePasswordDialogOpen = signal(false) changePasswordRemoveMode = signal(false) diff --git a/src/app/chat/user-settings/security/phone/phone.html b/src/app/chat/user-settings/security/phone/phone.html new file mode 100644 index 0000000..a806fdc --- /dev/null +++ b/src/app/chat/user-settings/security/phone/phone.html @@ -0,0 +1,99 @@ +
+ +

{{ "chat.userSettingsDialog.security.changePhoneDialog.warn"|translate }}

+ @if (step() == 0) { + + @if (!changePhoneRemoveMode()) { + + + + + } + + + + + + + @if (changePhoneForm.controls['currentPassword'].dirty) { + @if (changePhoneForm.controls['currentPassword'].hasError("required")) { + + } + + @if (changePhoneForm.controls['currentPassword'].hasError("incorrect")) { + + } + } + + +
+ +
+ } @else { +
+ @if (serviceManager.currentSession()!.userData.phoneSet) { + + + + + } + + @if (!changePhoneRemoveMode()) { + + + + + } +
+ +
+ +
+ } +
+ +
+ + {{ "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/phone/phone.scss b/src/app/chat/user-settings/security/phone/phone.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/chat/user-settings/security/phone/phone.spec.ts b/src/app/chat/user-settings/security/phone/phone.spec.ts new file mode 100644 index 0000000..579acf8 --- /dev/null +++ b/src/app/chat/user-settings/security/phone/phone.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Phone } from './phone'; + +describe('Phone', () => { + let component: Phone; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Phone], + }).compileComponents(); + + fixture = TestBed.createComponent(Phone); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/chat/user-settings/security/phone/phone.ts b/src/app/chat/user-settings/security/phone/phone.ts new file mode 100644 index 0000000..56bfff3 --- /dev/null +++ b/src/app/chat/user-settings/security/phone/phone.ts @@ -0,0 +1,111 @@ +import {Component, inject, signal} from '@angular/core'; +import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; +import {TranslatePipe} from '@ngx-translate/core'; +import {TuiBadge, TuiButtonLoading, TuiInputPhoneInternationalComponent, TuiSortCountriesPipe} from '@taiga-ui/kit'; +import { + TUI_BREAKPOINT, + TuiButton, + TuiDialog, + TuiErrorComponent, + TuiIcon, + TuiInputDirective, + TuiLabel, + TuiTextfieldComponent +} from '@taiga-ui/core'; +import {ServiceManager} from '../../../../service-manager'; +import {getCountries} from 'libphonenumber-js'; +import {TuiCountryIsoCode} from '@taiga-ui/i18n'; + +@Component({ + selector: 'user-settings-security-phone', + imports: [ + ReactiveFormsModule, + TranslatePipe, + TuiBadge, + TuiButton, + TuiButtonLoading, + TuiDialog, + TuiErrorComponent, + TuiIcon, + TuiInputDirective, + TuiLabel, + TuiTextfieldComponent, + TuiInputPhoneInternationalComponent, + TuiSortCountriesPipe + ], + templateUrl: './phone.html', + styleUrl: './phone.scss', +}) +export class Phone { + protected readonly countries = getCountries(); + protected countryIsoCode: TuiCountryIsoCode = 'HU'; + breakpoint = inject(TUI_BREAKPOINT) + + serviceManager = inject(ServiceManager) + + step = signal(0) + changePhoneDialogOpen = signal(false) + changePhoneRemoveMode = signal(false) + changePhonePending = signal(false) + verifyPhonePending = signal(false) + newNumber = "" + + changePhoneForm = new FormGroup({ + newNumber: new FormControl(""), + currentPassword: new FormControl(""), + }) + + verifyPhoneForm = new FormGroup({ + oldCode: new FormControl(0), + newCode: new FormControl(0, {validators: [Validators.required]}) + }) + + openChangePhoneDialog(modeRemove: boolean) { + this.changePhoneDialogOpen.set(true) + this.changePhoneRemoveMode.set(modeRemove) + this.changePhoneForm.controls["currentPassword"].clearValidators() + this.changePhoneForm.controls["newNumber"].clearValidators() + if (!modeRemove) { + this.changePhoneForm.controls["newNumber"].setValidators([Validators.required]) + this.changePhoneForm.controls["newNumber"].updateValueAndValidity() + } + + this.changePhoneForm.controls["currentPassword"].setValidators([Validators.required]) + this.changePhoneForm.controls["currentPassword"].updateValueAndValidity() + } + + async changePhone(currentPassword: string | null, newNumber: string | null) { + if (this.changePhoneRemoveMode()) { + newNumber = "remove" + } + this.changePhonePending.set(true) + const service = this.serviceManager.currentSessionHandler + if (service) { + try { + await service.changePhoneNumber(currentPassword ?? "", newNumber ?? "") + this.step.set(1) + this.newNumber = newNumber ?? "" + if (!this.changePhoneRemoveMode()) { + this.verifyPhoneForm.controls["oldCode"].setValidators([Validators.required]) + } + } catch (e) { + this.changePhoneForm.controls["currentPassword"].setErrors({incorrect: true}) + this.changePhonePending.set(false) + } + } + } + + async verifyPhone(newCode: number | null, oldCode: number | null) { + this.verifyPhonePending.set(true) + const service = this.serviceManager.currentSessionHandler + if (service) { + try { + await service.verifyPhoneNumberChange(this.newNumber, oldCode ?? 0, newCode ?? 0) + this.changePhoneDialogOpen.set(false) + } catch (e) { + this.verifyPhoneForm.controls["newCode"].setErrors({incorrect: true}) + this.verifyPhonePending.set(false) + } + } + } +} diff --git a/src/app/chat/user-settings/security/security.html b/src/app/chat/user-settings/security/security.html index 043b385..9a1d5a4 100644 --- a/src/app/chat/user-settings/security/security.html +++ b/src/app/chat/user-settings/security/security.html @@ -5,38 +5,5 @@
-
-
- - {{ "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 index 69c3bc1..3d3ed24 100644 --- a/src/app/chat/user-settings/security/security.scss +++ b/src/app/chat/user-settings/security/security.scss @@ -12,6 +12,18 @@ align-items: center; gap: 10px; + &.mobile { + grid-template-rows: 50px 50px; + grid-template-columns: 1fr; + height: 150px; + + main { + display: flex; + justify-content: start; + flex-direction: column; + } + } + ::ng-deep header { display: flex; gap: 5px; diff --git a/src/app/chat/user-settings/security/security.ts b/src/app/chat/user-settings/security/security.ts index 65619f0..28fb404 100644 --- a/src/app/chat/user-settings/security/security.ts +++ b/src/app/chat/user-settings/security/security.ts @@ -14,6 +14,7 @@ import { } from '@angular/forms'; import {Password} from './password/password'; import {Email} from './email/email'; +import {Phone} from './phone/phone'; @Component({ selector: 'user-settings-security', @@ -31,7 +32,8 @@ import {Email} from './email/email'; TuiButtonLoading, TuiErrorComponent, Password, - Email + Email, + Phone ], templateUrl: './security.html', styleUrl: './security.scss', diff --git a/src/app/chat/user-settings/user-settings.html b/src/app/chat/user-settings/user-settings.html index ea17621..84e0eb2 100644 --- a/src/app/chat/user-settings/user-settings.html +++ b/src/app/chat/user-settings/user-settings.html @@ -1,24 +1,38 @@ - - -
- @switch (selectedOption) { - @case ("security") { - - } +
+ @for (option of options; track option) { + + } +
+ } -
+ + @if (breakpoint() == "desktopLarge" || selectedOption != "") { +
+ @if (breakpoint() != "desktopLarge") { +
+ +

{{"chat.userSettingsDialog.options."+selectedOption|translate}}

+
+ } + @switch (selectedOption) { + @case ("security") { + + } + } +
+ } +
diff --git a/src/app/chat/user-settings/user-settings.scss b/src/app/chat/user-settings/user-settings.scss index 486ed67..3b994a2 100644 --- a/src/app/chat/user-settings/user-settings.scss +++ b/src/app/chat/user-settings/user-settings.scss @@ -1,9 +1,18 @@ -:host { +#layout { height: 100%; width: 100%; display: grid; grid-template-columns: 300px 1fr; + &.mobile { + grid-template-columns: 1fr; + + aside, main { + border-radius: 1.5rem; + background: transparent; + } + } + aside { background-color: var(--tui-background-base-alt); height: 100%; diff --git a/src/app/chat/user-settings/user-settings.ts b/src/app/chat/user-settings/user-settings.ts index 9a495f5..e712244 100644 --- a/src/app/chat/user-settings/user-settings.ts +++ b/src/app/chat/user-settings/user-settings.ts @@ -1,6 +1,6 @@ -import { Component } from '@angular/core'; +import {Component, EventEmitter, inject, OnInit, Output} from '@angular/core'; import {TranslatePipe} from '@ngx-translate/core'; -import {TuiButton, TuiButtonX, TuiIcon} from '@taiga-ui/core'; +import {TUI_BREAKPOINT, TuiButton, TuiButtonX, TuiIcon} from '@taiga-ui/core'; import {Security} from './security/security'; @Component({ @@ -15,28 +15,43 @@ import {Security} from './security/security'; templateUrl: './user-settings.html', styleUrl: './user-settings.scss', }) -export class UserSettings { +export class UserSettings implements OnInit { + breakpoint = inject(TUI_BREAKPOINT) + + @Output() close = new EventEmitter() + selectedOption = "security" options = [ { icon: "shield", name: "security", + implemented: true, }, { icon: "users", name: "profile", + implemented: false, }, { icon: "paint-roller", name: "themes", + implemented: false, }, { icon: "computer", - name: "sessions" + name: "sessions", + implemented: false, }, { icon: "hard-drive", - name: "storage" + name: "storage", + implemented: false, } ] + + ngOnInit() { + if (this.breakpoint() != "desktopLarge") { + this.selectedOption = "" + } + } } diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index 89323a8..329794b 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,5 +1,5 @@ export const environment = { - version: "3.0-beta8", + version: "3.0-beta9", api_url: "http://localhost:3000", cdn_url: "http://localhost:4000", ws_url: "ws://localhost:3000", diff --git a/src/environments/environment.ts b/src/environments/environment.ts index e4adc75..5ca1d25 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,5 +1,5 @@ export const environment = { - version: "3.0-beta8", + version: "3.0-beta9", api_url: "https://api.chatenium.hu", cdn_url: "https://cdn.chatenium.hu", ws_url: "wss://api.chatenium.hu",