feat: Add user metadata changing display name and description with sync
All checks were successful
Queue Release Build / prepare (push) Successful in 28s
Deploy Web Apps / deploy (push) Successful in 5m2s
Queue Release Build / build-windows (push) Successful in 16m44s
Queue Release Build / build-linux (push) Successful in 27m12s
Queue Release Build / finalize (push) Successful in 22s
All checks were successful
Queue Release Build / prepare (push) Successful in 28s
Deploy Web Apps / deploy (push) Successful in 5m2s
Queue Release Build / build-windows (push) Successful in 16m44s
Queue Release Build / build-linux (push) Successful in 27m12s
Queue Release Build / finalize (push) Successful in 22s
This commit is contained in:
@@ -11,7 +11,11 @@ import { UserAvatarComponent } from '../../../../shared';
|
||||
@Component({
|
||||
selector: 'app-user-bar',
|
||||
standalone: true,
|
||||
imports: [CommonModule, NgIcon, UserAvatarComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgIcon,
|
||||
UserAvatarComponent
|
||||
],
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
lucideLogIn,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Profile Avatar Domain
|
||||
|
||||
Owns local profile picture workflow: source validation, crop/zoom editor state, static 256x256 WebP rendering, animated avatar preservation, desktop file persistence, and P2P avatar sync metadata.
|
||||
Owns local profile picture workflow plus peer-synced profile-card metadata: source validation, crop/zoom editor state, static 256x256 WebP rendering, animated avatar preservation, desktop file persistence, and P2P avatar/profile sync metadata.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
@@ -9,7 +9,9 @@ Owns local profile picture workflow: source validation, crop/zoom editor state,
|
||||
- Render static avatars to `256x256` WebP with client-side compression.
|
||||
- Preserve animated `.gif` and animated `.webp` uploads without flattening frames.
|
||||
- Persist desktop copy at `user/<username>/profile/profile.<ext>` under app data.
|
||||
- Let the local user edit their profile-card display name and description.
|
||||
- Expose helpers used by store effects to keep avatar metadata (`avatarHash`, `avatarMime`, `avatarUpdatedAt`) consistent.
|
||||
- Reuse the avatar summary/request/full handshake to sync profile text (`displayName`, `description`, `profileUpdatedAt`) alongside avatar state.
|
||||
|
||||
## Module map
|
||||
|
||||
@@ -33,12 +35,14 @@ graph TD
|
||||
## Flow
|
||||
|
||||
1. `ProfileCardComponent` opens file picker from editable avatar button.
|
||||
2. `ProfileAvatarEditorComponent` previews exact crop using drag + zoom.
|
||||
3. `ProfileAvatarImageService` renders static uploads to `256x256` WebP, but keeps animated GIF and WebP sources intact.
|
||||
4. `ProfileAvatarStorageService` writes desktop copy when Electron is available.
|
||||
5. `UserAvatarEffects` broadcasts avatar summary, answers requests, streams chunks, and persists received avatars locally.
|
||||
2. `ProfileCardComponent` saves display-name and description edits through the users store.
|
||||
3. `ProfileAvatarEditorComponent` previews exact crop using drag + zoom.
|
||||
4. `ProfileAvatarImageService` renders static uploads to `256x256` WebP, but keeps animated GIF and WebP sources intact.
|
||||
5. `ProfileAvatarStorageService` writes desktop copy when Electron is available.
|
||||
6. `UserAvatarEffects` broadcasts avatar/profile summaries, answers requests, streams chunks when needed, and persists received profile state locally.
|
||||
|
||||
## Notes
|
||||
|
||||
- Static uploads are normalized to WebP. Animated GIF and animated WebP uploads keep their original animation, mime type, and full-frame presentation.
|
||||
- `avatarUrl` stays local display data. Version conflict resolution uses `avatarUpdatedAt` and `avatarHash`.
|
||||
- Profile text uses its own `profileUpdatedAt` version so display-name and description changes can sync without replacing a newer avatar.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div
|
||||
class="fixed inset-0 z-[112] bg-black/70 backdrop-blur-sm"
|
||||
(click)="cancelled.emit()"
|
||||
(keydown.enter)="cancelled.emit()"
|
||||
(keydown.space)="cancelled.emit()"
|
||||
(click)="cancelled.emit(undefined)"
|
||||
(keydown.enter)="cancelled.emit(undefined)"
|
||||
(keydown.space)="cancelled.emit(undefined)"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Close profile image editor"
|
||||
@@ -11,7 +11,6 @@
|
||||
<div class="fixed inset-0 z-[113] flex items-center justify-center p-4 pointer-events-none">
|
||||
<div
|
||||
class="pointer-events-auto flex max-h-[calc(100vh-2rem)] w-full max-w-4xl flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-2xl"
|
||||
(click)="$event.stopPropagation()"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
@@ -135,7 +134,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg bg-secondary px-4 py-2 text-sm text-foreground transition-colors hover:bg-secondary/80"
|
||||
(click)="cancelled.emit()"
|
||||
(click)="cancelled.emit(undefined)"
|
||||
[disabled]="processing()"
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
export class ProfileAvatarEditorComponent {
|
||||
readonly source = input.required<EditableProfileAvatarSource>();
|
||||
|
||||
readonly cancelled = output<void>();
|
||||
readonly cancelled = output<undefined>();
|
||||
readonly confirmed = output<ProcessedProfileAvatar>();
|
||||
|
||||
readonly frameSize = PROFILE_AVATAR_EDITOR_FRAME_SIZE;
|
||||
@@ -53,7 +53,7 @@ export class ProfileAvatarEditorComponent {
|
||||
@HostListener('document:keydown.escape')
|
||||
onEscape(): void {
|
||||
if (!this.processing()) {
|
||||
this.cancelled.emit();
|
||||
this.cancelled.emit(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import {
|
||||
Overlay,
|
||||
OverlayRef
|
||||
} from '@angular/cdk/overlay';
|
||||
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { ComponentPortal } from '@angular/cdk/portal';
|
||||
import {
|
||||
EditableProfileAvatarSource,
|
||||
ProcessedProfileAvatar
|
||||
} from '../../domain/profile-avatar.models';
|
||||
import { EditableProfileAvatarSource, ProcessedProfileAvatar } from '../../domain/profile-avatar.models';
|
||||
import { ProfileAvatarEditorComponent } from './profile-avatar-editor.component';
|
||||
|
||||
export const PROFILE_AVATAR_EDITOR_OVERLAY_CLASS = 'profile-avatar-editor-overlay-pane';
|
||||
@@ -25,7 +19,9 @@ export class ProfileAvatarEditorService {
|
||||
const overlayRef = this.overlay.create({
|
||||
disposeOnNavigation: true,
|
||||
panelClass: PROFILE_AVATAR_EDITOR_OVERLAY_CLASS,
|
||||
positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),
|
||||
positionStrategy: this.overlay.position().global()
|
||||
.centerHorizontally()
|
||||
.centerVertically(),
|
||||
scrollStrategy: this.overlay.scrollStrategies.block()
|
||||
});
|
||||
|
||||
@@ -55,7 +51,6 @@ export class ProfileAvatarEditorService {
|
||||
overlayRef.dispose();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
const cancelSub = componentRef.instance.cancelled.subscribe(() => finish(null));
|
||||
const confirmSub = componentRef.instance.confirmed.subscribe((avatar) => finish(avatar));
|
||||
const detachSub = overlayRef.detachments().subscribe(() => finish(null));
|
||||
|
||||
@@ -2,6 +2,6 @@ export * from './domain/profile-avatar.models';
|
||||
export { ProfileAvatarFacade } from './application/services/profile-avatar.facade';
|
||||
export { ProfileAvatarEditorComponent } from './feature/profile-avatar-editor/profile-avatar-editor.component';
|
||||
export {
|
||||
PROFILE_AVATAR_EDITOR_OVERLAY_CLASS,
|
||||
ProfileAvatarEditorService
|
||||
PROFILE_AVATAR_EDITOR_OVERLAY_CLASS,
|
||||
ProfileAvatarEditorService
|
||||
} from './feature/profile-avatar-editor/profile-avatar-editor.service';
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
isAnimatedGif,
|
||||
isAnimatedWebp
|
||||
} from './profile-avatar-image.service';
|
||||
/* eslint-disable @stylistic/js/array-element-newline */
|
||||
import { isAnimatedGif, isAnimatedWebp } from './profile-avatar-image.service';
|
||||
|
||||
describe('profile-avatar image animation detection', () => {
|
||||
it('detects animated gifs with multiple frames', () => {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
||||
import { User } from '../../../../shared-kernel';
|
||||
import {
|
||||
ProcessedProfileAvatar,
|
||||
resolveProfileAvatarStorageFileName
|
||||
} from '../../domain/profile-avatar.models';
|
||||
import type { User } from '../../../../shared-kernel';
|
||||
import { resolveProfileAvatarStorageFileName, type ProcessedProfileAvatar } from '../../domain/profile-avatar.models';
|
||||
|
||||
const LEGACY_PROFILE_FILE_NAMES = [
|
||||
'profile.webp',
|
||||
|
||||
Reference in New Issue
Block a user