mirror of
https://github.com/block-core/angor-hub-old.git
synced 2025-12-17 01:44:19 +01:00
Add Events support (#41)
* Update profile.component.html * Prepare event box and comment box for receiving and sending events and comments. * Add like, share and event preview UI to events * Add event box * Update user.ts * Update post.ts * Update notification.service.ts * Update metadata.service.ts * Update event.service.ts * Update notifications.component.ts * Update profile.component.ts * Update profile.component.html * Load user events * Fix UI * Add new service to get events * Update event service * Update event service * Update event service * Update publishEventToWriteRelays * Update event service * Clean and format code * Change events UI * Update event service * Update event service * Update event UI * Update profile.component.html * Add event list component and update profile
This commit is contained in:
@@ -31,7 +31,12 @@
|
||||
"quill-delta",
|
||||
"buffer",
|
||||
"localforage",
|
||||
"moment"
|
||||
"moment",
|
||||
"bech32",
|
||||
"bn.js",
|
||||
"qrcode",
|
||||
"dayjs",
|
||||
"dayjs/plugin/relativeTime"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "angor-hub",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "angor-hub",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.8",
|
||||
"dependencies": {
|
||||
"@angular-builders/custom-webpack": "^18.0.0",
|
||||
"@angular/animations": "18.2.6",
|
||||
@@ -91,7 +91,7 @@
|
||||
"karma-jasmine-html-reporter": "2.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"postcss": "8.4.47",
|
||||
"prettier": "3.3.3",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-organize-imports": "4.1.0",
|
||||
"prettier-plugin-tailwindcss": "0.6.8",
|
||||
"tailwindcss": "3.4.13",
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"test": "ng test",
|
||||
"deploy": "ng deploy",
|
||||
"version": "node -p \"require('./package.json').version\"",
|
||||
"changelog": "conventional-changelog -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md"
|
||||
"changelog": "conventional-changelog -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md",
|
||||
"format": "prettier --write \"src/**/*.{ts,html,css,scss,json,js}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular-builders/custom-webpack": "^18.0.0",
|
||||
@@ -98,7 +99,7 @@
|
||||
"karma-jasmine-html-reporter": "2.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"postcss": "8.4.47",
|
||||
"prettier": "3.3.3",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-organize-imports": "4.1.0",
|
||||
"prettier-plugin-tailwindcss": "0.6.8",
|
||||
"tailwindcss": "3.4.13",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,3 @@
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import {
|
||||
APP_INITIALIZER,
|
||||
ENVIRONMENT_INITIALIZER,
|
||||
EnvironmentProviders,
|
||||
Provider,
|
||||
importProvidersFrom,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MATERIAL_SANITY_CHECKS } from '@angular/material/core';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||
import {
|
||||
ANGOR_MOCK_API_DEFAULT_DELAY,
|
||||
mockApiInterceptor,
|
||||
@@ -25,6 +13,18 @@ import { AngorMediaWatcherService } from '@angor/services/media-watcher';
|
||||
import { AngorPlatformService } from '@angor/services/platform';
|
||||
import { AngorSplashScreenService } from '@angor/services/splash-screen';
|
||||
import { AngorUtilsService } from '@angor/services/utils';
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import {
|
||||
APP_INITIALIZER,
|
||||
ENVIRONMENT_INITIALIZER,
|
||||
EnvironmentProviders,
|
||||
Provider,
|
||||
importProvidersFrom,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MATERIAL_SANITY_CHECKS } from '@angular/material/core';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||
|
||||
export type AngorProviderConfig = {
|
||||
mockApi?: {
|
||||
@@ -40,10 +40,8 @@ export type AngorProviderConfig = {
|
||||
export const provideAngor = (
|
||||
config: AngorProviderConfig
|
||||
): Array<Provider | EnvironmentProviders> => {
|
||||
|
||||
const providers: Array<Provider | EnvironmentProviders> = [
|
||||
{
|
||||
|
||||
provide: MATERIAL_SANITY_CHECKS,
|
||||
useValue: {
|
||||
doctype: true,
|
||||
@@ -52,7 +50,6 @@ export const provideAngor = (
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
||||
useValue: {
|
||||
appearance: 'fill',
|
||||
@@ -103,7 +100,6 @@ export const provideAngor = (
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
if (config?.mockApi?.services) {
|
||||
providers.push(
|
||||
provideHttpClient(withInterceptors([mockApiInterceptor])),
|
||||
@@ -116,6 +112,5 @@ export const provideAngor = (
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return providers;
|
||||
};
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
* Defines animation curves for Angor.
|
||||
*/
|
||||
export class AngorAnimationCurves {
|
||||
static standard = 'cubic-bezier(0.4, 0.0, 0.2, 1)'; // Standard animation curve
|
||||
static deceleration = 'cubic-bezier(0.0, 0.0, 0.2, 1)'; // Deceleration curve
|
||||
static acceleration = 'cubic-bezier(0.4, 0.0, 1, 1)'; // Acceleration curve
|
||||
static sharp = 'cubic-bezier(0.4, 0.0, 0.6, 1)'; // Sharp curve
|
||||
static standard = 'cubic-bezier(0.4, 0.0, 0.2, 1)'; // Standard animation curve
|
||||
static deceleration = 'cubic-bezier(0.0, 0.0, 0.2, 1)'; // Deceleration curve
|
||||
static acceleration = 'cubic-bezier(0.4, 0.0, 1, 1)'; // Acceleration curve
|
||||
static sharp = 'cubic-bezier(0.4, 0.0, 0.6, 1)'; // Sharp curve
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines animation durations for Angor.
|
||||
*/
|
||||
export class AngorAnimationDurations {
|
||||
static complex = '375ms'; // Duration for complex animations
|
||||
static entering = '225ms'; // Duration for entering animations
|
||||
static exiting = '195ms'; // Duration for exiting animations
|
||||
static complex = '375ms'; // Duration for complex animations
|
||||
static entering = '225ms'; // Duration for entering animations
|
||||
static exiting = '195ms'; // Duration for exiting animations
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
AngorAnimationCurves,
|
||||
AngorAnimationDurations,
|
||||
} from '@angor/animations/defaults';
|
||||
import {
|
||||
animate,
|
||||
state,
|
||||
@@ -5,10 +9,6 @@ import {
|
||||
transition,
|
||||
trigger,
|
||||
} from '@angular/animations';
|
||||
import {
|
||||
AngorAnimationCurves,
|
||||
AngorAnimationDurations,
|
||||
} from '@angor/animations/defaults';
|
||||
|
||||
/**
|
||||
* Animation trigger for expand/collapse transitions
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
AngorAnimationCurves,
|
||||
AngorAnimationDurations,
|
||||
} from '@angor/animations/defaults';
|
||||
import {
|
||||
animate,
|
||||
state,
|
||||
@@ -5,10 +9,6 @@ import {
|
||||
transition,
|
||||
trigger,
|
||||
} from '@angular/animations';
|
||||
import {
|
||||
AngorAnimationCurves,
|
||||
AngorAnimationDurations,
|
||||
} from '@angor/animations/defaults';
|
||||
|
||||
/**
|
||||
* Fade in animation trigger
|
||||
|
||||
@@ -21,15 +21,42 @@ const shake = trigger('shake', [
|
||||
'{{timings}}',
|
||||
keyframes([
|
||||
style({ transform: 'translate3d(0, 0, 0)', offset: 0 }),
|
||||
style({ transform: 'translate3d(-10px, 0, 0)', offset: 0.1 }),
|
||||
style({ transform: 'translate3d(10px, 0, 0)', offset: 0.2 }),
|
||||
style({ transform: 'translate3d(-10px, 0, 0)', offset: 0.3 }),
|
||||
style({ transform: 'translate3d(10px, 0, 0)', offset: 0.4 }),
|
||||
style({ transform: 'translate3d(-10px, 0, 0)', offset: 0.5 }),
|
||||
style({ transform: 'translate3d(10px, 0, 0)', offset: 0.6 }),
|
||||
style({ transform: 'translate3d(-10px, 0, 0)', offset: 0.7 }),
|
||||
style({ transform: 'translate3d(10px, 0, 0)', offset: 0.8 }),
|
||||
style({ transform: 'translate3d(-10px, 0, 0)', offset: 0.9 }),
|
||||
style({
|
||||
transform: 'translate3d(-10px, 0, 0)',
|
||||
offset: 0.1,
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(10px, 0, 0)',
|
||||
offset: 0.2,
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(-10px, 0, 0)',
|
||||
offset: 0.3,
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(10px, 0, 0)',
|
||||
offset: 0.4,
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(-10px, 0, 0)',
|
||||
offset: 0.5,
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(10px, 0, 0)',
|
||||
offset: 0.6,
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(-10px, 0, 0)',
|
||||
offset: 0.7,
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(10px, 0, 0)',
|
||||
offset: 0.8,
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(-10px, 0, 0)',
|
||||
offset: 0.9,
|
||||
}),
|
||||
style({ transform: 'translate3d(0, 0, 0)', offset: 1 }),
|
||||
])
|
||||
),
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
AngorAnimationCurves,
|
||||
AngorAnimationDurations,
|
||||
} from '@angor/animations/defaults';
|
||||
import {
|
||||
animate,
|
||||
state,
|
||||
@@ -5,10 +9,6 @@ import {
|
||||
transition,
|
||||
trigger,
|
||||
} from '@angular/animations';
|
||||
import {
|
||||
AngorAnimationCurves,
|
||||
AngorAnimationDurations,
|
||||
} from '@angor/animations/defaults';
|
||||
|
||||
/**
|
||||
* Slide in from top animation trigger
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
AngorAnimationCurves,
|
||||
AngorAnimationDurations,
|
||||
} from '@angor/animations/defaults';
|
||||
import {
|
||||
animate,
|
||||
state,
|
||||
@@ -5,10 +9,6 @@ import {
|
||||
transition,
|
||||
trigger,
|
||||
} from '@angular/animations';
|
||||
import {
|
||||
AngorAnimationCurves,
|
||||
AngorAnimationDurations,
|
||||
} from '@angor/animations/defaults';
|
||||
|
||||
/**
|
||||
* Creates a reusable animation trigger with configurable parameters.
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { AngorAlertService } from '@angor/components/alert/alert.service';
|
||||
import {
|
||||
AngorAlertAppearance,
|
||||
AngorAlertType,
|
||||
} from '@angor/components/alert/alert.types';
|
||||
import { AngorUtilsService } from '@angor/services/utils/utils.service';
|
||||
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -16,13 +23,6 @@ import {
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { AngorAlertService } from '@angor/components/alert/alert.service';
|
||||
import {
|
||||
AngorAlertAppearance,
|
||||
AngorAlertType,
|
||||
} from '@angor/components/alert/alert.types';
|
||||
import { AngorUtilsService } from '@angor/services/utils/utils.service';
|
||||
import { Subject, filter, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
@@ -86,16 +86,22 @@ export class AngorAlertComponent implements OnChanges, OnInit, OnDestroy {
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if ('dismissed' in changes) {
|
||||
this.dismissed = coerceBooleanProperty(changes.dismissed.currentValue);
|
||||
this.dismissed = coerceBooleanProperty(
|
||||
changes.dismissed.currentValue
|
||||
);
|
||||
this._toggleDismiss(this.dismissed);
|
||||
}
|
||||
|
||||
if ('dismissible' in changes) {
|
||||
this.dismissible = coerceBooleanProperty(changes.dismissible.currentValue);
|
||||
this.dismissible = coerceBooleanProperty(
|
||||
changes.dismissible.currentValue
|
||||
);
|
||||
}
|
||||
|
||||
if ('showIcon' in changes) {
|
||||
this.showIcon = coerceBooleanProperty(changes.showIcon.currentValue);
|
||||
this.showIcon = coerceBooleanProperty(
|
||||
changes.showIcon.currentValue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { AngorCardFace } from '@angor/components/card/card.types';
|
||||
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
import {
|
||||
Component,
|
||||
@@ -7,8 +9,6 @@ import {
|
||||
SimpleChanges,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { AngorCardFace } from '@angor/components/card/card.types';
|
||||
|
||||
@Component({
|
||||
selector: 'angor-card',
|
||||
@@ -47,11 +47,15 @@ export class AngorCardComponent implements OnChanges {
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if ('expanded' in changes) {
|
||||
this.expanded = coerceBooleanProperty(changes.expanded.currentValue);
|
||||
this.expanded = coerceBooleanProperty(
|
||||
changes.expanded.currentValue
|
||||
);
|
||||
}
|
||||
|
||||
if ('flippable' in changes) {
|
||||
this.flippable = coerceBooleanProperty(changes.flippable.currentValue);
|
||||
this.flippable = coerceBooleanProperty(
|
||||
changes.flippable.currentValue
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { AngorDrawerService } from '@angor/components/drawer/drawer.service';
|
||||
import {
|
||||
AngorDrawerMode,
|
||||
AngorDrawerPosition,
|
||||
} from '@angor/components/drawer/drawer.types';
|
||||
import { AngorUtilsService } from '@angor/services/utils/utils.service';
|
||||
import {
|
||||
animate,
|
||||
AnimationBuilder,
|
||||
@@ -11,6 +17,7 @@ import {
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
@@ -19,14 +26,7 @@ import {
|
||||
Renderer2,
|
||||
SimpleChanges,
|
||||
ViewEncapsulation,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { AngorDrawerService } from '@angor/components/drawer/drawer.service';
|
||||
import {
|
||||
AngorDrawerMode,
|
||||
AngorDrawerPosition,
|
||||
} from '@angor/components/drawer/drawer.types';
|
||||
import { AngorUtilsService } from '@angor/services/utils/utils.service';
|
||||
|
||||
@Component({
|
||||
selector: 'angor-drawer',
|
||||
@@ -56,7 +56,8 @@ export class AngorDrawerComponent implements OnChanges, OnInit, OnDestroy {
|
||||
@Output() readonly fixedChanged = new EventEmitter<boolean>();
|
||||
@Output() readonly modeChanged = new EventEmitter<AngorDrawerMode>();
|
||||
@Output() readonly openedChanged = new EventEmitter<boolean>();
|
||||
@Output() readonly positionChanged = new EventEmitter<AngorDrawerPosition>();
|
||||
@Output() readonly positionChanged =
|
||||
new EventEmitter<AngorDrawerPosition>();
|
||||
|
||||
private _animationsEnabled: boolean = false;
|
||||
private _hovered: boolean = false;
|
||||
@@ -107,7 +108,11 @@ export class AngorDrawerComponent implements OnChanges, OnInit, OnDestroy {
|
||||
this._hideOverlay();
|
||||
}
|
||||
|
||||
if (previousMode === 'side' && currentMode === 'over' && this.opened) {
|
||||
if (
|
||||
previousMode === 'side' &&
|
||||
currentMode === 'over' &&
|
||||
this.opened
|
||||
) {
|
||||
this._showOverlay();
|
||||
}
|
||||
|
||||
@@ -125,7 +130,9 @@ export class AngorDrawerComponent implements OnChanges, OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
if ('transparentOverlay' in changes) {
|
||||
this.transparentOverlay = coerceBooleanProperty(changes.transparentOverlay.currentValue);
|
||||
this.transparentOverlay = coerceBooleanProperty(
|
||||
changes.transparentOverlay.currentValue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AngorDrawerComponent } from '@angor/components/drawer/drawer.component';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AngorDrawerService {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AngorHighlightService } from '@angor/components/highlight/highlight.service';
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { AngorHighlightService } from '@angor/components/highlight/highlight.service';
|
||||
|
||||
@Component({
|
||||
selector: 'textarea[angor-highlight]',
|
||||
|
||||
@@ -3,7 +3,6 @@ import hljs from 'highlight.js';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AngorHighlightService {
|
||||
|
||||
/**
|
||||
* Highlights the provided code using the specified language.
|
||||
*/
|
||||
@@ -28,15 +27,17 @@ export class AngorHighlightService {
|
||||
}
|
||||
|
||||
// Determine the smallest indentation
|
||||
lines.filter(line => line.length).forEach((line, index) => {
|
||||
if (index === 0) {
|
||||
indentation = line.search(/\S|$/);
|
||||
} else {
|
||||
indentation = Math.min(line.search(/\S|$/), indentation);
|
||||
}
|
||||
});
|
||||
lines
|
||||
.filter((line) => line.length)
|
||||
.forEach((line, index) => {
|
||||
if (index === 0) {
|
||||
indentation = line.search(/\S|$/);
|
||||
} else {
|
||||
indentation = Math.min(line.search(/\S|$/), indentation);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove extra indentation and return formatted code
|
||||
return lines.map(line => line.substring(indentation)).join('\n');
|
||||
return lines.map((line) => line.substring(indentation)).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
import { Component, inject, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { AngorLoadingService } from '@angor/services/loading';
|
||||
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
import {
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
TemplateRef,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
|
||||
@Component({
|
||||
selector: 'angor-masonry',
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { AngorHorizontalNavigationComponent } from '@angor/components/navigation/horizontal/horizontal.component';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorUtilsService } from '@angor/services/utils/utils.service';
|
||||
import { NgClass, NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -16,10 +20,6 @@ import {
|
||||
RouterLink,
|
||||
RouterLinkActive,
|
||||
} from '@angular/router';
|
||||
import { AngorHorizontalNavigationComponent } from '@angor/components/navigation/horizontal/horizontal.component';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorUtilsService } from '@angor/services/utils/utils.service';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
@@ -69,7 +69,7 @@ export class AngorHorizontalNavigationBasicItemComponent
|
||||
// "isActiveMatchOptions" or the equivalent form of
|
||||
// item's "exactMatch" option
|
||||
this.isActiveMatchOptions =
|
||||
this.item.isActiveMatchOptions ?? this.item.exactMatch
|
||||
(this.item.isActiveMatchOptions ?? this.item.exactMatch)
|
||||
? this._angorUtilsService.exactMatchOptions
|
||||
: this._angorUtilsService.subsetMatchOptions;
|
||||
|
||||
|
||||
@@ -66,7 +66,10 @@
|
||||
|
||||
<!-- Divider -->
|
||||
@if (item.type === 'divider') {
|
||||
<div class="angor-horizontal-navigation-menu-item" mat-menu-item>
|
||||
<div
|
||||
class="angor-horizontal-navigation-menu-item"
|
||||
mat-menu-item
|
||||
>
|
||||
<angor-horizontal-navigation-divider-item
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { AngorHorizontalNavigationBasicItemComponent } from '@angor/components/navigation/horizontal/components/basic/basic.component';
|
||||
import { AngorHorizontalNavigationDividerItemComponent } from '@angor/components/navigation/horizontal/components/divider/divider.component';
|
||||
import { AngorHorizontalNavigationComponent } from '@angor/components/navigation/horizontal/horizontal.component';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { NgClass, NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
@@ -14,11 +19,6 @@ import {
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenu, MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { AngorHorizontalNavigationBasicItemComponent } from '@angor/components/navigation/horizontal/components/basic/basic.component';
|
||||
import { AngorHorizontalNavigationDividerItemComponent } from '@angor/components/navigation/horizontal/components/divider/divider.component';
|
||||
import { AngorHorizontalNavigationComponent } from '@angor/components/navigation/horizontal/horizontal.component';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { AngorHorizontalNavigationComponent } from '@angor/components/navigation/horizontal/horizontal.component';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -8,9 +11,6 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { AngorHorizontalNavigationComponent } from '@angor/components/navigation/horizontal/horizontal.component';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { AngorHorizontalNavigationComponent } from '@angor/components/navigation/horizontal/horizontal.component';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -8,9 +11,6 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { AngorHorizontalNavigationComponent } from '@angor/components/navigation/horizontal/horizontal.component';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorUtilsService } from '@angor/services/utils/utils.service';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
@@ -10,10 +14,6 @@ import {
|
||||
ViewEncapsulation,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorUtilsService } from '@angor/services/utils/utils.service';
|
||||
import { ReplaySubject, Subject } from 'rxjs';
|
||||
import { AngorHorizontalNavigationBasicItemComponent } from './components/basic/basic.component';
|
||||
import { AngorHorizontalNavigationBranchItemComponent } from './components/branch/branch.component';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AngorNavigationService {
|
||||
@@ -93,7 +93,10 @@ export class AngorNavigationService {
|
||||
* @param id
|
||||
* @param navigation
|
||||
*/
|
||||
getItem(id: string, navigation: AngorNavigationItem[]): AngorNavigationItem | null {
|
||||
getItem(
|
||||
id: string,
|
||||
navigation: AngorNavigationItem[]
|
||||
): AngorNavigationItem | null {
|
||||
for (const item of navigation) {
|
||||
if (item.id === id) return item;
|
||||
if (item.children) {
|
||||
|
||||
@@ -38,6 +38,10 @@ export interface AngorNavigationItem {
|
||||
meta?: any;
|
||||
}
|
||||
|
||||
export type AngorVerticalNavigationAppearance = 'default' | 'compact' | 'dense' | 'thin';
|
||||
export type AngorVerticalNavigationAppearance =
|
||||
| 'default'
|
||||
| 'compact'
|
||||
| 'dense'
|
||||
| 'thin';
|
||||
export type AngorVerticalNavigationMode = 'over' | 'side';
|
||||
export type AngorVerticalNavigationPosition = 'left' | 'right';
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationBasicItemComponent } from '@angor/components/navigation/vertical/components/basic/basic.component';
|
||||
import { AngorVerticalNavigationCollapsableItemComponent } from '@angor/components/navigation/vertical/components/collapsable/collapsable.component';
|
||||
import { AngorVerticalNavigationDividerItemComponent } from '@angor/components/navigation/vertical/components/divider/divider.component';
|
||||
import { AngorVerticalNavigationGroupItemComponent } from '@angor/components/navigation/vertical/components/group/group.component';
|
||||
import { AngorVerticalNavigationSpacerItemComponent } from '@angor/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { AngorVerticalNavigationComponent } from '@angor/components/navigation/vertical/vertical.component';
|
||||
import { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
@@ -14,14 +22,6 @@ import {
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationBasicItemComponent } from '@angor/components/navigation/vertical/components/basic/basic.component';
|
||||
import { AngorVerticalNavigationCollapsableItemComponent } from '@angor/components/navigation/vertical/components/collapsable/collapsable.component';
|
||||
import { AngorVerticalNavigationDividerItemComponent } from '@angor/components/navigation/vertical/components/divider/divider.component';
|
||||
import { AngorVerticalNavigationGroupItemComponent } from '@angor/components/navigation/vertical/components/group/group.component';
|
||||
import { AngorVerticalNavigationSpacerItemComponent } from '@angor/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { AngorVerticalNavigationComponent } from '@angor/components/navigation/vertical/vertical.component';
|
||||
import { Subject, filter, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationComponent } from '@angor/components/navigation/vertical/vertical.component';
|
||||
import { AngorUtilsService } from '@angor/services/utils/utils.service';
|
||||
import { NgClass, NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -15,10 +19,6 @@ import {
|
||||
RouterLink,
|
||||
RouterLinkActive,
|
||||
} from '@angular/router';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationComponent } from '@angor/components/navigation/vertical/vertical.component';
|
||||
import { AngorUtilsService } from '@angor/services/utils/utils.service';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
@@ -67,7 +67,7 @@ export class AngorVerticalNavigationBasicItemComponent
|
||||
// "isActiveMatchOptions" or the equivalent form of
|
||||
// item's "exactMatch" option
|
||||
this.isActiveMatchOptions =
|
||||
this.item.isActiveMatchOptions ?? this.item.exactMatch
|
||||
(this.item.isActiveMatchOptions ?? this.item.exactMatch)
|
||||
? this._angorUtilsService.exactMatchOptions
|
||||
: this._angorUtilsService.subsetMatchOptions;
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationBasicItemComponent } from '@angor/components/navigation/vertical/components/basic/basic.component';
|
||||
import { AngorVerticalNavigationDividerItemComponent } from '@angor/components/navigation/vertical/components/divider/divider.component';
|
||||
import { AngorVerticalNavigationGroupItemComponent } from '@angor/components/navigation/vertical/components/group/group.component';
|
||||
import { AngorVerticalNavigationSpacerItemComponent } from '@angor/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { AngorVerticalNavigationComponent } from '@angor/components/navigation/vertical/vertical.component';
|
||||
import { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
@@ -14,14 +22,6 @@ import {
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationBasicItemComponent } from '@angor/components/navigation/vertical/components/basic/basic.component';
|
||||
import { AngorVerticalNavigationDividerItemComponent } from '@angor/components/navigation/vertical/components/divider/divider.component';
|
||||
import { AngorVerticalNavigationGroupItemComponent } from '@angor/components/navigation/vertical/components/group/group.component';
|
||||
import { AngorVerticalNavigationSpacerItemComponent } from '@angor/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { AngorVerticalNavigationComponent } from '@angor/components/navigation/vertical/vertical.component';
|
||||
import { Subject, filter, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationComponent } from '@angor/components/navigation/vertical/vertical.component';
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -8,9 +11,6 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationComponent } from '@angor/components/navigation/vertical/vertical.component';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationBasicItemComponent } from '@angor/components/navigation/vertical/components/basic/basic.component';
|
||||
import { AngorVerticalNavigationCollapsableItemComponent } from '@angor/components/navigation/vertical/components/collapsable/collapsable.component';
|
||||
import { AngorVerticalNavigationDividerItemComponent } from '@angor/components/navigation/vertical/components/divider/divider.component';
|
||||
import { AngorVerticalNavigationSpacerItemComponent } from '@angor/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { AngorVerticalNavigationComponent } from '@angor/components/navigation/vertical/vertical.component';
|
||||
import { BooleanInput } from '@angular/cdk/coercion';
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
@@ -11,13 +18,6 @@ import {
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationBasicItemComponent } from '@angor/components/navigation/vertical/components/basic/basic.component';
|
||||
import { AngorVerticalNavigationCollapsableItemComponent } from '@angor/components/navigation/vertical/components/collapsable/collapsable.component';
|
||||
import { AngorVerticalNavigationDividerItemComponent } from '@angor/components/navigation/vertical/components/divider/divider.component';
|
||||
import { AngorVerticalNavigationSpacerItemComponent } from '@angor/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { AngorVerticalNavigationComponent } from '@angor/components/navigation/vertical/vertical.component';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationComponent } from '@angor/components/navigation/vertical/vertical.component';
|
||||
import { NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -8,9 +11,6 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationComponent } from '@angor/components/navigation/vertical/vertical.component';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -80,8 +80,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import {
|
||||
AngorNavigationItem,
|
||||
AngorVerticalNavigationAppearance,
|
||||
AngorVerticalNavigationMode,
|
||||
AngorVerticalNavigationPosition,
|
||||
} from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationAsideItemComponent } from '@angor/components/navigation/vertical/components/aside/aside.component';
|
||||
import { AngorVerticalNavigationBasicItemComponent } from '@angor/components/navigation/vertical/components/basic/basic.component';
|
||||
import { AngorVerticalNavigationCollapsableItemComponent } from '@angor/components/navigation/vertical/components/collapsable/collapsable.component';
|
||||
import { AngorVerticalNavigationDividerItemComponent } from '@angor/components/navigation/vertical/components/divider/divider.component';
|
||||
import { AngorVerticalNavigationGroupItemComponent } from '@angor/components/navigation/vertical/components/group/group.component';
|
||||
import { AngorVerticalNavigationSpacerItemComponent } from '@angor/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { AngorScrollbarDirective } from '@angor/directives/scrollbar/scrollbar.directive';
|
||||
import { AngorUtilsService } from '@angor/services/utils/utils.service';
|
||||
import {
|
||||
animate,
|
||||
AnimationBuilder,
|
||||
@@ -30,22 +46,6 @@ import {
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { AngorNavigationService } from '@angor/components/navigation/navigation.service';
|
||||
import {
|
||||
AngorNavigationItem,
|
||||
AngorVerticalNavigationAppearance,
|
||||
AngorVerticalNavigationMode,
|
||||
AngorVerticalNavigationPosition,
|
||||
} from '@angor/components/navigation/navigation.types';
|
||||
import { AngorVerticalNavigationAsideItemComponent } from '@angor/components/navigation/vertical/components/aside/aside.component';
|
||||
import { AngorVerticalNavigationBasicItemComponent } from '@angor/components/navigation/vertical/components/basic/basic.component';
|
||||
import { AngorVerticalNavigationCollapsableItemComponent } from '@angor/components/navigation/vertical/components/collapsable/collapsable.component';
|
||||
import { AngorVerticalNavigationDividerItemComponent } from '@angor/components/navigation/vertical/components/divider/divider.component';
|
||||
import { AngorVerticalNavigationGroupItemComponent } from '@angor/components/navigation/vertical/components/group/group.component';
|
||||
import { AngorVerticalNavigationSpacerItemComponent } from '@angor/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { AngorScrollbarDirective } from '@angor/directives/scrollbar/scrollbar.directive';
|
||||
import { AngorUtilsService } from '@angor/services/utils/utils.service';
|
||||
import {
|
||||
delay,
|
||||
filter,
|
||||
|
||||
@@ -21,7 +21,7 @@ export class AngorScrollResetDirective implements OnInit, OnDestroy {
|
||||
ngOnInit(): void {
|
||||
this._router.events
|
||||
.pipe(
|
||||
filter(event => event instanceof NavigationEnd),
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
takeUntil(this._unsubscribeAll)
|
||||
)
|
||||
.subscribe(() => {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
ScrollbarGeometry,
|
||||
ScrollbarPosition,
|
||||
} from '@angor/directives/scrollbar/scrollbar.types';
|
||||
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import {
|
||||
@@ -10,10 +14,6 @@ import {
|
||||
SimpleChanges,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ScrollbarGeometry,
|
||||
ScrollbarPosition,
|
||||
} from '@angor/directives/scrollbar/scrollbar.types';
|
||||
import { merge } from 'lodash-es';
|
||||
import PerfectScrollbar from 'perfect-scrollbar';
|
||||
import { Subject, debounceTime, fromEvent, takeUntil } from 'rxjs';
|
||||
@@ -47,12 +47,20 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if ('angorScrollbar' in changes) {
|
||||
this.angorScrollbar = coerceBooleanProperty(changes.angorScrollbar.currentValue);
|
||||
this.angorScrollbar ? this._initScrollbar() : this._destroyScrollbar();
|
||||
this.angorScrollbar = coerceBooleanProperty(
|
||||
changes.angorScrollbar.currentValue
|
||||
);
|
||||
this.angorScrollbar
|
||||
? this._initScrollbar()
|
||||
: this._destroyScrollbar();
|
||||
}
|
||||
|
||||
if ('angorScrollbarOptions' in changes) {
|
||||
this._options = merge({}, this._options, changes.angorScrollbarOptions.currentValue);
|
||||
this._options = merge(
|
||||
{},
|
||||
this._options,
|
||||
changes.angorScrollbarOptions.currentValue
|
||||
);
|
||||
this._reinitializeScrollbar();
|
||||
}
|
||||
}
|
||||
@@ -92,7 +100,10 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
||||
|
||||
position(absolute: boolean = false): ScrollbarPosition {
|
||||
if (!absolute && this._ps) {
|
||||
return new ScrollbarPosition(this._ps.reach.x || 0, this._ps.reach.y || 0);
|
||||
return new ScrollbarPosition(
|
||||
this._ps.reach.x || 0,
|
||||
this._ps.reach.y || 0
|
||||
);
|
||||
} else {
|
||||
return new ScrollbarPosition(
|
||||
this._elementRef.nativeElement.scrollLeft,
|
||||
@@ -115,7 +126,6 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
scrollToX(x: number, speed?: number): void {
|
||||
this.animateScrolling('scrollLeft', x, speed);
|
||||
}
|
||||
@@ -129,7 +139,9 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
scrollToBottom(offset: number = 0, speed?: number): void {
|
||||
const top = this._elementRef.nativeElement.scrollHeight - this._elementRef.nativeElement.clientHeight;
|
||||
const top =
|
||||
this._elementRef.nativeElement.scrollHeight -
|
||||
this._elementRef.nativeElement.clientHeight;
|
||||
this.animateScrolling('scrollTop', top - offset, speed);
|
||||
}
|
||||
|
||||
@@ -138,7 +150,9 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
scrollToRight(offset: number = 0, speed?: number): void {
|
||||
const left = this._elementRef.nativeElement.scrollWidth - this._elementRef.nativeElement.clientWidth;
|
||||
const left =
|
||||
this._elementRef.nativeElement.scrollWidth -
|
||||
this._elementRef.nativeElement.clientWidth;
|
||||
this.animateScrolling('scrollLeft', left - offset, speed);
|
||||
}
|
||||
|
||||
@@ -152,14 +166,29 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
||||
if (!element) return;
|
||||
|
||||
const elementPos = element.getBoundingClientRect();
|
||||
const scrollerPos = this._elementRef.nativeElement.getBoundingClientRect();
|
||||
const scrollerPos =
|
||||
this._elementRef.nativeElement.getBoundingClientRect();
|
||||
|
||||
if (this._elementRef.nativeElement.classList.contains('ps--active-x')) {
|
||||
this._scrollToInAxis(elementPos.left, scrollerPos.left, 'scrollLeft', offset, ignoreVisible, speed);
|
||||
this._scrollToInAxis(
|
||||
elementPos.left,
|
||||
scrollerPos.left,
|
||||
'scrollLeft',
|
||||
offset,
|
||||
ignoreVisible,
|
||||
speed
|
||||
);
|
||||
}
|
||||
|
||||
if (this._elementRef.nativeElement.classList.contains('ps--active-y')) {
|
||||
this._scrollToInAxis(elementPos.top, scrollerPos.top, 'scrollTop', offset, ignoreVisible, speed);
|
||||
this._scrollToInAxis(
|
||||
elementPos.top,
|
||||
scrollerPos.top,
|
||||
'scrollTop',
|
||||
offset,
|
||||
ignoreVisible,
|
||||
speed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,8 +205,16 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private _initScrollbar(): void {
|
||||
if (this._ps || this._platform.ANDROID || this._platform.IOS || !this._platform.isBrowser) return;
|
||||
this._ps = new PerfectScrollbar(this._elementRef.nativeElement, { ...this._options });
|
||||
if (
|
||||
this._ps ||
|
||||
this._platform.ANDROID ||
|
||||
this._platform.IOS ||
|
||||
!this._platform.isBrowser
|
||||
)
|
||||
return;
|
||||
this._ps = new PerfectScrollbar(this._elementRef.nativeElement, {
|
||||
...this._options,
|
||||
});
|
||||
}
|
||||
|
||||
private _destroyScrollbar(): void {
|
||||
@@ -198,7 +235,8 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
||||
ignoreVisible: boolean,
|
||||
speed?: number
|
||||
): void {
|
||||
if (ignoreVisible && elementPos <= scrollerPos - Math.abs(offset)) return;
|
||||
if (ignoreVisible && elementPos <= scrollerPos - Math.abs(offset))
|
||||
return;
|
||||
|
||||
const currentPos = this._elementRef.nativeElement[target];
|
||||
const position = elementPos - scrollerPos + currentPos;
|
||||
@@ -213,7 +251,9 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
||||
|
||||
const step = (newTimestamp: number) => {
|
||||
scrollCount += Math.PI / (speed / (newTimestamp - oldTimestamp));
|
||||
const newValue = Math.round(value + cosParameter + cosParameter * Math.cos(scrollCount));
|
||||
const newValue = Math.round(
|
||||
value + cosParameter + cosParameter * Math.cos(scrollCount)
|
||||
);
|
||||
|
||||
if (this._elementRef.nativeElement[target] === oldValue) {
|
||||
if (scrollCount >= Math.PI) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ANGOR_MOCK_API_DEFAULT_DELAY } from '@angor/lib/mock-api/mock-api.constants';
|
||||
import { AngorMockApiService } from '@angor/lib/mock-api/mock-api.service';
|
||||
import {
|
||||
HttpErrorResponse,
|
||||
HttpEvent,
|
||||
@@ -6,8 +8,6 @@ import {
|
||||
HttpResponse,
|
||||
} from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { ANGOR_MOCK_API_DEFAULT_DELAY } from '@angor/lib/mock-api/mock-api.constants';
|
||||
import { AngorMockApiService } from '@angor/lib/mock-api/mock-api.service';
|
||||
import { Observable, delay, of, switchMap, throwError } from 'rxjs';
|
||||
import { AngorMockApiMethods } from './mock-api.types';
|
||||
|
||||
@@ -43,11 +43,14 @@ export const mockApiInterceptor = (
|
||||
switchMap((response) => {
|
||||
// If no response is returned, generate a 404 error
|
||||
if (!response) {
|
||||
return throwError(() => new HttpErrorResponse({
|
||||
error: 'NOT FOUND',
|
||||
status: 404,
|
||||
statusText: 'NOT FOUND',
|
||||
}));
|
||||
return throwError(
|
||||
() =>
|
||||
new HttpErrorResponse({
|
||||
error: 'NOT FOUND',
|
||||
status: 404,
|
||||
statusText: 'NOT FOUND',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Parse the response data (status and body)
|
||||
@@ -58,19 +61,24 @@ export const mockApiInterceptor = (
|
||||
|
||||
// If status code is between 200 and 300, return a successful response
|
||||
if (data.status >= 200 && data.status < 300) {
|
||||
return of(new HttpResponse({
|
||||
body: data.body,
|
||||
status: data.status,
|
||||
statusText: 'OK',
|
||||
}));
|
||||
return of(
|
||||
new HttpResponse({
|
||||
body: data.body,
|
||||
status: data.status,
|
||||
statusText: 'OK',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// For other status codes, throw an error response
|
||||
return throwError(() => new HttpErrorResponse({
|
||||
error: data.body?.error,
|
||||
status: data.status,
|
||||
statusText: 'ERROR',
|
||||
}));
|
||||
return throwError(
|
||||
() =>
|
||||
new HttpErrorResponse({
|
||||
error: data.body?.error,
|
||||
status: data.status,
|
||||
statusText: 'ERROR',
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpRequest } from '@angular/common/http';
|
||||
import { AngorMockApiReplyCallback } from '@angor/lib/mock-api/mock-api.types';
|
||||
import { HttpRequest } from '@angular/common/http';
|
||||
import { Observable, of, take, throwError } from 'rxjs';
|
||||
|
||||
export class AngorMockApiHandler {
|
||||
@@ -17,7 +17,10 @@ export class AngorMockApiHandler {
|
||||
* @param url - The URL for the mock API handler
|
||||
* @param delay - Optional delay for the response
|
||||
*/
|
||||
constructor(public url: string, public delay?: number) {}
|
||||
constructor(
|
||||
public url: string,
|
||||
public delay?: number
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Getter for the response observable.
|
||||
@@ -27,12 +30,16 @@ export class AngorMockApiHandler {
|
||||
get response(): Observable<any> {
|
||||
// Check if the execution limit has been reached
|
||||
if (this._replyCount > 0 && this._replyCount <= this._replied) {
|
||||
return throwError(() => new Error('Execution limit has been reached!'));
|
||||
return throwError(
|
||||
() => new Error('Execution limit has been reached!')
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure the response callback exists
|
||||
if (!this._reply) {
|
||||
return throwError(() => new Error('Response callback function does not exist!'));
|
||||
return throwError(
|
||||
() => new Error('Response callback function does not exist!')
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure the request exists
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AngorMockApiHandler } from '@angor/lib/mock-api/mock-api.request-handler';
|
||||
import { AngorMockApiMethods } from '@angor/lib/mock-api/mock-api.types';
|
||||
import { compact, fromPairs } from 'lodash-es';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { fromPairs } from 'lodash-es';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AngorMockApiService {
|
||||
private readonly _handlers: Record<AngorMockApiMethods, Map<string, AngorMockApiHandler>> = {
|
||||
private readonly _handlers: Record<
|
||||
AngorMockApiMethods,
|
||||
Map<string, AngorMockApiHandler>
|
||||
> = {
|
||||
get: new Map<string, AngorMockApiHandler>(),
|
||||
post: new Map<string, AngorMockApiHandler>(),
|
||||
patch: new Map<string, AngorMockApiHandler>(),
|
||||
@@ -26,24 +29,36 @@ export class AngorMockApiService {
|
||||
findHandler(
|
||||
method: AngorMockApiMethods,
|
||||
url: string
|
||||
): { handler: AngorMockApiHandler | undefined; urlParams: Record<string, string> } {
|
||||
const matchingHandler = { handler: undefined, urlParams: {} as Record<string, string> };
|
||||
): {
|
||||
handler: AngorMockApiHandler | undefined;
|
||||
urlParams: Record<string, string>;
|
||||
} {
|
||||
const matchingHandler = {
|
||||
handler: undefined,
|
||||
urlParams: {} as Record<string, string>,
|
||||
};
|
||||
const urlParts = url.split('/');
|
||||
const handlers = this._handlers[method.toLowerCase() as AngorMockApiMethods];
|
||||
const handlers =
|
||||
this._handlers[method.toLowerCase() as AngorMockApiMethods];
|
||||
|
||||
for (const [handlerUrl, handler] of handlers) {
|
||||
const handlerUrlParts = handlerUrl.split('/');
|
||||
|
||||
if (urlParts.length === handlerUrlParts.length) {
|
||||
const matches = handlerUrlParts.every((part, index) =>
|
||||
part.startsWith(':') || part === urlParts[index]
|
||||
const matches = handlerUrlParts.every(
|
||||
(part, index) =>
|
||||
part.startsWith(':') || part === urlParts[index]
|
||||
);
|
||||
|
||||
if (matches) {
|
||||
matchingHandler.handler = handler;
|
||||
matchingHandler.urlParams = fromPairs(
|
||||
handlerUrlParts
|
||||
.map((part, index) => (part.startsWith(':') ? [part.substring(1), urlParts[index]] : undefined))
|
||||
.map((part, index) =>
|
||||
part.startsWith(':')
|
||||
? [part.substring(1), urlParts[index]]
|
||||
: undefined
|
||||
)
|
||||
.filter(Boolean)
|
||||
);
|
||||
break;
|
||||
@@ -150,7 +165,11 @@ export class AngorMockApiService {
|
||||
* @param delay - (Optional) Delay for the response in milliseconds
|
||||
* @returns An instance of AngorMockApiHandler
|
||||
*/
|
||||
private _registerHandler(method: AngorMockApiMethods, url: string, delay?: number): AngorMockApiHandler {
|
||||
private _registerHandler(
|
||||
method: AngorMockApiMethods,
|
||||
url: string,
|
||||
delay?: number
|
||||
): AngorMockApiHandler {
|
||||
const handler = new AngorMockApiHandler(url, delay);
|
||||
this._handlers[method].set(url, handler);
|
||||
return handler;
|
||||
|
||||
@@ -18,7 +18,11 @@ export class AngorFindByKeyPipe implements PipeTransform {
|
||||
* @param source The array of objects to search within.
|
||||
* @returns A single object if `value` is a string, or an array of objects if `value` is an array.
|
||||
*/
|
||||
transform(value: string | string[], key: string, source: any[]): any | any[] {
|
||||
transform(
|
||||
value: string | string[],
|
||||
key: string,
|
||||
source: any[]
|
||||
): any | any[] {
|
||||
// If value is an array of strings, map each to its corresponding object in the source.
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) =>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { ANGOR_CONFIG } from '@angor/services/config/config.constants';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { merge } from 'lodash-es';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AngorConfigService {
|
||||
private readonly _defaultConfig = inject(ANGOR_CONFIG);
|
||||
private readonly _configSubject = new BehaviorSubject<any>(this._defaultConfig);
|
||||
private readonly _configSubject = new BehaviorSubject<any>(
|
||||
this._defaultConfig
|
||||
);
|
||||
|
||||
/**
|
||||
* Getter for config as an Observable.
|
||||
|
||||
@@ -9,9 +9,9 @@ export type Themes = Array<{ id: string; name: string }>;
|
||||
* This ensures consistency when defining or updating app settings.
|
||||
*/
|
||||
export interface AngorConfig {
|
||||
layout: string; // Layout type (e.g., 'vertical', 'horizontal')
|
||||
scheme: Scheme; // Color scheme: 'auto', 'dark', or 'light'
|
||||
screens: Screens; // Screen breakpoints, e.g., { 'xs': '600px', ... }
|
||||
theme: Theme; // Active theme identifier, e.g., 'theme-default'
|
||||
themes: Themes; // List of available themes, each with an id and name
|
||||
layout: string; // Layout type (e.g., 'vertical', 'horizontal')
|
||||
scheme: Scheme; // Color scheme: 'auto', 'dark', or 'light'
|
||||
screens: Screens; // Screen breakpoints, e.g., { 'xs': '600px', ... }
|
||||
theme: Theme; // Active theme identifier, e.g., 'theme-default'
|
||||
themes: Themes; // List of available themes, each with an id and name
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import { AngorConfirmationConfig } from '@angor/services/confirmation/confirmation.types';
|
||||
import { AngorConfirmationDialogComponent } from '@angor/services/confirmation/dialog/dialog.component';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import { merge } from 'lodash-es';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AngorConfirmationConfig } from '@angor/services/confirmation/confirmation.types';
|
||||
import { NgClass } from '@angular/common';
|
||||
import { Component, ViewEncapsulation, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { AngorConfirmationConfig } from '@angor/services/confirmation/confirmation.types';
|
||||
|
||||
@Component({
|
||||
selector: 'angor-confirmation-dialog',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AngorLoadingService } from '@angor/services/loading/loading.service';
|
||||
import { HttpEvent, HttpHandlerFn, HttpRequest } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { AngorLoadingService } from '@angor/services/loading/loading.service';
|
||||
import { Observable, finalize, take } from 'rxjs';
|
||||
|
||||
export const angorLoadingInterceptor = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AngorConfigService } from '@angor/services/config';
|
||||
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { AngorConfigService } from '@angor/services/config';
|
||||
import { fromPairs } from 'lodash-es';
|
||||
import { Observable, ReplaySubject, map, switchMap } from 'rxjs';
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
/* ----------------------------------------------------------------------------------------------------- */
|
||||
/* @ Example viewer
|
||||
/* ----------------------------------------------------------------------------------------------------- */
|
||||
.example-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -80,7 +80,6 @@ $dark-base: (
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
/* Include the core Angular Material styles */
|
||||
@include mat.core();
|
||||
|
||||
|
||||
@@ -13,17 +13,6 @@ const jsonToSassMap = require(
|
||||
path.resolve(__dirname, '../utils/json-to-sass-map')
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Utilities
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Normalizes the provided theme by omitting empty values and values that
|
||||
* start with "on" from each palette. Also sets the correct DEFAULT value
|
||||
* of each palette.
|
||||
*
|
||||
* @param theme
|
||||
*/
|
||||
const normalizeTheme = (theme) => {
|
||||
return _.fromPairs(
|
||||
_.map(
|
||||
@@ -43,17 +32,10 @@ const normalizeTheme = (theme) => {
|
||||
);
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ ANGOR TailwindCSS Main Plugin
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const theming = plugin.withOptions(
|
||||
(options) =>
|
||||
({ addComponents, e, theme }) => {
|
||||
/**
|
||||
* Create user themes object by going through the provided themes and
|
||||
* merging them with the provided "default" so, we can have a complete
|
||||
* set of color palettes for each user theme.
|
||||
*/
|
||||
|
||||
const userThemes = _.fromPairs(
|
||||
_.map(options.themes, (theme, themeName) => [
|
||||
themeName,
|
||||
@@ -61,10 +43,6 @@ const theming = plugin.withOptions(
|
||||
])
|
||||
);
|
||||
|
||||
/**
|
||||
* Normalize the themes and assign it to the themes object. This will
|
||||
* be the final object that we create a SASS map from
|
||||
*/
|
||||
let themes = _.fromPairs(
|
||||
_.map(userThemes, (theme, themeName) => [
|
||||
themeName,
|
||||
@@ -72,10 +50,6 @@ const theming = plugin.withOptions(
|
||||
])
|
||||
);
|
||||
|
||||
/**
|
||||
* Go through the themes to generate the contrasts and filter the
|
||||
* palettes to only have "primary", "accent" and "warn" objects.
|
||||
*/
|
||||
themes = _.fromPairs(
|
||||
_.map(themes, (theme, themeName) => [
|
||||
themeName,
|
||||
@@ -105,10 +79,6 @@ const theming = plugin.withOptions(
|
||||
])
|
||||
);
|
||||
|
||||
/**
|
||||
* Go through the themes and attach appropriate class selectors so,
|
||||
* we can use them to encapsulate each theme.
|
||||
*/
|
||||
themes = _.fromPairs(
|
||||
_.map(themes, (theme, themeName) => [
|
||||
themeName,
|
||||
@@ -119,18 +89,15 @@ const theming = plugin.withOptions(
|
||||
])
|
||||
);
|
||||
|
||||
/* Generate the SASS map using the themes object */
|
||||
const sassMap = jsonToSassMap(
|
||||
JSON.stringify({ 'user-themes': themes })
|
||||
);
|
||||
|
||||
/* Get the file path */
|
||||
const filename = path.resolve(
|
||||
__dirname,
|
||||
'../../styles/user-themes.scss'
|
||||
);
|
||||
|
||||
/* Read the file and get its data */
|
||||
let data;
|
||||
try {
|
||||
data = fs.readFileSync(filename, { encoding: 'utf8' });
|
||||
@@ -138,7 +105,6 @@ const theming = plugin.withOptions(
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
/* Write the file if the map has been changed */
|
||||
if (data !== sassMap) {
|
||||
try {
|
||||
fs.writeFileSync(filename, sassMap, { encoding: 'utf8' });
|
||||
@@ -147,11 +113,6 @@ const theming = plugin.withOptions(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through the user's themes and build Tailwind components containing
|
||||
* CSS Custom Properties using the colors from them. This allows switching
|
||||
* themes by simply replacing a class name as well as nesting them.
|
||||
*/
|
||||
addComponents(
|
||||
_.fromPairs(
|
||||
_.map(options.themes, (theme, themeName) => [
|
||||
@@ -214,9 +175,7 @@ const theming = plugin.withOptions(
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Generate scheme based css custom properties and utility classes
|
||||
*/
|
||||
|
||||
const schemeCustomProps = _.map(
|
||||
['light', 'dark'],
|
||||
(colorScheme) => {
|
||||
@@ -234,31 +193,9 @@ const theming = plugin.withOptions(
|
||||
|
||||
return {
|
||||
[isDark ? darkSchemeSelectors : lightSchemeSelectors]: {
|
||||
/**
|
||||
* If a custom property is not available, browsers will use
|
||||
* the fallback value. In this case, we want to use '--is-dark'
|
||||
* as the indicator of a dark theme so, we can use it like this:
|
||||
* background-color: var(--is-dark, red);
|
||||
*
|
||||
* If we set '--is-dark' as "true" on dark themes, the above rule
|
||||
* won't work because of the said "fallback value" logic. Therefore,
|
||||
* we set the '--is-dark' to "false" on light themes and not set it
|
||||
* at all on dark themes so that the fallback value can be used on
|
||||
* dark themes.
|
||||
*
|
||||
* On light themes, since '--is-dark' exists, the above rule will be
|
||||
* interpolated as:
|
||||
* "background-color: false"
|
||||
*
|
||||
* On dark themes, since '--is-dark' doesn't exist, the fallback value
|
||||
* will be used ('red' in this case) and the rule will be interpolated as:
|
||||
* "background-color: red"
|
||||
*
|
||||
* It's easier to understand and remember like this.
|
||||
*/
|
||||
|
||||
...(!isDark ? { '--is-dark': 'false' } : {}),
|
||||
|
||||
/* Generate custom properties from customProps */
|
||||
..._.fromPairs(
|
||||
_.flatten(
|
||||
_.map(background, (value, key) => [
|
||||
@@ -287,7 +224,6 @@ const theming = plugin.withOptions(
|
||||
);
|
||||
|
||||
const schemeUtilities = (() => {
|
||||
/* Generate general styles & utilities */
|
||||
return {};
|
||||
})();
|
||||
|
||||
@@ -298,11 +234,6 @@ const theming = plugin.withOptions(
|
||||
return {
|
||||
theme: {
|
||||
extend: {
|
||||
/**
|
||||
* Add 'Primary', 'Accent' and 'Warn' palettes as colors so all color utilities
|
||||
* are generated for them; "bg-primary", "text-on-primary", "bg-accent-600" etc.
|
||||
* This will also allow using arbitrary values with them such as opacity and such.
|
||||
*/
|
||||
colors: _.fromPairs(
|
||||
_.flatten(
|
||||
_.map(
|
||||
@@ -398,7 +329,6 @@ const theming = plugin.withOptions(
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
const plugin = require('tailwindcss/plugin');
|
||||
|
||||
module.exports = plugin(({ addComponents }) => {
|
||||
/*
|
||||
* Add base components. These are very important for everything to look
|
||||
* correct. We are adding these to the 'components' layer because they must
|
||||
* be defined before pretty much everything else.
|
||||
*/
|
||||
addComponents({
|
||||
'.mat-icon': {
|
||||
'--tw-text-opacity': '1',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex: 1 1 auto; // Flex-grow, flex-shrink, and basis set to auto
|
||||
width: 100%; // Full width of the container
|
||||
height: 100%; // Full height of the container
|
||||
flex: 1 1 auto; // Flex-grow, flex-shrink, and basis set to auto
|
||||
width: 100%; // Full width of the container
|
||||
height: 100%; // Full height of the container
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { provideAngor } from '@angor';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { APP_INITIALIZER, ApplicationConfig, inject, isDevMode } from '@angular/core';
|
||||
import {
|
||||
APP_INITIALIZER,
|
||||
ApplicationConfig,
|
||||
inject,
|
||||
isDevMode,
|
||||
} from '@angular/core';
|
||||
import { LuxonDateAdapter } from '@angular/material-luxon-adapter';
|
||||
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
@@ -9,20 +15,18 @@ import {
|
||||
withInMemoryScrolling,
|
||||
withPreloading,
|
||||
} from '@angular/router';
|
||||
import { provideAngor } from '@angor';
|
||||
import { provideServiceWorker } from '@angular/service-worker';
|
||||
import { TranslocoService, provideTransloco } from '@ngneat/transloco';
|
||||
import { WebLNProvider } from '@webbtc/webln-types';
|
||||
import { appRoutes } from 'app/app.routes';
|
||||
import { provideIcons } from 'app/core/icons/icons.provider';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { TranslocoHttpLoader } from './core/transloco/transloco.http-loader';
|
||||
import { provideServiceWorker } from '@angular/service-worker';
|
||||
import { HashService } from './services/hash.service';
|
||||
import { navigationServices } from './layout/navigation/navigation.services';
|
||||
import { WebLNProvider } from '@webbtc/webln-types';
|
||||
import { HashService } from './services/hash.service';
|
||||
import { NostrWindow } from './types/nostr';
|
||||
|
||||
export function initializeApp(hashService: HashService) {
|
||||
console.log('initializeApp. Getting hashService.load.');
|
||||
return (): Promise<void> => hashService.load();
|
||||
}
|
||||
export const appConfig: ApplicationConfig = {
|
||||
@@ -31,7 +35,7 @@ export const appConfig: ApplicationConfig = {
|
||||
provideHttpClient(),
|
||||
provideServiceWorker('ngsw-worker.js', {
|
||||
enabled: !isDevMode(),
|
||||
registrationStrategy: 'registerWhenStable:30000'
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
@@ -54,12 +58,12 @@ export const appConfig: ApplicationConfig = {
|
||||
provide: MAT_DATE_FORMATS,
|
||||
useValue: {
|
||||
parse: {
|
||||
dateInput: 'D', // Date format for parsing
|
||||
dateInput: 'D', // Date format for parsing
|
||||
},
|
||||
display: {
|
||||
dateInput: 'DDD', // Date format for input display
|
||||
monthYearLabel: 'LLL yyyy', // Format for month-year labels
|
||||
dateA11yLabel: 'DD', // Accessible format for dates
|
||||
dateInput: 'DDD', // Date format for input display
|
||||
monthYearLabel: 'LLL yyyy', // Format for month-year labels
|
||||
dateA11yLabel: 'DD', // Accessible format for dates
|
||||
monthYearA11yLabel: 'LLLL yyyy', // Accessible format for month-year
|
||||
},
|
||||
},
|
||||
@@ -72,7 +76,7 @@ export const appConfig: ApplicationConfig = {
|
||||
{
|
||||
id: 'en',
|
||||
label: 'English',
|
||||
}
|
||||
},
|
||||
],
|
||||
defaultLang: 'en',
|
||||
fallbackLang: 'en',
|
||||
@@ -121,13 +125,11 @@ export const appConfig: ApplicationConfig = {
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
||||
],
|
||||
};
|
||||
declare global {
|
||||
interface Window {
|
||||
webln?: WebLNProvider;
|
||||
nostr?: NostrWindow;
|
||||
webln?: WebLNProvider;
|
||||
nostr?: NostrWindow;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ import { forkJoin } from 'rxjs';
|
||||
*/
|
||||
export const initialDataResolver = () => {
|
||||
const navigationService = inject(NavigationService);
|
||||
const quickChatService = inject(QuickChatService);
|
||||
const quickChatService = inject(QuickChatService);
|
||||
|
||||
// Combine API calls into a single observable
|
||||
return forkJoin([
|
||||
navigationService.get(), // Fetch navigation data
|
||||
navigationService.get(), // Fetch navigation data
|
||||
// quickChatService.getChats(), // Fetch chat data
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { initialDataResolver } from 'app/app.resolvers';
|
||||
import { LayoutComponent } from 'app/layout/layout.component';
|
||||
import { LayoutComponent } from 'app/layout/layout.component';
|
||||
import { authGuard } from './core/auth/auth.guard';
|
||||
|
||||
/**
|
||||
* Application routes configuration
|
||||
*/
|
||||
export const appRoutes: Route[] = [
|
||||
|
||||
// Redirect root path to '/explore'
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'home'
|
||||
redirectTo: 'home',
|
||||
},
|
||||
|
||||
{
|
||||
path: 'project/:pubkey',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'explore'
|
||||
redirectTo: 'explore',
|
||||
},
|
||||
|
||||
// Redirect login user to '/explore'
|
||||
{
|
||||
path: 'login-redirect',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'explore'
|
||||
redirectTo: 'explore',
|
||||
},
|
||||
|
||||
// Routes for guests
|
||||
@@ -36,13 +35,15 @@ export const appRoutes: Route[] = [
|
||||
children: [
|
||||
{
|
||||
path: 'login',
|
||||
loadChildren: () => import('app/components/auth/login/login.routes')
|
||||
loadChildren: () =>
|
||||
import('app/components/auth/login/login.routes'),
|
||||
},
|
||||
{
|
||||
path: 'register',
|
||||
loadChildren: () => import('app/components/auth/register/register.routes')
|
||||
}
|
||||
]
|
||||
loadChildren: () =>
|
||||
import('app/components/auth/register/register.routes'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Routes for authenticated users
|
||||
@@ -55,13 +56,12 @@ export const appRoutes: Route[] = [
|
||||
children: [
|
||||
{
|
||||
path: 'logout',
|
||||
loadChildren: () => import('app/components/auth/logout/logout.routes')
|
||||
}
|
||||
]
|
||||
loadChildren: () =>
|
||||
import('app/components/auth/logout/logout.routes'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
|
||||
// Authenticated routes for Angor
|
||||
{
|
||||
path: '',
|
||||
@@ -72,41 +72,49 @@ export const appRoutes: Route[] = [
|
||||
children: [
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () => import('app/components/home/home.routes')
|
||||
loadChildren: () => import('app/components/home/home.routes'),
|
||||
},
|
||||
{
|
||||
path: 'explore',
|
||||
loadChildren: () => import('app/components/explore/explore.routes')
|
||||
loadChildren: () =>
|
||||
import('app/components/explore/explore.routes'),
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
loadChildren: () => import('app/components/profile/profile.routes')
|
||||
loadChildren: () =>
|
||||
import('app/components/profile/profile.routes'),
|
||||
},
|
||||
{
|
||||
path: 'profile/:pubkey',
|
||||
loadChildren: () => import('app/components/profile/profile.routes')
|
||||
loadChildren: () =>
|
||||
import('app/components/profile/profile.routes'),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
loadChildren: () => import('app/components/settings/settings.routes')
|
||||
loadChildren: () =>
|
||||
import('app/components/settings/settings.routes'),
|
||||
},
|
||||
{
|
||||
path: 'settings/:id',
|
||||
loadChildren: () => import('app/components/settings/settings.routes')
|
||||
loadChildren: () =>
|
||||
import('app/components/settings/settings.routes'),
|
||||
},
|
||||
{
|
||||
path: 'chat',
|
||||
loadChildren: () => import('app/components/chat/chat.routes')
|
||||
loadChildren: () => import('app/components/chat/chat.routes'),
|
||||
},
|
||||
{
|
||||
path: '404-not-found',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('app/components/pages/error/error-404/error-404.routes')
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'app/components/pages/error/error-404/error-404.routes'
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '404-not-found'
|
||||
}
|
||||
]
|
||||
}
|
||||
redirectTo: '404-not-found',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
<div class="flex min-w-0 flex-auto flex-col items-center sm:flex-row sm:justify-center md:items-start md:justify-start">
|
||||
<div
|
||||
class="flex min-w-0 flex-auto flex-col items-center sm:flex-row sm:justify-center md:items-start md:justify-start"
|
||||
>
|
||||
<div
|
||||
class="w-full px-4 py-8 sm:bg-card sm:w-auto sm:rounded-2xl sm:p-12 sm:shadow md:flex md:h-full md:w-1/2 md:items-center md:justify-end md:rounded-none md:p-16 md:shadow-none">
|
||||
class="w-full px-4 py-8 sm:bg-card sm:w-auto sm:rounded-2xl sm:p-12 sm:shadow md:flex md:h-full md:w-1/2 md:items-center md:justify-end md:rounded-none md:p-16 md:shadow-none"
|
||||
>
|
||||
<div class="mx-auto w-full max-w-80 sm:mx-0 sm:w-80">
|
||||
<!-- Title -->
|
||||
<div class="mt-8 text-4xl font-extrabold leading-tight tracking-tight">
|
||||
<div
|
||||
class="mt-8 text-4xl font-extrabold leading-tight tracking-tight"
|
||||
>
|
||||
Login
|
||||
</div>
|
||||
<div class="mt-0.5 flex items-baseline font-medium">
|
||||
<div>Don't have an account?</div>
|
||||
<a class="ml-1 text-primary-500 hover:underline" [routerLink]="['/register']">Register</a>
|
||||
<a
|
||||
class="ml-1 text-primary-500 hover:underline"
|
||||
[routerLink]="['/register']"
|
||||
>Register</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<angor-alert *ngIf="showSecAlert" class="mt-8" [appearance]="'outline'" [showIcon]="false" [type]="secAlert.type"
|
||||
[@shake]="secAlert.type === 'error'">
|
||||
<angor-alert
|
||||
*ngIf="showSecAlert"
|
||||
class="mt-8"
|
||||
[appearance]="'outline'"
|
||||
[showIcon]="false"
|
||||
[type]="secAlert.type"
|
||||
[@shake]="secAlert.type === 'error'"
|
||||
>
|
||||
{{ secAlert.message }}
|
||||
</angor-alert>
|
||||
|
||||
@@ -26,8 +41,16 @@
|
||||
|
||||
<!-- extension login buttons -->
|
||||
<div class="mt-8 flex items-center space-x-4">
|
||||
<button class="flex-auto space-x-2" type="button" mat-stroked-button (click)="loginWithNostrExtension()">
|
||||
<mat-icon class="icon-size-5" [svgIcon]="'feather:zap'"></mat-icon>
|
||||
<button
|
||||
class="flex-auto space-x-2"
|
||||
type="button"
|
||||
mat-stroked-button
|
||||
(click)="loginWithNostrExtension()"
|
||||
>
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'feather:zap'"
|
||||
></mat-icon>
|
||||
<span>Login with Nostr Extension</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -40,7 +63,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Login form with Secret Key -->
|
||||
<form class="mt-8" [formGroup]="SecretKeyLoginForm" (ngSubmit)="loginWithSecretKey()">
|
||||
<form
|
||||
class="mt-8"
|
||||
[formGroup]="SecretKeyLoginForm"
|
||||
(ngSubmit)="loginWithSecretKey()"
|
||||
>
|
||||
<!-- secret key field -->
|
||||
<div class="mt-8 flex items-center">
|
||||
<div class="mt-px flex-auto border-t"></div>
|
||||
@@ -49,8 +76,14 @@
|
||||
</div>
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Secret Key</mat-label>
|
||||
<input matInput formControlName="secretKey" autocomplete="secretKey" />
|
||||
@if (SecretKeyLoginForm.get('secretKey').hasError('required')) {
|
||||
<input
|
||||
matInput
|
||||
formControlName="secretKey"
|
||||
autocomplete="secretKey"
|
||||
/>
|
||||
@if (
|
||||
SecretKeyLoginForm.get('secretKey').hasError('required')
|
||||
) {
|
||||
<mat-error> Secret key is required </mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@@ -58,47 +91,94 @@
|
||||
<!-- Password field -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput type="password" [formControlName]="'password'" autocomplete="current-password-seckey" #secretPasswordField />
|
||||
<button mat-icon-button type="button" (click)="
|
||||
secretPasswordField.type === 'password'
|
||||
? (secretPasswordField.type = 'text')
|
||||
: (secretPasswordField.type = 'password')
|
||||
" matSuffix>
|
||||
<mat-icon *ngIf="secretPasswordField.type === 'password'" class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:eye'"></mat-icon>
|
||||
<mat-icon *ngIf="secretPasswordField.type === 'text'" class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:eye-slash'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
type="password"
|
||||
[formControlName]="'password'"
|
||||
autocomplete="current-password-seckey"
|
||||
#secretPasswordField
|
||||
/>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
(click)="
|
||||
secretPasswordField.type === 'password'
|
||||
? (secretPasswordField.type = 'text')
|
||||
: (secretPasswordField.type = 'password')
|
||||
"
|
||||
matSuffix
|
||||
>
|
||||
<mat-icon
|
||||
*ngIf="secretPasswordField.type === 'password'"
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:eye'"
|
||||
></mat-icon>
|
||||
<mat-icon
|
||||
*ngIf="secretPasswordField.type === 'text'"
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:eye-slash'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="SecretKeyLoginForm.get('password').hasError('required')"> Password is required
|
||||
<mat-error
|
||||
*ngIf="
|
||||
SecretKeyLoginForm.get('password').hasError(
|
||||
'required'
|
||||
)
|
||||
"
|
||||
>
|
||||
Password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Submit button -->
|
||||
<button class="angor-mat-button-large mt-6 w-full" mat-flat-button color="primary"
|
||||
[disabled]="SecretKeyLoginForm.invalid">
|
||||
<button
|
||||
class="angor-mat-button-large mt-6 w-full"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
[disabled]="SecretKeyLoginForm.invalid"
|
||||
>
|
||||
<span *ngIf="!loading">Login</span>
|
||||
<mat-progress-spinner *ngIf="loading" diameter="24" mode="indeterminate"></mat-progress-spinner>
|
||||
<mat-progress-spinner
|
||||
*ngIf="loading"
|
||||
diameter="24"
|
||||
mode="indeterminate"
|
||||
></mat-progress-spinner>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
<div class="mt-8 flex items-center">
|
||||
<div class="mt-px flex-auto border-t"></div>
|
||||
<div class="text-secondary mx-2">Or enter menemonic</div>
|
||||
<div class="mt-px flex-auto border-t"></div>
|
||||
</div>
|
||||
|
||||
<angor-alert *ngIf="showMenemonicAlert" class="mt-8" [appearance]="'outline'" [showIcon]="false" [type]="menemonicAlert.type"
|
||||
[@shake]="menemonicAlert.type === 'error'">
|
||||
<angor-alert
|
||||
*ngIf="showMenemonicAlert"
|
||||
class="mt-8"
|
||||
[appearance]="'outline'"
|
||||
[showIcon]="false"
|
||||
[type]="menemonicAlert.type"
|
||||
[@shake]="menemonicAlert.type === 'error'"
|
||||
>
|
||||
{{ menemonicAlert.message }}
|
||||
</angor-alert>
|
||||
<!-- Login form with Menemonic -->
|
||||
<form class="mt-8" [formGroup]="MenemonicLoginForm" (ngSubmit)="loginWithMenemonic()">
|
||||
<form
|
||||
class="mt-8"
|
||||
[formGroup]="MenemonicLoginForm"
|
||||
(ngSubmit)="loginWithMenemonic()"
|
||||
>
|
||||
<!-- Menemonic field -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Menemonic</mat-label>
|
||||
<input matInput formControlName="menemonic" autocomplete="menemonic" />
|
||||
@if (MenemonicLoginForm.get('menemonic').hasError('required')) {
|
||||
<input
|
||||
matInput
|
||||
formControlName="menemonic"
|
||||
autocomplete="menemonic"
|
||||
/>
|
||||
@if (
|
||||
MenemonicLoginForm.get('menemonic').hasError('required')
|
||||
) {
|
||||
<mat-error> Menemonic is required </mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@@ -106,66 +186,156 @@
|
||||
<!-- Passphrase field -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Passphrase (Optional)</mat-label>
|
||||
<input matInput type="password" [formControlName]="'passphrase'" autocomplete="current-passphrase-menemonic"
|
||||
#passphraseField />
|
||||
<button mat-icon-button type="button" (click)="
|
||||
passphraseField.type === 'password'
|
||||
? (passphraseField.type = 'text')
|
||||
: (passphraseField.type = 'password')
|
||||
" matSuffix>
|
||||
<mat-icon *ngIf="passphraseField.type === 'password'" class="icon-size-5" [svgIcon]="'heroicons_solid:eye'"></mat-icon>
|
||||
<mat-icon *ngIf="passphraseField.type === 'text'" class="icon-size-5" [svgIcon]="'heroicons_solid:eye-slash'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
type="password"
|
||||
[formControlName]="'passphrase'"
|
||||
autocomplete="current-passphrase-menemonic"
|
||||
#passphraseField
|
||||
/>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
(click)="
|
||||
passphraseField.type === 'password'
|
||||
? (passphraseField.type = 'text')
|
||||
: (passphraseField.type = 'password')
|
||||
"
|
||||
matSuffix
|
||||
>
|
||||
<mat-icon
|
||||
*ngIf="passphraseField.type === 'password'"
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:eye'"
|
||||
></mat-icon>
|
||||
<mat-icon
|
||||
*ngIf="passphraseField.type === 'text'"
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:eye-slash'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="MenemonicLoginForm.get('passphrase').hasError('required')"> Passphrase is required
|
||||
<mat-error
|
||||
*ngIf="
|
||||
MenemonicLoginForm.get('passphrase').hasError(
|
||||
'required'
|
||||
)
|
||||
"
|
||||
>
|
||||
Passphrase is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Password field -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput type="password" [formControlName]="'password'" autocomplete="current-password-menemonic"
|
||||
#menemonicPasswordField />
|
||||
<button mat-icon-button type="button" (click)="
|
||||
menemonicPasswordField.type === 'password'
|
||||
? (menemonicPasswordField.type = 'text')
|
||||
: (menemonicPasswordField.type = 'password')
|
||||
" matSuffix>
|
||||
<mat-icon *ngIf="menemonicPasswordField.type === 'password'" class="icon-size-5" [svgIcon]="'heroicons_solid:eye'"></mat-icon>
|
||||
<mat-icon *ngIf="menemonicPasswordField.type === 'text'" class="icon-size-5" [svgIcon]="'heroicons_solid:eye-slash'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
type="password"
|
||||
[formControlName]="'password'"
|
||||
autocomplete="current-password-menemonic"
|
||||
#menemonicPasswordField
|
||||
/>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
(click)="
|
||||
menemonicPasswordField.type === 'password'
|
||||
? (menemonicPasswordField.type = 'text')
|
||||
: (menemonicPasswordField.type = 'password')
|
||||
"
|
||||
matSuffix
|
||||
>
|
||||
<mat-icon
|
||||
*ngIf="menemonicPasswordField.type === 'password'"
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:eye'"
|
||||
></mat-icon>
|
||||
<mat-icon
|
||||
*ngIf="menemonicPasswordField.type === 'text'"
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:eye-slash'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="MenemonicLoginForm.get('password').hasError('required')"> Password is required
|
||||
<mat-error
|
||||
*ngIf="
|
||||
MenemonicLoginForm.get('password').hasError(
|
||||
'required'
|
||||
)
|
||||
"
|
||||
>
|
||||
Password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Submit button -->
|
||||
<button class="angor-mat-button-large mt-6 w-full" mat-flat-button color="primary"
|
||||
[disabled]="MenemonicLoginForm.invalid">
|
||||
<button
|
||||
class="angor-mat-button-large mt-6 w-full"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
[disabled]="MenemonicLoginForm.invalid"
|
||||
>
|
||||
<span *ngIf="!loading">Login</span>
|
||||
<mat-progress-spinner *ngIf="loading" diameter="24" mode="indeterminate"></mat-progress-spinner>
|
||||
<mat-progress-spinner
|
||||
*ngIf="loading"
|
||||
diameter="24"
|
||||
mode="indeterminate"
|
||||
></mat-progress-spinner>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative hidden h-full w-1/2 flex-auto items-center justify-center overflow-hidden bg-gray-800 p-16 dark:border-l md:flex lg:px-28">
|
||||
|
||||
<svg class="absolute inset-0 pointer-events-none" viewBox="0 0 960 540" width="100%" height="100%"
|
||||
preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="text-gray-700 opacity-25" fill="none" stroke="currentColor" stroke-width="100">
|
||||
class="relative hidden h-full w-1/2 flex-auto items-center justify-center overflow-hidden bg-gray-800 p-16 dark:border-l md:flex lg:px-28"
|
||||
>
|
||||
<svg
|
||||
class="pointer-events-none absolute inset-0"
|
||||
viewBox="0 0 960 540"
|
||||
width="100%"
|
||||
height="100%"
|
||||
preserveAspectRatio="xMidYMax slice"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
class="text-gray-700 opacity-25"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="100"
|
||||
>
|
||||
<circle r="234" cx="196" cy="23"></circle>
|
||||
<circle r="234" cx="790" cy="491"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<svg class="absolute -top-16 -right-16 text-gray-700" viewBox="0 0 220 192" width="220" height="192"
|
||||
fill="none">
|
||||
<svg
|
||||
class="absolute -right-16 -top-16 text-gray-700"
|
||||
viewBox="0 0 220 192"
|
||||
width="220"
|
||||
height="192"
|
||||
fill="none"
|
||||
>
|
||||
<defs>
|
||||
<pattern id="837c3e70-6c3a-44e6-8854-cc48c737b659" x="0" y="0" width="20" height="20"
|
||||
patternUnits="userSpaceOnUse">
|
||||
<rect x="0" y="0" width="4" height="4" fill="currentColor"></rect>
|
||||
<pattern
|
||||
id="837c3e70-6c3a-44e6-8854-cc48c737b659"
|
||||
x="0"
|
||||
y="0"
|
||||
width="20"
|
||||
height="20"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="4"
|
||||
height="4"
|
||||
fill="currentColor"
|
||||
></rect>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="220" height="192" fill="url(#837c3e70-6c3a-44e6-8854-cc48c737b659)"></rect>
|
||||
<rect
|
||||
width="220"
|
||||
height="192"
|
||||
fill="url(#837c3e70-6c3a-44e6-8854-cc48c737b659)"
|
||||
></rect>
|
||||
</svg>
|
||||
<!-- Background and Content -->
|
||||
<div class="relative z-10 w-full max-w-2xl">
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { AngorAlertComponent } from '@angor/components/alert';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -26,7 +32,7 @@ import { SignerService } from 'app/services/signer.service';
|
||||
MatIconModule,
|
||||
MatCheckboxModule,
|
||||
MatProgressSpinnerModule,
|
||||
CommonModule
|
||||
CommonModule,
|
||||
],
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
@@ -41,15 +47,15 @@ export class LoginComponent implements OnInit {
|
||||
loading = false;
|
||||
isInstalledExtension = false;
|
||||
privateKey: Uint8Array = new Uint8Array();
|
||||
publicKey: string = "";
|
||||
npub: string = "";
|
||||
nsec: string = "";
|
||||
publicKey: string = '';
|
||||
npub: string = '';
|
||||
nsec: string = '';
|
||||
|
||||
constructor(
|
||||
private _formBuilder: FormBuilder,
|
||||
private _router: Router,
|
||||
private _signerService: SignerService
|
||||
) { }
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initializeForms();
|
||||
@@ -59,20 +65,25 @@ export class LoginComponent implements OnInit {
|
||||
private initializeForms(): void {
|
||||
this.SecretKeyLoginForm = this._formBuilder.group({
|
||||
secretKey: ['', [Validators.required, Validators.minLength(3)]],
|
||||
password: ['', Validators.required]
|
||||
password: ['', Validators.required],
|
||||
});
|
||||
|
||||
this.MenemonicLoginForm = this._formBuilder.group({
|
||||
menemonic: ['', [Validators.required, Validators.minLength(3)]],
|
||||
passphrase: [''], // Passphrase is optional
|
||||
password: ['', Validators.required]
|
||||
password: ['', Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
private checkNostrExtensionAvailability(): void {
|
||||
const globalContext = globalThis as unknown as { nostr?: { signEvent?: Function } };
|
||||
const globalContext = globalThis as unknown as {
|
||||
nostr?: { signEvent?: Function };
|
||||
};
|
||||
|
||||
if (globalContext.nostr && typeof globalContext.nostr.signEvent === 'function') {
|
||||
if (
|
||||
globalContext.nostr &&
|
||||
typeof globalContext.nostr.signEvent === 'function'
|
||||
) {
|
||||
this.isInstalledExtension = true;
|
||||
} else {
|
||||
this.isInstalledExtension = false;
|
||||
@@ -91,7 +102,10 @@ export class LoginComponent implements OnInit {
|
||||
this.showSecAlert = false;
|
||||
|
||||
try {
|
||||
const success = this._signerService.handleLoginWithKey(secretKey,password); // Updated method to handle both nsec and hex
|
||||
const success = this._signerService.handleLoginWithKey(
|
||||
secretKey,
|
||||
password
|
||||
); // Updated method to handle both nsec and hex
|
||||
|
||||
if (success) {
|
||||
// Successful login
|
||||
@@ -102,26 +116,33 @@ export class LoginComponent implements OnInit {
|
||||
} catch (error) {
|
||||
// Handle login failure
|
||||
this.loading = false;
|
||||
this.secAlert.message = (error instanceof Error) ? error.message : 'An unexpected error occurred.';
|
||||
this.secAlert.message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'An unexpected error occurred.';
|
||||
this.showSecAlert = true;
|
||||
console.error("Login error: ", error);
|
||||
console.error('Login error: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
loginWithMenemonic(): void {
|
||||
if (this.MenemonicLoginForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menemonic = this.MenemonicLoginForm.get('menemonic')?.value;
|
||||
const passphrase = this.MenemonicLoginForm.get('passphrase')?.value || ''; // Optional passphrase
|
||||
const passphrase =
|
||||
this.MenemonicLoginForm.get('passphrase')?.value || ''; // Optional passphrase
|
||||
const password = this.MenemonicLoginForm.get('password')?.value;
|
||||
|
||||
this.loading = true;
|
||||
this.showMenemonicAlert = false;
|
||||
|
||||
const success = this._signerService.handleLoginWithMenemonic(menemonic, passphrase,password);
|
||||
const success = this._signerService.handleLoginWithMenemonic(
|
||||
menemonic,
|
||||
passphrase,
|
||||
password
|
||||
);
|
||||
|
||||
if (success) {
|
||||
this._router.navigateByUrl('/home');
|
||||
@@ -140,5 +161,4 @@ export class LoginComponent implements OnInit {
|
||||
console.error('Failed to log in using Nostr extension');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,9 +27,8 @@
|
||||
<a
|
||||
class="ml-1 text-primary-500 hover:underline"
|
||||
[routerLink]="['/login']"
|
||||
>
|
||||
>
|
||||
<span class="text-2xl font-extrabold">Go to login</span>
|
||||
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,10 @@ export class LogoutComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
constructor(private _router: Router, private _signerService: SignerService) {}
|
||||
constructor(
|
||||
private _router: Router,
|
||||
private _signerService: SignerService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
timer(1000, 1000)
|
||||
@@ -35,7 +38,6 @@ export class LogoutComponent implements OnInit, OnDestroy {
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
@@ -44,6 +46,6 @@ export class LogoutComponent implements OnInit, OnDestroy {
|
||||
logout(): void {
|
||||
this._signerService.clearPassword();
|
||||
this._signerService.logout();
|
||||
console.log("User logged out and keys removed from localStorage.");
|
||||
console.log('User logged out and keys removed from localStorage.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +1,231 @@
|
||||
<div class="flex min-w-0 flex-auto flex-col items-center sm:flex-row sm:justify-center md:items-start md:justify-start">
|
||||
<div
|
||||
class="flex min-w-0 flex-auto flex-col items-center sm:flex-row sm:justify-center md:items-start md:justify-start"
|
||||
>
|
||||
<div
|
||||
class="w-full px-4 py-8 sm:bg-card sm:w-auto sm:rounded-2xl sm:p-12 sm:shadow md:flex md:h-full md:w-1/2 md:items-center md:justify-end md:rounded-none md:p-16 md:shadow-none">
|
||||
class="w-full px-4 py-8 sm:bg-card sm:w-auto sm:rounded-2xl sm:p-12 sm:shadow md:flex md:h-full md:w-1/2 md:items-center md:justify-end md:rounded-none md:p-16 md:shadow-none"
|
||||
>
|
||||
<div class="mx-auto w-full max-w-80 sm:mx-0 sm:w-80">
|
||||
<!-- Title -->
|
||||
<div class="mt-8 text-4xl font-extrabold leading-tight tracking-tight">
|
||||
<div
|
||||
class="mt-8 text-4xl font-extrabold leading-tight tracking-tight"
|
||||
>
|
||||
Register
|
||||
</div>
|
||||
<div class="mt-0.5 flex items-baseline font-medium">
|
||||
<div>Already have an account?</div>
|
||||
<a class="ml-1 text-primary-500 hover:underline" [routerLink]="['/login']">Login
|
||||
<a
|
||||
class="ml-1 text-primary-500 hover:underline"
|
||||
[routerLink]="['/login']"
|
||||
>Login
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Alert -->
|
||||
@if (showAlert) {
|
||||
<angor-alert class="mt-8" [appearance]="'outline'" [showIcon]="false" [type]="alert.type"
|
||||
[@shake]="alert.type === 'error'">
|
||||
{{ alert.message }}
|
||||
</angor-alert>
|
||||
<angor-alert
|
||||
class="mt-8"
|
||||
[appearance]="'outline'"
|
||||
[showIcon]="false"
|
||||
[type]="alert.type"
|
||||
[@shake]="alert.type === 'error'"
|
||||
>
|
||||
{{ alert.message }}
|
||||
</angor-alert>
|
||||
}
|
||||
|
||||
<!-- Register form -->
|
||||
<form class="mt-8" [formGroup]="registerForm" #registerNgForm="ngForm">
|
||||
<form
|
||||
class="mt-8"
|
||||
[formGroup]="registerForm"
|
||||
#registerNgForm="ngForm"
|
||||
>
|
||||
<!-- Name field (secretKey) -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Full name</mat-label>
|
||||
<input id="name" matInput [formControlName]="'name'" autocomplete="name"/>
|
||||
<mat-error *ngIf="registerForm.get('name').hasError('required')"> Full name is required </mat-error>
|
||||
<input
|
||||
id="name"
|
||||
matInput
|
||||
[formControlName]="'name'"
|
||||
autocomplete="name"
|
||||
/>
|
||||
<mat-error
|
||||
*ngIf="registerForm.get('name').hasError('required')"
|
||||
>
|
||||
Full name is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Username field -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Username</mat-label>
|
||||
<input id="username" matInput [formControlName]="'username'" autocomplete="username"/>
|
||||
<mat-error *ngIf="registerForm.get('username').hasError('required')"> Username is required </mat-error>
|
||||
<input
|
||||
id="username"
|
||||
matInput
|
||||
[formControlName]="'username'"
|
||||
autocomplete="username"
|
||||
/>
|
||||
<mat-error
|
||||
*ngIf="
|
||||
registerForm.get('username').hasError('required')
|
||||
"
|
||||
>
|
||||
Username is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- About field -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>About</mat-label>
|
||||
<textarea id="about" matInput [formControlName]="'about'"></textarea>
|
||||
<textarea
|
||||
id="about"
|
||||
matInput
|
||||
[formControlName]="'about'"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Avatar URL field -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Avatar URL</mat-label>
|
||||
<input id="avatarUrl" matInput [formControlName]="'avatarUrl'" autocomplete="avatarUrl" />
|
||||
<input
|
||||
id="avatarUrl"
|
||||
matInput
|
||||
[formControlName]="'avatarUrl'"
|
||||
autocomplete="avatarUrl"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<!-- Password field -->
|
||||
<!-- Password field -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Password</mat-label>
|
||||
<input id="password" matInput type="password" [formControlName]="'password'" autocomplete="password" #passwordField />
|
||||
<button mat-icon-button type="button" (click)="
|
||||
<input
|
||||
id="password"
|
||||
matInput
|
||||
type="password"
|
||||
[formControlName]="'password'"
|
||||
autocomplete="password"
|
||||
#passwordField
|
||||
/>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
(click)="
|
||||
passwordField.type === 'password'
|
||||
? (passwordField.type = 'text')
|
||||
: (passwordField.type = 'password')
|
||||
" matSuffix>
|
||||
<mat-icon *ngIf="passwordField.type === 'password'" class="icon-size-5" [svgIcon]="'heroicons_solid:eye'"></mat-icon>
|
||||
<mat-icon *ngIf="passwordField.type === 'text'" class="icon-size-5" [svgIcon]="'heroicons_solid:eye-slash'"></mat-icon>
|
||||
"
|
||||
matSuffix
|
||||
>
|
||||
<mat-icon
|
||||
*ngIf="passwordField.type === 'password'"
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:eye'"
|
||||
></mat-icon>
|
||||
<mat-icon
|
||||
*ngIf="passwordField.type === 'text'"
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:eye-slash'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="registerForm.get('password').hasError('required')"> Password is required </mat-error>
|
||||
<mat-error
|
||||
*ngIf="
|
||||
registerForm.get('password').hasError('required')
|
||||
"
|
||||
>
|
||||
Password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- ToS and PP -->
|
||||
<div class="mt-1.5 inline-flex w-full items-end">
|
||||
<mat-checkbox class="-ml-2" [color]="'primary'" [formControlName]="'agreements'">
|
||||
<mat-checkbox
|
||||
class="-ml-2"
|
||||
[color]="'primary'"
|
||||
[formControlName]="'agreements'"
|
||||
>
|
||||
<span>I agree with</span>
|
||||
<a class="ml-1 text-primary-500 hover:underline" [routerLink]="['./']">Terms</a>
|
||||
<a
|
||||
class="ml-1 text-primary-500 hover:underline"
|
||||
[routerLink]="['./']"
|
||||
>Terms</a
|
||||
>
|
||||
<span>and</span>
|
||||
<a class="ml-1 text-primary-500 hover:underline" [routerLink]="['./']">Privacy Policy</a>
|
||||
<a
|
||||
class="ml-1 text-primary-500 hover:underline"
|
||||
[routerLink]="['./']"
|
||||
>Privacy Policy</a
|
||||
>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- Submit button -->
|
||||
<button class="angor-mat-button-large mt-6 w-full" mat-flat-button [color]="'primary'"
|
||||
[disabled]="registerForm.invalid" (click)="register()">
|
||||
<button
|
||||
class="angor-mat-button-large mt-6 w-full"
|
||||
mat-flat-button
|
||||
[color]="'primary'"
|
||||
[disabled]="registerForm.invalid"
|
||||
(click)="register()"
|
||||
>
|
||||
<span>Create your account</span>
|
||||
<mat-progress-spinner *ngIf="registerForm.disabled" [diameter]="24" [mode]="'indeterminate'"></mat-progress-spinner>
|
||||
<mat-progress-spinner
|
||||
*ngIf="registerForm.disabled"
|
||||
[diameter]="24"
|
||||
[mode]="'indeterminate'"
|
||||
></mat-progress-spinner>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative hidden h-full w-1/2 flex-auto items-center justify-center overflow-hidden bg-gray-800 p-16 dark:border-l md:flex lg:px-28">
|
||||
|
||||
<svg class="absolute inset-0 pointer-events-none" viewBox="0 0 960 540" width="100%" height="100%"
|
||||
preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="text-gray-700 opacity-25" fill="none" stroke="currentColor" stroke-width="100">
|
||||
class="relative hidden h-full w-1/2 flex-auto items-center justify-center overflow-hidden bg-gray-800 p-16 dark:border-l md:flex lg:px-28"
|
||||
>
|
||||
<svg
|
||||
class="pointer-events-none absolute inset-0"
|
||||
viewBox="0 0 960 540"
|
||||
width="100%"
|
||||
height="100%"
|
||||
preserveAspectRatio="xMidYMax slice"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
class="text-gray-700 opacity-25"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="100"
|
||||
>
|
||||
<circle r="234" cx="196" cy="23"></circle>
|
||||
<circle r="234" cx="790" cy="491"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<svg class="absolute -top-16 -right-16 text-gray-700" viewBox="0 0 220 192" width="220" height="192"
|
||||
fill="none">
|
||||
<svg
|
||||
class="absolute -right-16 -top-16 text-gray-700"
|
||||
viewBox="0 0 220 192"
|
||||
width="220"
|
||||
height="192"
|
||||
fill="none"
|
||||
>
|
||||
<defs>
|
||||
<pattern id="837c3e70-6c3a-44e6-8854-cc48c737b659" x="0" y="0" width="20" height="20"
|
||||
patternUnits="userSpaceOnUse">
|
||||
<rect x="0" y="0" width="4" height="4" fill="currentColor"></rect>
|
||||
<pattern
|
||||
id="837c3e70-6c3a-44e6-8854-cc48c737b659"
|
||||
x="0"
|
||||
y="0"
|
||||
width="20"
|
||||
height="20"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="4"
|
||||
height="4"
|
||||
fill="currentColor"
|
||||
></rect>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="220" height="192" fill="url(#837c3e70-6c3a-44e6-8854-cc48c737b659)"></rect>
|
||||
<rect
|
||||
width="220"
|
||||
height="192"
|
||||
fill="url(#837c3e70-6c3a-44e6-8854-cc48c737b659)"
|
||||
></rect>
|
||||
</svg>
|
||||
<!-- Content -->
|
||||
<div class="relative z-10 w-full max-w-2xl">
|
||||
@@ -110,8 +234,8 @@
|
||||
<div>Angor Hub</div>
|
||||
</div>
|
||||
<div class="mt-6 text-lg leading-6 tracking-tight text-gray-400">
|
||||
Angor Hub is a Nostr client that is customized around the Angor protocol, a decentralized crowdfunding
|
||||
platform.
|
||||
Angor Hub is a Nostr client that is customized around the Angor
|
||||
protocol, a decentralized crowdfunding platform.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { AngorAlertComponent, AngorAlertType } from '@angor/components/alert';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import {
|
||||
FormsModule,
|
||||
@@ -14,11 +17,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { angorAnimations } from '@angor/animations';
|
||||
import { AngorAlertComponent, AngorAlertType } from '@angor/components/alert';
|
||||
import { SecurityService } from 'app/services/security.service';
|
||||
import { SignerService } from 'app/services/signer.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'auth-register',
|
||||
@@ -37,7 +36,7 @@ import { CommonModule } from '@angular/common';
|
||||
MatIconModule,
|
||||
MatCheckboxModule,
|
||||
MatProgressSpinnerModule,
|
||||
CommonModule
|
||||
CommonModule,
|
||||
],
|
||||
})
|
||||
export class RegisterComponent implements OnInit {
|
||||
@@ -89,7 +88,10 @@ export class RegisterComponent implements OnInit {
|
||||
if (!keys) {
|
||||
// If key generation failed, enable the form and show an error
|
||||
this.registerForm.enable();
|
||||
this.alert = { type: 'error', message: 'Error generating keys. Please try again.' };
|
||||
this.alert = {
|
||||
type: 'error',
|
||||
message: 'Error generating keys. Please try again.',
|
||||
};
|
||||
this.showAlert = true;
|
||||
return;
|
||||
}
|
||||
@@ -112,11 +114,13 @@ export class RegisterComponent implements OnInit {
|
||||
console.log('User Metadata:', userMetadata);
|
||||
|
||||
// Display success alert
|
||||
this.alert = { type: 'success', message: 'Account created successfully!' };
|
||||
this.alert = {
|
||||
type: 'success',
|
||||
message: 'Account created successfully!',
|
||||
};
|
||||
this.showAlert = true;
|
||||
|
||||
// Redirect to home
|
||||
this._router.navigateByUrl('/home');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ const conversationResolver = (
|
||||
const chatService = inject(ChatService);
|
||||
const router = inject(Router);
|
||||
|
||||
let chatId = route.paramMap.get('id') || localStorage.getItem('currentChatId');
|
||||
let chatId =
|
||||
route.paramMap.get('id') || localStorage.getItem('currentChatId');
|
||||
|
||||
if (!chatId) {
|
||||
const parentUrl = state.url.split('/').slice(0, -1).join('/');
|
||||
@@ -45,7 +46,6 @@ const conversationResolver = (
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '',
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, Subject, throwError, of, Subscriber, from } from 'rxjs';
|
||||
import { catchError, filter, map, switchMap, take, takeUntil, tap } from 'rxjs/operators';
|
||||
import { Chat, Contact, Profile } from 'app/components/chat/chat.types';
|
||||
import { IndexedDBService } from 'app/services/indexed-db.service';
|
||||
import { MetadataService } from 'app/services/metadata.service';
|
||||
import { SignerService } from 'app/services/signer.service';
|
||||
import { Filter, NostrEvent } from 'nostr-tools';
|
||||
import { RelayService } from 'app/services/relay.service';
|
||||
import { SignerService } from 'app/services/signer.service';
|
||||
import { Filter, getEventHash, NostrEvent } from 'nostr-tools';
|
||||
import { EncryptedDirectMessage } from 'nostr-tools/kinds';
|
||||
import { getEventHash } from 'nostr-tools';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
from,
|
||||
Observable,
|
||||
of,
|
||||
Subject,
|
||||
throwError,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
filter,
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ChatService implements OnDestroy {
|
||||
@@ -18,22 +32,26 @@ export class ChatService implements OnDestroy {
|
||||
private isDecrypting = false;
|
||||
private recipientPublicKey: string;
|
||||
private message: string;
|
||||
private decryptedPrivateKey: string = "";
|
||||
private decryptedPrivateKey: string = '';
|
||||
private _chat: BehaviorSubject<Chat | null> = new BehaviorSubject(null);
|
||||
private _chats: BehaviorSubject<Chat[] | null> = new BehaviorSubject(null);
|
||||
private _contact: BehaviorSubject<Contact | null> = new BehaviorSubject(null);
|
||||
private _contacts: BehaviorSubject<Contact[] | null> = new BehaviorSubject(null);
|
||||
private _profile: BehaviorSubject<Profile | null> = new BehaviorSubject(null);
|
||||
private _contact: BehaviorSubject<Contact | null> = new BehaviorSubject(
|
||||
null
|
||||
);
|
||||
private _contacts: BehaviorSubject<Contact[] | null> = new BehaviorSubject(
|
||||
null
|
||||
);
|
||||
private _profile: BehaviorSubject<Profile | null> = new BehaviorSubject(
|
||||
null
|
||||
);
|
||||
private _unsubscribeAll: Subject<void> = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private _metadataService: MetadataService,
|
||||
private _signerService: SignerService,
|
||||
private _indexedDBService: IndexedDBService,
|
||||
private _relayService: RelayService,
|
||||
|
||||
|
||||
) { }
|
||||
private _relayService: RelayService
|
||||
) {}
|
||||
get profile$(): Observable<Profile | null> {
|
||||
return this._profile.asObservable();
|
||||
}
|
||||
@@ -54,46 +72,50 @@ export class ChatService implements OnDestroy {
|
||||
return this._contacts.asObservable();
|
||||
}
|
||||
|
||||
|
||||
checkCurrentChatOnPageRefresh(chatIdFromURL: string): void {
|
||||
if (chatIdFromURL) {
|
||||
const currentChat = this._chat.value;
|
||||
this.getChatById(chatIdFromURL).subscribe(chat => {
|
||||
this.getChatById(chatIdFromURL).subscribe((chat) => {
|
||||
if (chat) {
|
||||
this._chat.next(chat);
|
||||
this.loadChatHistory(chatIdFromURL);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getContact(pubkey: string): Promise<void> {
|
||||
try {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = await this._metadataService.fetchMetadataWithCache(pubkey);
|
||||
const metadata =
|
||||
await this._metadataService.fetchMetadataWithCache(pubkey);
|
||||
if (metadata) {
|
||||
const contact: Contact = {
|
||||
pubKey: pubkey,
|
||||
displayName: metadata.name ? metadata.name : 'Unknown',
|
||||
picture: metadata.picture,
|
||||
about: metadata.about
|
||||
about: metadata.about,
|
||||
};
|
||||
this._contact.next(contact);
|
||||
|
||||
this._indexedDBService.getMetadataStream()
|
||||
this._indexedDBService
|
||||
.getMetadataStream()
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((updatedMetadata) => {
|
||||
if (updatedMetadata && updatedMetadata.pubkey === pubkey) {
|
||||
if (
|
||||
updatedMetadata &&
|
||||
updatedMetadata.pubkey === pubkey
|
||||
) {
|
||||
const updatedContact: Contact = {
|
||||
pubKey: pubkey,
|
||||
displayName: updatedMetadata.metadata.name ? updatedMetadata.metadata.name : 'Unknown',
|
||||
displayName: updatedMetadata.metadata.name
|
||||
? updatedMetadata.metadata.name
|
||||
: 'Unknown',
|
||||
picture: updatedMetadata.metadata.picture,
|
||||
about: updatedMetadata.metadata.about
|
||||
about: updatedMetadata.metadata.about,
|
||||
};
|
||||
this._contact.next(updatedContact);
|
||||
}
|
||||
@@ -104,18 +126,23 @@ export class ChatService implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getContacts(): Observable<Contact[]> {
|
||||
return new Observable<Contact[]>((observer) => {
|
||||
this._indexedDBService.getAllUsers()
|
||||
this._indexedDBService
|
||||
.getAllUsers()
|
||||
.then((cachedContacts: Contact[]) => {
|
||||
if (cachedContacts && cachedContacts.length > 0) {
|
||||
const validatedContacts = cachedContacts.map(contact => {
|
||||
if (!contact.pubKey) {
|
||||
console.error('Contact is missing pubKey:', contact);
|
||||
const validatedContacts = cachedContacts.map(
|
||||
(contact) => {
|
||||
if (!contact.pubKey) {
|
||||
console.error(
|
||||
'Contact is missing pubKey:',
|
||||
contact
|
||||
);
|
||||
}
|
||||
return contact;
|
||||
}
|
||||
return contact;
|
||||
});
|
||||
);
|
||||
|
||||
this._contacts.next(validatedContacts);
|
||||
observer.next(validatedContacts);
|
||||
@@ -125,33 +152,49 @@ export class ChatService implements OnDestroy {
|
||||
observer.complete();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error loading cached contacts from IndexedDB:', error);
|
||||
console.error(
|
||||
'Error loading cached contacts from IndexedDB:',
|
||||
error
|
||||
);
|
||||
observer.next([]);
|
||||
observer.complete();
|
||||
});
|
||||
|
||||
return { unsubscribe() { } };
|
||||
return { unsubscribe() {} };
|
||||
});
|
||||
}
|
||||
|
||||
async updateChatListMetadata(): Promise<void> {
|
||||
const pubKeys = this.chatList.map(chat => chat.contact?.pubKey).filter(pubKey => pubKey);
|
||||
const pubKeys = this.chatList
|
||||
.map((chat) => chat.contact?.pubKey)
|
||||
.filter((pubKey) => pubKey);
|
||||
if (pubKeys.length > 0) {
|
||||
const metadataList = await this._metadataService.fetchMetadataForMultipleKeys(pubKeys);
|
||||
const metadataList =
|
||||
await this._metadataService.fetchMetadataForMultipleKeys(
|
||||
pubKeys
|
||||
);
|
||||
|
||||
metadataList.forEach(metadata => {
|
||||
const contact = this._contacts.value?.find(contact => contact.pubKey === metadata.pubkey);
|
||||
metadataList.forEach((metadata) => {
|
||||
const contact = this._contacts.value?.find(
|
||||
(contact) => contact.pubKey === metadata.pubkey
|
||||
);
|
||||
if (contact) {
|
||||
contact.displayName = metadata.metadata.name || 'Unknown';
|
||||
contact.picture = metadata.metadata.picture || contact.picture;
|
||||
contact.picture =
|
||||
metadata.metadata.picture || contact.picture;
|
||||
contact.about = metadata.metadata.about || contact.about;
|
||||
}
|
||||
|
||||
const chat = this.chatList.find(chat => chat.contact?.pubKey === metadata.pubkey);
|
||||
const chat = this.chatList.find(
|
||||
(chat) => chat.contact?.pubKey === metadata.pubkey
|
||||
);
|
||||
if (chat) {
|
||||
chat.contact.displayName = metadata.metadata.name || 'Unknown';
|
||||
chat.contact.picture = metadata.metadata.picture || chat.contact.picture;
|
||||
chat.contact.about = metadata.metadata.about || chat.contact.about;
|
||||
chat.contact.displayName =
|
||||
metadata.metadata.name || 'Unknown';
|
||||
chat.contact.picture =
|
||||
metadata.metadata.picture || chat.contact.picture;
|
||||
chat.contact.about =
|
||||
metadata.metadata.about || chat.contact.about;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -161,40 +204,60 @@ export class ChatService implements OnDestroy {
|
||||
}
|
||||
|
||||
private subscribeToRealTimeMetadataUpdates(pubKey: string): void {
|
||||
this._metadataService.getMetadataStream()
|
||||
.pipe(filter(updatedMetadata => updatedMetadata && updatedMetadata.pubkey === pubKey))
|
||||
.subscribe(updatedMetadata => {
|
||||
const chat = this.chatList.find(chat => chat.contact?.pubKey === pubKey);
|
||||
this._metadataService
|
||||
.getMetadataStream()
|
||||
.pipe(
|
||||
filter(
|
||||
(updatedMetadata) =>
|
||||
updatedMetadata && updatedMetadata.pubkey === pubKey
|
||||
)
|
||||
)
|
||||
.subscribe((updatedMetadata) => {
|
||||
const chat = this.chatList.find(
|
||||
(chat) => chat.contact?.pubKey === pubKey
|
||||
);
|
||||
if (chat) {
|
||||
chat.contact.displayName = updatedMetadata.metadata.name || 'Unknown';
|
||||
chat.contact.picture = updatedMetadata.metadata.picture || chat.contact.picture;
|
||||
chat.contact.about = updatedMetadata.metadata.about || chat.contact.about;
|
||||
chat.contact.displayName =
|
||||
updatedMetadata.metadata.name || 'Unknown';
|
||||
chat.contact.picture =
|
||||
updatedMetadata.metadata.picture ||
|
||||
chat.contact.picture;
|
||||
chat.contact.about =
|
||||
updatedMetadata.metadata.about || chat.contact.about;
|
||||
this._chats.next(this.chatList);
|
||||
}
|
||||
|
||||
const contact = this._contacts.value?.find(contact => contact.pubKey === pubKey);
|
||||
const contact = this._contacts.value?.find(
|
||||
(contact) => contact.pubKey === pubKey
|
||||
);
|
||||
if (contact) {
|
||||
contact.displayName = updatedMetadata.metadata.name || 'Unknown';
|
||||
contact.picture = updatedMetadata.metadata.picture || contact.picture;
|
||||
contact.about = updatedMetadata.metadata.about || contact.about;
|
||||
contact.displayName =
|
||||
updatedMetadata.metadata.name || 'Unknown';
|
||||
contact.picture =
|
||||
updatedMetadata.metadata.picture || contact.picture;
|
||||
contact.about =
|
||||
updatedMetadata.metadata.about || contact.about;
|
||||
this._contacts.next(this._contacts.value || []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async getProfile(): Promise<void> {
|
||||
try {
|
||||
const publicKey = this._signerService.getPublicKey();
|
||||
const metadata = await this._metadataService.fetchMetadataWithCache(publicKey);
|
||||
const metadata =
|
||||
await this._metadataService.fetchMetadataWithCache(publicKey);
|
||||
if (metadata) {
|
||||
this._profile.next(metadata);
|
||||
|
||||
|
||||
this._indexedDBService.getMetadataStream()
|
||||
this._indexedDBService
|
||||
.getMetadataStream()
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((updatedMetadata) => {
|
||||
if (updatedMetadata && updatedMetadata.pubkey === publicKey) {
|
||||
if (
|
||||
updatedMetadata &&
|
||||
updatedMetadata.pubkey === publicKey
|
||||
) {
|
||||
this._profile.next(updatedMetadata.metadata);
|
||||
}
|
||||
});
|
||||
@@ -206,14 +269,14 @@ export class ChatService implements OnDestroy {
|
||||
|
||||
async getChats(): Promise<Observable<Chat[]>> {
|
||||
return this.getChatListStream().pipe(
|
||||
tap(chats => {
|
||||
tap((chats) => {
|
||||
if (chats && chats.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pubKeys = chats
|
||||
.filter(chat => chat.contact?.pubKey)
|
||||
.map(chat => chat.contact!.pubKey);
|
||||
.filter((chat) => chat.contact?.pubKey)
|
||||
.map((chat) => chat.contact!.pubKey);
|
||||
|
||||
// Subscribe to all metadata updates in parallel
|
||||
this.subscribeToRealTimeMetadataUpdatesBatch(pubKeys);
|
||||
@@ -226,12 +289,19 @@ export class ChatService implements OnDestroy {
|
||||
const useExtension = await this._signerService.isUsingExtension();
|
||||
const useSecretKey = await this._signerService.isUsingSecretKey();
|
||||
|
||||
this.decryptedPrivateKey = useSecretKey ? await this._signerService.getDecryptedSecretKey() : '';
|
||||
this.decryptedPrivateKey = useSecretKey
|
||||
? await this._signerService.getDecryptedSecretKey()
|
||||
: '';
|
||||
|
||||
// Perform metadata and chat loading in parallel for speed
|
||||
await Promise.all([
|
||||
this.updateChatListMetadata(),
|
||||
this.subscribeToChatList(pubkey, useExtension, useSecretKey, this.decryptedPrivateKey)
|
||||
this.subscribeToChatList(
|
||||
pubkey,
|
||||
useExtension,
|
||||
useSecretKey,
|
||||
this.decryptedPrivateKey
|
||||
),
|
||||
]);
|
||||
|
||||
return this.getChatListStream();
|
||||
@@ -239,46 +309,80 @@ export class ChatService implements OnDestroy {
|
||||
|
||||
private subscribeToRealTimeMetadataUpdatesBatch(pubKeys: string[]): void {
|
||||
// Batch subscribe to all pubKeys metadata updates for efficiency
|
||||
pubKeys.forEach(pubKey => {
|
||||
pubKeys.forEach((pubKey) => {
|
||||
this.subscribeToRealTimeMetadataUpdates(pubKey);
|
||||
});
|
||||
}
|
||||
|
||||
subscribeToChatList(pubkey: string, useExtension: boolean, useSecretKey: boolean, decryptedSenderPrivateKey: string): Observable<Chat[]> {
|
||||
subscribeToChatList(
|
||||
pubkey: string,
|
||||
useExtension: boolean,
|
||||
useSecretKey: boolean,
|
||||
decryptedSenderPrivateKey: string
|
||||
): Observable<Chat[]> {
|
||||
this._relayService.ensureConnectedRelays().then(async () => {
|
||||
const filters: Filter[] = [
|
||||
{ kinds: [EncryptedDirectMessage], authors: [pubkey] , limit:1500},
|
||||
{ kinds: [EncryptedDirectMessage], '#p': [pubkey] , limit:1500}
|
||||
{
|
||||
kinds: [EncryptedDirectMessage],
|
||||
authors: [pubkey],
|
||||
limit: 1500,
|
||||
},
|
||||
{
|
||||
kinds: [EncryptedDirectMessage],
|
||||
'#p': [pubkey],
|
||||
limit: 1500,
|
||||
},
|
||||
];
|
||||
|
||||
this._relayService.getPool().subscribeMany(this._relayService.getConnectedRelays(), filters, {
|
||||
onevent: async (event: NostrEvent) => {
|
||||
const otherPartyPubKey = event.pubkey === pubkey
|
||||
? event.tags.find(tag => tag[0] === 'p')?.[1] || ''
|
||||
: event.pubkey;
|
||||
this._relayService
|
||||
.getPool()
|
||||
.subscribeMany(
|
||||
this._relayService.getConnectedRelays(),
|
||||
filters,
|
||||
{
|
||||
onevent: async (event: NostrEvent) => {
|
||||
const otherPartyPubKey =
|
||||
event.pubkey === pubkey
|
||||
? event.tags.find(
|
||||
(tag) => tag[0] === 'p'
|
||||
)?.[1] || ''
|
||||
: event.pubkey;
|
||||
|
||||
if (!otherPartyPubKey) return;
|
||||
if (!otherPartyPubKey) return;
|
||||
|
||||
const lastTimestamp = this.latestMessageTimestamps[otherPartyPubKey] || 0;
|
||||
if (event.created_at > lastTimestamp) {
|
||||
this.messageQueue.push(event);
|
||||
this.processNextMessage(pubkey, useExtension, useSecretKey, decryptedSenderPrivateKey);
|
||||
const lastTimestamp =
|
||||
this.latestMessageTimestamps[
|
||||
otherPartyPubKey
|
||||
] || 0;
|
||||
if (event.created_at > lastTimestamp) {
|
||||
this.messageQueue.push(event);
|
||||
this.processNextMessage(
|
||||
pubkey,
|
||||
useExtension,
|
||||
useSecretKey,
|
||||
decryptedSenderPrivateKey
|
||||
);
|
||||
}
|
||||
},
|
||||
oneose: () => {
|
||||
const currentChats = this.chatList || [];
|
||||
if (currentChats.length > 0) {
|
||||
this._chats.next(this.chatList);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
oneose: () => {
|
||||
|
||||
const currentChats = this.chatList || [];
|
||||
if (currentChats.length > 0) {
|
||||
this._chats.next(this.chatList);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
return this.getChatListStream();
|
||||
}
|
||||
|
||||
private async processNextMessage(pubkey: string, useExtension: boolean, useSecretKey: boolean, decryptedSenderPrivateKey: string): Promise<void> {
|
||||
private async processNextMessage(
|
||||
pubkey: string,
|
||||
useExtension: boolean,
|
||||
useSecretKey: boolean,
|
||||
decryptedSenderPrivateKey: string
|
||||
): Promise<void> {
|
||||
if (this.isDecrypting || this.messageQueue.length === 0) return;
|
||||
|
||||
this.isDecrypting = true;
|
||||
@@ -290,7 +394,7 @@ export class ChatService implements OnDestroy {
|
||||
|
||||
const isSentByUser = event.pubkey === pubkey;
|
||||
const otherPartyPubKey = isSentByUser
|
||||
? event.tags.find(tag => tag[0] === 'p')?.[1] || ''
|
||||
? event.tags.find((tag) => tag[0] === 'p')?.[1] || ''
|
||||
: event.pubkey;
|
||||
|
||||
if (!otherPartyPubKey) continue;
|
||||
@@ -304,7 +408,12 @@ export class ChatService implements OnDestroy {
|
||||
);
|
||||
|
||||
if (decryptedMessage) {
|
||||
this.addOrUpdateChatList(otherPartyPubKey, decryptedMessage, event.created_at, isSentByUser);
|
||||
this.addOrUpdateChatList(
|
||||
otherPartyPubKey,
|
||||
decryptedMessage,
|
||||
event.created_at,
|
||||
isSentByUser
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -314,8 +423,15 @@ export class ChatService implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private addOrUpdateChatList(pubKey: string, message: string, createdAt: number, isMine: boolean): void {
|
||||
const existingChat = this.chatList.find(chat => chat.contact?.pubKey === pubKey);
|
||||
private addOrUpdateChatList(
|
||||
pubKey: string,
|
||||
message: string,
|
||||
createdAt: number,
|
||||
isMine: boolean
|
||||
): void {
|
||||
const existingChat = this.chatList.find(
|
||||
(chat) => chat.contact?.pubKey === pubKey
|
||||
);
|
||||
|
||||
const newMessage = {
|
||||
id: `${pubKey}-${createdAt}`,
|
||||
@@ -327,11 +443,19 @@ export class ChatService implements OnDestroy {
|
||||
};
|
||||
|
||||
if (existingChat) {
|
||||
const messageExists = existingChat.messages?.some(m => m.id === newMessage.id);
|
||||
const messageExists = existingChat.messages?.some(
|
||||
(m) => m.id === newMessage.id
|
||||
);
|
||||
|
||||
if (!messageExists) {
|
||||
existingChat.messages = [...(existingChat.messages || []), newMessage]
|
||||
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
||||
existingChat.messages = [
|
||||
...(existingChat.messages || []),
|
||||
newMessage,
|
||||
].sort(
|
||||
(a, b) =>
|
||||
new Date(a.createdAt).getTime() -
|
||||
new Date(b.createdAt).getTime()
|
||||
);
|
||||
|
||||
if (Number(existingChat.lastMessageAt) < createdAt) {
|
||||
existingChat.lastMessage = message;
|
||||
@@ -339,25 +463,34 @@ export class ChatService implements OnDestroy {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const contactInfo = this._contacts.value?.find(contact => contact.pubKey === pubKey) || { pubKey };
|
||||
const contactInfo = this._contacts.value?.find(
|
||||
(contact) => contact.pubKey === pubKey
|
||||
) || { pubKey };
|
||||
|
||||
const newChat: Chat = {
|
||||
id: pubKey,
|
||||
contact: {
|
||||
pubKey: contactInfo.pubKey,
|
||||
name: contactInfo.name || "Unknown",
|
||||
picture: contactInfo.picture || "/images/avatars/avatar-placeholder.png",
|
||||
about: contactInfo.about || "",
|
||||
displayName: contactInfo.displayName || contactInfo.name || "Unknown"
|
||||
name: contactInfo.name || 'Unknown',
|
||||
picture:
|
||||
contactInfo.picture ||
|
||||
'/images/avatars/avatar-placeholder.png',
|
||||
about: contactInfo.about || '',
|
||||
displayName:
|
||||
contactInfo.displayName ||
|
||||
contactInfo.name ||
|
||||
'Unknown',
|
||||
},
|
||||
lastMessage: message,
|
||||
lastMessageAt: createdAt.toString(),
|
||||
messages: [newMessage]
|
||||
messages: [newMessage],
|
||||
};
|
||||
this.chatList.push(newChat);
|
||||
}
|
||||
|
||||
this.chatList.sort((a, b) => Number(b.lastMessageAt!) - Number(a.lastMessageAt!));
|
||||
this.chatList.sort(
|
||||
(a, b) => Number(b.lastMessageAt!) - Number(a.lastMessageAt!)
|
||||
);
|
||||
this._chats.next(this.chatList);
|
||||
}
|
||||
|
||||
@@ -373,9 +506,16 @@ export class ChatService implements OnDestroy {
|
||||
recipientPublicKey: string
|
||||
): Promise<string> {
|
||||
if (useExtension && !useSecretKey) {
|
||||
return await this._signerService.decryptMessageWithExtension(recipientPublicKey, event.content);
|
||||
return await this._signerService.decryptMessageWithExtension(
|
||||
recipientPublicKey,
|
||||
event.content
|
||||
);
|
||||
} else if (useSecretKey && !useExtension) {
|
||||
return await this._signerService.decryptMessage(decryptedSenderPrivateKey, recipientPublicKey, event.content);
|
||||
return await this._signerService.decryptMessage(
|
||||
decryptedSenderPrivateKey,
|
||||
recipientPublicKey,
|
||||
event.content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,83 +523,143 @@ export class ChatService implements OnDestroy {
|
||||
const myPubKey = this._signerService.getPublicKey();
|
||||
|
||||
const historyFilter: Filter[] = [
|
||||
{ kinds: [EncryptedDirectMessage], authors: [myPubKey], '#p': [pubKey], limit: 10 },
|
||||
{ kinds: [EncryptedDirectMessage], authors: [pubKey], '#p': [myPubKey], limit: 10 }
|
||||
{
|
||||
kinds: [EncryptedDirectMessage],
|
||||
authors: [myPubKey],
|
||||
'#p': [pubKey],
|
||||
limit: 10,
|
||||
},
|
||||
{
|
||||
kinds: [EncryptedDirectMessage],
|
||||
authors: [pubKey],
|
||||
'#p': [myPubKey],
|
||||
limit: 10,
|
||||
},
|
||||
];
|
||||
|
||||
this._relayService.getPool().subscribeMany(this._relayService.getConnectedRelays(), historyFilter, {
|
||||
onevent: async (event: NostrEvent) => {
|
||||
const isSentByMe = event.pubkey === myPubKey;
|
||||
const senderOrRecipientPubKey = isSentByMe ? pubKey : event.pubkey;
|
||||
const useExtension = await this._signerService.isUsingExtension();
|
||||
const useSecretKey = await this._signerService.isUsingSecretKey();
|
||||
const decryptedMessage = await this.decryptReceivedMessage(
|
||||
event,
|
||||
useExtension,
|
||||
useSecretKey,
|
||||
this.decryptedPrivateKey,
|
||||
senderOrRecipientPubKey
|
||||
);
|
||||
this._relayService
|
||||
.getPool()
|
||||
.subscribeMany(
|
||||
this._relayService.getConnectedRelays(),
|
||||
historyFilter,
|
||||
{
|
||||
onevent: async (event: NostrEvent) => {
|
||||
const isSentByMe = event.pubkey === myPubKey;
|
||||
const senderOrRecipientPubKey = isSentByMe
|
||||
? pubKey
|
||||
: event.pubkey;
|
||||
const useExtension =
|
||||
await this._signerService.isUsingExtension();
|
||||
const useSecretKey =
|
||||
await this._signerService.isUsingSecretKey();
|
||||
const decryptedMessage =
|
||||
await this.decryptReceivedMessage(
|
||||
event,
|
||||
useExtension,
|
||||
useSecretKey,
|
||||
this.decryptedPrivateKey,
|
||||
senderOrRecipientPubKey
|
||||
);
|
||||
|
||||
if (decryptedMessage) {
|
||||
const messageTimestamp = Math.floor(event.created_at);
|
||||
if (decryptedMessage) {
|
||||
const messageTimestamp = Math.floor(
|
||||
event.created_at
|
||||
);
|
||||
|
||||
this.addOrUpdateChatList(pubKey, decryptedMessage, messageTimestamp, isSentByMe);
|
||||
this._chat.next(this.chatList.find(chat => chat.id === pubKey));
|
||||
this.addOrUpdateChatList(
|
||||
pubKey,
|
||||
decryptedMessage,
|
||||
messageTimestamp,
|
||||
isSentByMe
|
||||
);
|
||||
this._chat.next(
|
||||
this.chatList.find((chat) => chat.id === pubKey)
|
||||
);
|
||||
}
|
||||
},
|
||||
oneose: () => {},
|
||||
}
|
||||
},
|
||||
oneose: () => {
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
private async fetchChatHistory(pubKey: string): Promise<any[]> {
|
||||
const myPubKey = this._signerService.getPublicKey();
|
||||
|
||||
const historyFilter: Filter[] = [
|
||||
{ kinds: [EncryptedDirectMessage], authors: [myPubKey], '#p': [pubKey], limit: 10 },
|
||||
{ kinds: [EncryptedDirectMessage], authors: [pubKey], '#p': [myPubKey], limit: 10 }
|
||||
{
|
||||
kinds: [EncryptedDirectMessage],
|
||||
authors: [myPubKey],
|
||||
'#p': [pubKey],
|
||||
limit: 10,
|
||||
},
|
||||
{
|
||||
kinds: [EncryptedDirectMessage],
|
||||
authors: [pubKey],
|
||||
'#p': [myPubKey],
|
||||
limit: 10,
|
||||
},
|
||||
];
|
||||
|
||||
const messages: any[] = [];
|
||||
|
||||
this._relayService.getPool().subscribeMany(this._relayService.getConnectedRelays(), historyFilter, {
|
||||
onevent: async (event: NostrEvent) => {
|
||||
const isSentByMe = event.pubkey === myPubKey;
|
||||
const senderOrRecipientPubKey = isSentByMe ? pubKey : event.pubkey;
|
||||
const useExtension = await this._signerService.isUsingExtension();
|
||||
const useSecretKey = await this._signerService.isUsingSecretKey();
|
||||
const decryptedMessage = await this.decryptReceivedMessage(
|
||||
event,
|
||||
useExtension,
|
||||
useSecretKey,
|
||||
this.decryptedPrivateKey,
|
||||
senderOrRecipientPubKey
|
||||
);
|
||||
this._relayService
|
||||
.getPool()
|
||||
.subscribeMany(
|
||||
this._relayService.getConnectedRelays(),
|
||||
historyFilter,
|
||||
{
|
||||
onevent: async (event: NostrEvent) => {
|
||||
const isSentByMe = event.pubkey === myPubKey;
|
||||
const senderOrRecipientPubKey = isSentByMe
|
||||
? pubKey
|
||||
: event.pubkey;
|
||||
const useExtension =
|
||||
await this._signerService.isUsingExtension();
|
||||
const useSecretKey =
|
||||
await this._signerService.isUsingSecretKey();
|
||||
const decryptedMessage =
|
||||
await this.decryptReceivedMessage(
|
||||
event,
|
||||
useExtension,
|
||||
useSecretKey,
|
||||
this.decryptedPrivateKey,
|
||||
senderOrRecipientPubKey
|
||||
);
|
||||
|
||||
if (decryptedMessage) {
|
||||
const messageTimestamp = Math.floor(event.created_at);
|
||||
if (decryptedMessage) {
|
||||
const messageTimestamp = Math.floor(
|
||||
event.created_at
|
||||
);
|
||||
|
||||
const message = {
|
||||
id: event.id,
|
||||
chatId: pubKey,
|
||||
contactId: senderOrRecipientPubKey,
|
||||
isMine: isSentByMe,
|
||||
value: decryptedMessage,
|
||||
createdAt: new Date(messageTimestamp * 1000).toISOString(),
|
||||
};
|
||||
const message = {
|
||||
id: event.id,
|
||||
chatId: pubKey,
|
||||
contactId: senderOrRecipientPubKey,
|
||||
isMine: isSentByMe,
|
||||
value: decryptedMessage,
|
||||
createdAt: new Date(
|
||||
messageTimestamp * 1000
|
||||
).toISOString(),
|
||||
};
|
||||
|
||||
messages.push(message);
|
||||
messages.push(message);
|
||||
|
||||
this.addOrUpdateChatList(pubKey, decryptedMessage, messageTimestamp, isSentByMe);
|
||||
this._chat.next(this.chatList.find(chat => chat.id === pubKey));
|
||||
this.addOrUpdateChatList(
|
||||
pubKey,
|
||||
decryptedMessage,
|
||||
messageTimestamp,
|
||||
isSentByMe
|
||||
);
|
||||
this._chat.next(
|
||||
this.chatList.find((chat) => chat.id === pubKey)
|
||||
);
|
||||
}
|
||||
},
|
||||
oneose: () => {},
|
||||
}
|
||||
},
|
||||
oneose: () => {
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
return messages;
|
||||
}
|
||||
@@ -484,10 +684,14 @@ export class ChatService implements OnDestroy {
|
||||
|
||||
event.id = getEventHash(event);
|
||||
|
||||
return from(this._relayService.publishEventToRelays(event)).pipe(
|
||||
return from(
|
||||
this._relayService.publishEventToRelays(event)
|
||||
).pipe(
|
||||
map(() => {
|
||||
if (chats) {
|
||||
const index = chats.findIndex((item) => item.id === id);
|
||||
const index = chats.findIndex(
|
||||
(item) => item.id === id
|
||||
);
|
||||
if (index !== -1) {
|
||||
chats[index] = chat;
|
||||
this._chats.next(chats);
|
||||
@@ -496,7 +700,10 @@ export class ChatService implements OnDestroy {
|
||||
return chat;
|
||||
}),
|
||||
catchError((error) => {
|
||||
console.error('Failed to update chat via Nostr:', error);
|
||||
console.error(
|
||||
'Failed to update chat via Nostr:',
|
||||
error
|
||||
);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
@@ -516,7 +723,7 @@ export class ChatService implements OnDestroy {
|
||||
return this.createNewChat(id, contact);
|
||||
}
|
||||
|
||||
const cachedChat = chats.find(chat => chat.id === id);
|
||||
const cachedChat = chats.find((chat) => chat.id === id);
|
||||
if (cachedChat) {
|
||||
this._chat.next(cachedChat);
|
||||
this.loadChatHistory(this.recipientPublicKey);
|
||||
@@ -534,8 +741,6 @@ export class ChatService implements OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
createNewChat(id: string, contact: Contact = null): Observable<Chat> {
|
||||
// const existingChat = this._chats.value?.find(chat => chat.id === id);
|
||||
|
||||
@@ -545,11 +750,21 @@ export class ChatService implements OnDestroy {
|
||||
const newChat: Chat = {
|
||||
id: id || '',
|
||||
contact: contact
|
||||
? { pubKey: contact.pubKey || id, name: contact.name || 'Unknown', picture: contact.picture || '/images/avatars/avatar-placeholder.png' }
|
||||
: { pubKey: id, name: 'Unknown', picture: '/images/avatars/avatar-placeholder.png' },
|
||||
? {
|
||||
pubKey: contact.pubKey || id,
|
||||
name: contact.name || 'Unknown',
|
||||
picture:
|
||||
contact.picture ||
|
||||
'/images/avatars/avatar-placeholder.png',
|
||||
}
|
||||
: {
|
||||
pubKey: id,
|
||||
name: 'Unknown',
|
||||
picture: '/images/avatars/avatar-placeholder.png',
|
||||
},
|
||||
lastMessage: 'new chat...',
|
||||
lastMessageAt: Math.floor(Date.now() / 1000).toString() || '0',
|
||||
messages: []
|
||||
messages: [],
|
||||
};
|
||||
|
||||
// const updatedChats = this._chats.value ? [...this._chats.value, newChat] : [newChat];
|
||||
@@ -559,10 +774,13 @@ export class ChatService implements OnDestroy {
|
||||
map((metadata: any) => {
|
||||
newChat.contact = {
|
||||
pubKey: id,
|
||||
name: metadata?.name || "Unknown",
|
||||
picture: metadata?.picture || "/images/avatars/avatar-placeholder.png",
|
||||
about: metadata?.about || "",
|
||||
displayName: metadata?.displayName || metadata?.name || "Unknown"
|
||||
name: metadata?.name || 'Unknown',
|
||||
picture:
|
||||
metadata?.picture ||
|
||||
'/images/avatars/avatar-placeholder.png',
|
||||
about: metadata?.about || '',
|
||||
displayName:
|
||||
metadata?.displayName || metadata?.name || 'Unknown',
|
||||
};
|
||||
|
||||
return newChat;
|
||||
@@ -576,8 +794,10 @@ export class ChatService implements OnDestroy {
|
||||
chatId: id,
|
||||
contactId: id,
|
||||
isMine: true,
|
||||
value: "new chat...",
|
||||
createdAt: Math.floor(Date.now() / 1000).toString(),
|
||||
value: 'new chat...',
|
||||
createdAt: Math.floor(
|
||||
Date.now() / 1000
|
||||
).toString(),
|
||||
};
|
||||
|
||||
messages.push(testMessage);
|
||||
@@ -591,7 +811,9 @@ export class ChatService implements OnDestroy {
|
||||
newChat.lastMessageAt = lastMessage.createdAt;
|
||||
}
|
||||
|
||||
const updatedChatsWithMessages = this._chats.value ? [...this._chats.value, newChat] : [newChat];
|
||||
const updatedChatsWithMessages = this._chats.value
|
||||
? [...this._chats.value, newChat]
|
||||
: [newChat];
|
||||
this._chats.next(updatedChatsWithMessages);
|
||||
this._chat.next(newChat);
|
||||
|
||||
@@ -602,14 +824,10 @@ export class ChatService implements OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
resetChat(): void {
|
||||
this._chat.next(null);
|
||||
}
|
||||
|
||||
|
||||
public async sendPrivateMessage(message: string): Promise<void> {
|
||||
try {
|
||||
this.message = message;
|
||||
@@ -620,20 +838,31 @@ export class ChatService implements OnDestroy {
|
||||
await this.handleMessageSendingWithExtension();
|
||||
} else if (!useExtension && useSecretKey) {
|
||||
if (!this.isValidMessageSetup()) {
|
||||
console.error('Message, sender private key, or recipient public key is not properly set.');
|
||||
console.error(
|
||||
'Message, sender private key, or recipient public key is not properly set.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const encryptedMessage = await this._signerService.encryptMessage(
|
||||
this.decryptedPrivateKey,
|
||||
this.recipientPublicKey,
|
||||
this.message
|
||||
const encryptedMessage =
|
||||
await this._signerService.encryptMessage(
|
||||
this.decryptedPrivateKey,
|
||||
this.recipientPublicKey,
|
||||
this.message
|
||||
);
|
||||
|
||||
const messageEvent = this._signerService.getUnsignedEvent(
|
||||
4,
|
||||
[['p', this.recipientPublicKey]],
|
||||
encryptedMessage
|
||||
);
|
||||
|
||||
const messageEvent = this._signerService.getUnsignedEvent(4, [['p', this.recipientPublicKey]], encryptedMessage);
|
||||
const signedEvent = this._signerService.getSignedEvent(
|
||||
messageEvent,
|
||||
this.decryptedPrivateKey
|
||||
);
|
||||
|
||||
const signedEvent = this._signerService.getSignedEvent(messageEvent, this.decryptedPrivateKey);
|
||||
|
||||
const published = await this._relayService.publishEventToRelays(signedEvent);
|
||||
const published =
|
||||
await this._relayService.publishEventToRelays(signedEvent);
|
||||
|
||||
if (published) {
|
||||
this.message = '';
|
||||
@@ -641,7 +870,6 @@ export class ChatService implements OnDestroy {
|
||||
console.error('Failed to send the message.');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error sending private message:', error);
|
||||
}
|
||||
@@ -649,20 +877,23 @@ export class ChatService implements OnDestroy {
|
||||
|
||||
private async handleMessageSendingWithExtension(): Promise<void> {
|
||||
try {
|
||||
const encryptedMessage = await this._signerService.encryptMessageWithExtension(
|
||||
this.message,
|
||||
this.recipientPublicKey
|
||||
);
|
||||
const encryptedMessage =
|
||||
await this._signerService.encryptMessageWithExtension(
|
||||
this.message,
|
||||
this.recipientPublicKey
|
||||
);
|
||||
|
||||
const signedEvent = await this._signerService.signEventWithExtension({
|
||||
kind: 4,
|
||||
pubkey: this._signerService.getPublicKey(),
|
||||
tags: [['p', this.recipientPublicKey]],
|
||||
content: encryptedMessage,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
const signedEvent =
|
||||
await this._signerService.signEventWithExtension({
|
||||
kind: 4,
|
||||
pubkey: this._signerService.getPublicKey(),
|
||||
tags: [['p', this.recipientPublicKey]],
|
||||
content: encryptedMessage,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
|
||||
const published = await this._relayService.publishEventToRelays(signedEvent);
|
||||
const published =
|
||||
await this._relayService.publishEventToRelays(signedEvent);
|
||||
|
||||
if (published) {
|
||||
this.message = '';
|
||||
@@ -682,6 +913,4 @@ export class ChatService implements OnDestroy {
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<div class="bg-card relative flex w-full flex-auto dark:bg-transparent">
|
||||
<mat-drawer-container class="h-full flex-auto" [hasBackdrop]="false">
|
||||
<!-- Drawer -->
|
||||
<mat-drawer class="w-full dark:bg-gray-900 sm:w-100 lg:border-r lg:shadow-none" [autoFocus]="false"
|
||||
[(opened)]="drawerOpened" #drawer>
|
||||
<mat-drawer
|
||||
class="w-full dark:bg-gray-900 sm:w-100 lg:border-r lg:shadow-none"
|
||||
[autoFocus]="false"
|
||||
[(opened)]="drawerOpened"
|
||||
#drawer
|
||||
>
|
||||
<!-- New chat -->
|
||||
@if (drawerComponent === 'new-chat') {
|
||||
<chat-new-chat [drawer]="drawer"></chat-new-chat>
|
||||
<chat-new-chat [drawer]="drawer"></chat-new-chat>
|
||||
}
|
||||
|
||||
<!-- Profile -->
|
||||
@if (drawerComponent === 'profile') {
|
||||
<chat-profile [drawer]="drawer"></chat-profile>
|
||||
<chat-profile [drawer]="drawer"></chat-profile>
|
||||
}
|
||||
</mat-drawer>
|
||||
|
||||
@@ -18,180 +22,276 @@
|
||||
<mat-drawer-content class="flex overflow-hidden">
|
||||
<!-- Chats list -->
|
||||
@if (chats && chats.length > 0) {
|
||||
<div
|
||||
class="bg-card relative flex w-full min-w-0 flex-auto flex-col dark:bg-transparent lg:min-w-100 lg:max-w-100">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-0 flex-col border-b bg-gray-50 px-8 py-4 dark:bg-transparent">
|
||||
<div class="flex items-center">
|
||||
<div class="mr-1 flex cursor-pointer items-center" (click)="openProfile()">
|
||||
<div class="h-10 w-10">
|
||||
@if (profile?.picture) {
|
||||
<img class="h-full w-full rounded-full object-cover" [src]="profile?.picture"
|
||||
onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
|
||||
alt="Profile picture" />
|
||||
}
|
||||
@if (!profile?.picture) {
|
||||
<div
|
||||
class="flex h-full w-full items-center justify-center rounded-full bg-gray-200 text-lg uppercase text-gray-600 dark:bg-gray-700 dark:text-gray-200">
|
||||
{{ profile?.name?.charAt(0) }}
|
||||
<div
|
||||
class="bg-card relative flex w-full min-w-0 flex-auto flex-col dark:bg-transparent lg:min-w-100 lg:max-w-100"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="flex flex-0 flex-col border-b bg-gray-50 px-8 py-4 dark:bg-transparent"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="mr-1 flex cursor-pointer items-center"
|
||||
(click)="openProfile()"
|
||||
>
|
||||
<div class="h-10 w-10">
|
||||
@if (profile?.picture) {
|
||||
<img
|
||||
class="h-full w-full rounded-full object-cover"
|
||||
[src]="profile?.picture"
|
||||
onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
|
||||
alt="Profile picture"
|
||||
/>
|
||||
}
|
||||
@if (!profile?.picture) {
|
||||
<div
|
||||
class="flex h-full w-full items-center justify-center rounded-full bg-gray-200 text-lg uppercase text-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{{ profile?.name?.charAt(0) }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="ml-4 truncate font-medium">
|
||||
{{ profile?.name }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="ml-4 truncate font-medium">
|
||||
{{ profile?.name }}
|
||||
</div>
|
||||
</div>
|
||||
<button class="ml-auto" mat-icon-button (click)="openNewChat()">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:plus-circle'"></mat-icon>
|
||||
</button>
|
||||
<button class="-mr-4 ml-1" mat-icon-button [matMenuTriggerFor]="chatsHeaderMenu">
|
||||
<mat-icon [svgIcon]="
|
||||
<button
|
||||
class="ml-auto"
|
||||
mat-icon-button
|
||||
(click)="openNewChat()"
|
||||
>
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_outline:plus-circle'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
class="-mr-4 ml-1"
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="chatsHeaderMenu"
|
||||
>
|
||||
<mat-icon
|
||||
[svgIcon]="
|
||||
'heroicons_outline:ellipsis-vertical'
|
||||
"></mat-icon>
|
||||
<mat-menu #chatsHeaderMenu>
|
||||
<button mat-menu-item>
|
||||
<mat-icon [svgIcon]="
|
||||
"
|
||||
></mat-icon>
|
||||
<mat-menu #chatsHeaderMenu>
|
||||
<button mat-menu-item>
|
||||
<mat-icon
|
||||
[svgIcon]="
|
||||
'heroicons_outline:user-group'
|
||||
"></mat-icon>
|
||||
New group
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon [svgIcon]="
|
||||
"
|
||||
></mat-icon>
|
||||
New group
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon
|
||||
[svgIcon]="
|
||||
'heroicons_outline:chat-bubble-left-right'
|
||||
"></mat-icon>
|
||||
Create a room
|
||||
</button>
|
||||
<button mat-menu-item (click)="openProfile()">
|
||||
<mat-icon [svgIcon]="
|
||||
"
|
||||
></mat-icon>
|
||||
Create a room
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="openProfile()"
|
||||
>
|
||||
<mat-icon
|
||||
[svgIcon]="
|
||||
'heroicons_outline:user-circle'
|
||||
"></mat-icon>
|
||||
Profile
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon [svgIcon]="
|
||||
"
|
||||
></mat-icon>
|
||||
Profile
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon
|
||||
[svgIcon]="
|
||||
'heroicons_outline:archive-box'
|
||||
"></mat-icon>
|
||||
Archived
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon [svgIcon]="'heroicons_outline:star'"></mat-icon>
|
||||
Starred
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon [svgIcon]="
|
||||
"
|
||||
></mat-icon>
|
||||
Archived
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_outline:star'"
|
||||
></mat-icon>
|
||||
Starred
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon
|
||||
[svgIcon]="
|
||||
'heroicons_outline:cog-8-tooth'
|
||||
"></mat-icon>
|
||||
Settings
|
||||
</button>
|
||||
</mat-menu>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Search -->
|
||||
<div class="mt-4">
|
||||
<mat-form-field class="angor-mat-rounded angor-mat-dense w-full" [subscriptSizing]="'dynamic'">
|
||||
<mat-icon matPrefix class="icon-size-5" [svgIcon]="
|
||||
"
|
||||
></mat-icon>
|
||||
Settings
|
||||
</button>
|
||||
</mat-menu>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Search -->
|
||||
<div class="mt-4">
|
||||
<mat-form-field
|
||||
class="angor-mat-rounded angor-mat-dense w-full"
|
||||
[subscriptSizing]="'dynamic'"
|
||||
>
|
||||
<mat-icon
|
||||
matPrefix
|
||||
class="icon-size-5"
|
||||
[svgIcon]="
|
||||
'heroicons_solid:magnifying-glass'
|
||||
"></mat-icon>
|
||||
<input matInput [autocomplete]="'off'" [placeholder]="'Search or start new chat'"
|
||||
(input)="filterChats(searchField.value)" #searchField />
|
||||
</mat-form-field>
|
||||
"
|
||||
></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[autocomplete]="'off'"
|
||||
[placeholder]="'Search or start new chat'"
|
||||
(input)="filterChats(searchField.value)"
|
||||
#searchField
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chats -->
|
||||
<div class="flex-auto overflow-y-auto">
|
||||
@if (filteredChats.length > 0) {
|
||||
@for (
|
||||
chat of filteredChats;
|
||||
track trackByFn($index, chat)
|
||||
) {
|
||||
<a class="z-20 flex cursor-pointer items-center border-b px-8 py-5" [ngClass]="{
|
||||
<!-- Chats -->
|
||||
<div class="flex-auto overflow-y-auto">
|
||||
@if (filteredChats.length > 0) {
|
||||
@for (
|
||||
chat of filteredChats;
|
||||
track trackByFn($index, chat)
|
||||
) {
|
||||
<a
|
||||
class="z-20 flex cursor-pointer items-center border-b px-8 py-5"
|
||||
[ngClass]="{
|
||||
'dark:hover:bg-hover hover:bg-gray-100':
|
||||
!selectedChat ||
|
||||
selectedChat.id !== chat.id,
|
||||
'bg-primary-50 dark:bg-hover':
|
||||
selectedChat &&
|
||||
selectedChat.id === chat.id,
|
||||
}" [routerLink]="[chat.id]">
|
||||
<div class="relative flex h-10 w-10 flex-0 items-center justify-center">
|
||||
@if (chat.unreadCount > 0) {
|
||||
<div class="ring-bg-card absolute bottom-0 right-0 -ml-0.5 h-2 w-2 flex-0 rounded-full bg-primary text-on-primary ring-2 dark:bg-primary-500 dark:ring-gray-900"
|
||||
[class.ring-primary-50]="
|
||||
}"
|
||||
[routerLink]="[chat.id]"
|
||||
>
|
||||
<div
|
||||
class="relative flex h-10 w-10 flex-0 items-center justify-center"
|
||||
>
|
||||
@if (chat.unreadCount > 0) {
|
||||
<div
|
||||
class="ring-bg-card absolute bottom-0 right-0 -ml-0.5 h-2 w-2 flex-0 rounded-full bg-primary text-on-primary ring-2 dark:bg-primary-500 dark:ring-gray-900"
|
||||
[class.ring-primary-50]="
|
||||
selectedChat &&
|
||||
selectedChat.id === chat.id
|
||||
"></div>
|
||||
}
|
||||
<!-- If contact has a picture -->
|
||||
<img *ngIf="chat.contact?.picture" class="h-full w-full rounded-full object-cover"
|
||||
[src]="chat.contact?.picture" (error)="
|
||||
"
|
||||
></div>
|
||||
}
|
||||
<!-- If contact has a picture -->
|
||||
<img
|
||||
*ngIf="chat.contact?.picture"
|
||||
class="h-full w-full rounded-full object-cover"
|
||||
[src]="chat.contact?.picture"
|
||||
(error)="
|
||||
this.src =
|
||||
'/images/avatars/avatar-placeholder.png'
|
||||
" alt="Contact picture" />
|
||||
"
|
||||
alt="Contact picture"
|
||||
/>
|
||||
|
||||
<!-- If contact doesn't have a picture, display the first letter of the name -->
|
||||
<div *ngIf="!chat.contact?.picture"
|
||||
class="flex h-full w-full items-center justify-center rounded-full bg-gray-200 text-lg uppercase text-gray-600 dark:bg-gray-700 dark:text-gray-200">
|
||||
{{ chat.contact?.name?.charAt(0) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 min-w-0">
|
||||
<div class="truncate font-medium leading-5">
|
||||
{{ chat.contact?.name }}
|
||||
</div>
|
||||
<div class="text-secondary truncate leading-5" [class.text-primary]="
|
||||
<!-- If contact doesn't have a picture, display the first letter of the name -->
|
||||
<div
|
||||
*ngIf="!chat.contact?.picture"
|
||||
class="flex h-full w-full items-center justify-center rounded-full bg-gray-200 text-lg uppercase text-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{{ chat.contact?.name?.charAt(0) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 min-w-0">
|
||||
<div
|
||||
class="truncate font-medium leading-5"
|
||||
>
|
||||
{{ chat.contact?.name }}
|
||||
</div>
|
||||
<div
|
||||
class="text-secondary truncate leading-5"
|
||||
[class.text-primary]="
|
||||
chat.unreadCount > 0
|
||||
" [class.dark:text-primary-500]="
|
||||
"
|
||||
[class.dark:text-primary-500]="
|
||||
chat.unreadCount > 0
|
||||
">
|
||||
{{ chat.lastMessage | checkmessage}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto flex flex-col items-end self-start pl-2">
|
||||
<div class="text-secondary overflow-hidden whitespace-nowrap text-sm leading-5">
|
||||
{{ chat.lastMessageAt | ago }}
|
||||
</div>
|
||||
@if (chat.muted) {
|
||||
<mat-icon class="text-hint icon-size-5" [svgIcon]="
|
||||
"
|
||||
>
|
||||
{{
|
||||
chat.lastMessage | checkmessage
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ml-auto flex flex-col items-end self-start pl-2"
|
||||
>
|
||||
<div
|
||||
class="text-secondary overflow-hidden whitespace-nowrap text-sm leading-5"
|
||||
>
|
||||
{{ chat.lastMessageAt | ago }}
|
||||
</div>
|
||||
@if (chat.muted) {
|
||||
<mat-icon
|
||||
class="text-hint icon-size-5"
|
||||
[svgIcon]="
|
||||
'heroicons_solid:speaker-x-mark'
|
||||
"></mat-icon>
|
||||
"
|
||||
></mat-icon>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
} @else {
|
||||
<div class="flex h-full flex-auto flex-col items-center justify-center">
|
||||
<mat-icon class="icon-size-24" [svgIcon]="
|
||||
} @else {
|
||||
<div
|
||||
class="flex h-full flex-auto flex-col items-center justify-center"
|
||||
>
|
||||
<mat-icon
|
||||
class="icon-size-24"
|
||||
[svgIcon]="
|
||||
'heroicons_outline:chat-bubble-oval-left-ellipsis'
|
||||
"></mat-icon>
|
||||
<div class="text-secondary mt-4 text-2xl font-semibold tracking-tight">
|
||||
No chats
|
||||
</div>
|
||||
"
|
||||
></mat-icon>
|
||||
<div
|
||||
class="text-secondary mt-4 text-2xl font-semibold tracking-tight"
|
||||
>
|
||||
No chats
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="flex h-full flex-auto flex-col items-center justify-center">
|
||||
<mat-icon class="icon-size-24" [svgIcon]="
|
||||
<div
|
||||
class="flex h-full flex-auto flex-col items-center justify-center"
|
||||
>
|
||||
<mat-icon
|
||||
class="icon-size-24"
|
||||
[svgIcon]="
|
||||
'heroicons_outline:chat-bubble-oval-left-ellipsis'
|
||||
"></mat-icon>
|
||||
<div class="text-secondary mt-4 text-2xl font-semibold tracking-tight">
|
||||
No chats
|
||||
"
|
||||
></mat-icon>
|
||||
<div
|
||||
class="text-secondary mt-4 text-2xl font-semibold tracking-tight"
|
||||
>
|
||||
No chats
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- No chats template -->
|
||||
|
||||
<!-- Conversation -->
|
||||
@if (chats && chats.length > 0) {
|
||||
<div class="flex-auto border-l" [ngClass]="{
|
||||
<div
|
||||
class="flex-auto border-l"
|
||||
[ngClass]="{
|
||||
'absolute inset-0 z-20 flex lg:static lg:inset-auto':
|
||||
selectedChat && selectedChat.id,
|
||||
'hidden lg:flex': !selectedChat || !selectedChat.id,
|
||||
}">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
}"
|
||||
>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
}
|
||||
</mat-drawer-content>
|
||||
</mat-drawer-container>
|
||||
|
||||
@@ -14,13 +14,13 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { ActivatedRoute, RouterLink, RouterOutlet } from '@angular/router';
|
||||
import { catchError, of, Subject, takeUntil } from 'rxjs';
|
||||
import { AgoPipe } from 'app/shared/pipes/ago.pipe';
|
||||
import { CheckmessagePipe } from 'app/shared/pipes/checkmessage.pipe';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { ChatService } from '../chat.service';
|
||||
import { Chat, Profile } from '../chat.types';
|
||||
import { NewChatComponent } from '../new-chat/new-chat.component';
|
||||
import { ProfileComponent } from '../profile/profile.component';
|
||||
import { AgoPipe } from 'app/shared/pipes/ago.pipe';
|
||||
import { CheckmessagePipe } from 'app/shared/pipes/checkmessage.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'chat-chats',
|
||||
@@ -42,7 +42,7 @@ import { CheckmessagePipe } from 'app/shared/pipes/checkmessage.pipe';
|
||||
RouterOutlet,
|
||||
AgoPipe,
|
||||
CommonModule,
|
||||
CheckmessagePipe
|
||||
CheckmessagePipe,
|
||||
],
|
||||
})
|
||||
export class ChatsComponent implements OnInit, OnDestroy {
|
||||
@@ -67,9 +67,8 @@ export class ChatsComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private _chatService: ChatService,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private route: ActivatedRoute,
|
||||
|
||||
) { }
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Angular lifecycle hook (ngOnInit) for component initialization.
|
||||
@@ -100,7 +99,7 @@ export class ChatsComponent implements OnInit, OnDestroy {
|
||||
this._markForCheck();
|
||||
});
|
||||
|
||||
this._chatService.InitSubscribeToChatList();
|
||||
this._chatService.InitSubscribeToChatList();
|
||||
|
||||
const savedChatId = localStorage.getItem('currentChatId');
|
||||
|
||||
@@ -128,7 +127,7 @@ export class ChatsComponent implements OnInit, OnDestroy {
|
||||
this.filteredChats = this.chats;
|
||||
} else {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
this.filteredChats = this.chats.filter(chat =>
|
||||
this.filteredChats = this.chats.filter((chat) =>
|
||||
chat.contact?.name.toLowerCase().includes(lowerCaseQuery)
|
||||
);
|
||||
}
|
||||
@@ -169,6 +168,4 @@ export class ChatsComponent implements OnInit, OnDestroy {
|
||||
private _markForCheck(): void {
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -29,12 +29,14 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mt-4 text-lg font-medium">{{ chat.contact?.name }}</div>
|
||||
<div class="text-secondary mt-0.5 text-md">
|
||||
<div class="mt-4 text-lg font-medium">
|
||||
<a [routerLink]="['/profile', chat.contact?.pubKey]">
|
||||
{{ chat.contact?.name }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-secondary ml-4 mr-4 mt-0.5 text-md">
|
||||
{{ chat.contact?.about }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,15 +7,16 @@ import {
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatDrawer } from '@angular/material/sidenav';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { Chat } from '../chat.types';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'chat-contact-info',
|
||||
templateUrl: './contact-info.component.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [MatButtonModule, MatIconModule],
|
||||
imports: [MatButtonModule, MatIconModule, RouterModule],
|
||||
})
|
||||
export class ContactInfoComponent {
|
||||
@Input() chat: Chat;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
.c-img{
|
||||
max-width: 100%; border-radius: 10px;
|
||||
.c-img {
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.c-video
|
||||
{
|
||||
max-width: 100%; border-radius: 10px;
|
||||
.c-video {
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@@ -176,33 +176,44 @@
|
||||
>
|
||||
<!-- Bubble -->
|
||||
<div
|
||||
class="relative max-w-3/4 rounded-lg px-2 py-2"
|
||||
[ngClass]="{
|
||||
'bg-gray-400 text-blue-50': message.isMine,
|
||||
'bg-gray-500 text-gray-50': !message.isMine
|
||||
}"
|
||||
>
|
||||
<!-- Speech bubble tail -->
|
||||
@if (
|
||||
last ||
|
||||
chat.messages[i + 1].isMine !== message.isMine
|
||||
) {
|
||||
class="relative max-w-3/4 rounded-lg px-2 py-2"
|
||||
[ngClass]="{
|
||||
'bg-gray-400 text-blue-50':
|
||||
message.isMine,
|
||||
'bg-gray-500 text-gray-50':
|
||||
!message.isMine,
|
||||
}"
|
||||
>
|
||||
<!-- Speech bubble tail -->
|
||||
@if (
|
||||
last ||
|
||||
chat.messages[i + 1].isMine !==
|
||||
message.isMine
|
||||
) {
|
||||
<div
|
||||
class="absolute bottom-0 w-3"
|
||||
[ngClass]="{
|
||||
'-right-1 -mr-px mb-px text-gray-400':
|
||||
message.isMine,
|
||||
'-left-1 -ml-px mb-px -scale-x-1 text-gray-500':
|
||||
!message.isMine,
|
||||
}"
|
||||
>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
speechBubbleExtension
|
||||
"
|
||||
></ng-container>
|
||||
</div>
|
||||
}
|
||||
<!-- Message -->
|
||||
<div
|
||||
class="absolute bottom-0 w-3"
|
||||
[ngClass]="{
|
||||
'-right-1 -mr-px mb-px text-gray-400': message.isMine,
|
||||
'-left-1 -ml-px mb-px -scale-x-1 text-gray-500': !message.isMine
|
||||
}"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="speechBubbleExtension"></ng-container>
|
||||
</div>
|
||||
}
|
||||
<!-- Message -->
|
||||
<div
|
||||
class="min-w-4 leading-5 break-words whitespace-normal"
|
||||
[innerHTML]="parseContent(message.value)"
|
||||
></div>
|
||||
</div>
|
||||
class="min-w-4 whitespace-normal break-words leading-5"
|
||||
[innerHTML]="
|
||||
parseContent(message.value)
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Time -->
|
||||
@if (
|
||||
@@ -237,7 +248,7 @@
|
||||
class="flex items-end border-t bg-gray-50 p-4 dark:bg-transparent"
|
||||
>
|
||||
<div class="my-px flex h-11 items-center">
|
||||
<button mat-icon-button (click)="openGifDialog()">
|
||||
<button mat-icon-button (click)="openGifDialog()">
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_outline:gif'"
|
||||
></mat-icon>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { AngorMediaWatcherService } from '@angor/services/media-watcher';
|
||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { CommonModule, DatePipe, NgClass, NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
CommonModule,
|
||||
DatePipe,
|
||||
NgClass,
|
||||
NgTemplateOutlet,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
@@ -19,17 +25,16 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { AngorMediaWatcherService } from '@angor/services/media-watcher';
|
||||
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { ContactInfoComponent } from '../contact-info/contact-info.component';
|
||||
import { Chat } from 'app/layout/common/quick-chat/quick-chat.types';
|
||||
import { ChatService } from '../chat.service';
|
||||
import { PickerComponent } from '@ctrl/ngx-emoji-mart';
|
||||
import { AngorConfigService } from '@angor/services/config';
|
||||
import { GifDialogComponent } from 'app/shared/gif-dialog/gif-dialog.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { PickerComponent } from '@ctrl/ngx-emoji-mart';
|
||||
import { Chat } from 'app/layout/common/quick-chat/quick-chat.types';
|
||||
import { GifDialogComponent } from 'app/shared/gif-dialog/gif-dialog.component';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { ChatService } from '../chat.service';
|
||||
import { ContactInfoComponent } from '../contact-info/contact-info.component';
|
||||
|
||||
@Component({
|
||||
selector: 'chat-conversation',
|
||||
@@ -52,7 +57,7 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
TextFieldModule,
|
||||
DatePipe,
|
||||
PickerComponent,
|
||||
CommonModule
|
||||
CommonModule,
|
||||
],
|
||||
})
|
||||
export class ConversationComponent implements OnInit, OnDestroy {
|
||||
@@ -68,7 +73,6 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
||||
isListening: boolean = false;
|
||||
userEdited: boolean = false;
|
||||
|
||||
|
||||
constructor(
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _chatService: ChatService,
|
||||
@@ -78,11 +82,15 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
||||
public dialog: MatDialog,
|
||||
private sanitizer: DomSanitizer
|
||||
) {
|
||||
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
|
||||
const SpeechRecognition =
|
||||
(window as any).SpeechRecognition ||
|
||||
(window as any).webkitSpeechRecognition;
|
||||
|
||||
if (!SpeechRecognition) {
|
||||
console.error('Speech recognition is not supported in this browser.');
|
||||
return;
|
||||
console.error(
|
||||
'Speech recognition is not supported in this browser.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.recognition = new SpeechRecognition();
|
||||
this.recognition.lang = 'en-US';
|
||||
@@ -91,41 +99,35 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
||||
this.setupRecognitionEvents();
|
||||
}
|
||||
|
||||
|
||||
|
||||
openGifDialog(): void {
|
||||
const dialogRef = this.dialog.open(GifDialogComponent, {
|
||||
width: '600px',
|
||||
maxHeight: '80vh',
|
||||
data: { apiKey: 'LIVDSRZULELA' }
|
||||
});
|
||||
const dialogRef = this.dialog.open(GifDialogComponent, {
|
||||
width: '600px',
|
||||
maxHeight: '80vh',
|
||||
data: { apiKey: 'LIVDSRZULELA' },
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
const messageContent = result;
|
||||
|
||||
const messageContent = result
|
||||
|
||||
if (messageContent) {
|
||||
|
||||
this.messageInput.nativeElement.value = '';
|
||||
this._chatService.sendPrivateMessage(messageContent)
|
||||
.then(() => {
|
||||
if (messageContent) {
|
||||
this.messageInput.nativeElement.value = '';
|
||||
this._chatService
|
||||
.sendPrivateMessage(messageContent)
|
||||
.then(() => {
|
||||
this.messageInput.nativeElement.value = '';
|
||||
this.finalTranscript = '';
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to send message:', error);
|
||||
});
|
||||
this.finalTranscript = '';
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to send message:', error);
|
||||
});
|
||||
this.finalTranscript = '';
|
||||
this.userEdited = false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
this.userEdited = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
this._angorConfigService.config$.subscribe((config) => {
|
||||
if (config.scheme === 'auto') {
|
||||
@@ -140,22 +142,18 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
||||
.subscribe((chat: Chat) => {
|
||||
this.chat = chat;
|
||||
|
||||
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
|
||||
this._angorMediaWatcherService.onMediaChange$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(({ matchingAliases }) => {
|
||||
|
||||
if (matchingAliases.includes('lg')) {
|
||||
this.drawerMode = 'side';
|
||||
} else {
|
||||
this.drawerMode = 'over';
|
||||
}
|
||||
|
||||
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
@@ -163,50 +161,50 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
||||
parseContent(content: string): SafeHtml {
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
const cleanedContent = content.replace(/["]+/g, '');
|
||||
const parsedContent = cleanedContent.replace(urlRegex, (url) => {
|
||||
if (url.match(/\.(jpeg|jpg|gif|png|bmp|svg|webp|tiff)$/) != null) {
|
||||
return `<img src="${url}" alt="Image" width="100%" class="c-img">`;
|
||||
} else if (url.match(/\.(mp4|webm)$/) != null) {
|
||||
return `<video controls width="100%" class="c-video"><source src="${url}" type="video/mp4">Your browser does not support the video tag.</video>`;
|
||||
} else if (url.match(/(youtu\.be\/|youtube\.com\/watch\?v=)/)) {
|
||||
let videoId;
|
||||
if (url.includes('youtu.be/')) {
|
||||
videoId = url.split('youtu.be/')[1];
|
||||
} else if (url.includes('watch?v=')) {
|
||||
const urlParams = new URLSearchParams(url.split('?')[1]);
|
||||
videoId = urlParams.get('v');
|
||||
}
|
||||
return `<iframe width="100%" class="c-video" src="https://www.youtube.com/embed/${videoId}" frameborder="0" allowfullscreen></iframe>`;
|
||||
} else {
|
||||
return `<a href="${url}" target="_blank">${url}</a>`;
|
||||
}
|
||||
}).replace(/\n/g, '<br>');
|
||||
const parsedContent = cleanedContent
|
||||
.replace(urlRegex, (url) => {
|
||||
if (
|
||||
url.match(/\.(jpeg|jpg|gif|png|bmp|svg|webp|tiff)$/) != null
|
||||
) {
|
||||
return `<img src="${url}" alt="Image" width="100%" class="c-img">`;
|
||||
} else if (url.match(/\.(mp4|webm)$/) != null) {
|
||||
return `<video controls width="100%" class="c-video"><source src="${url}" type="video/mp4">Your browser does not support the video tag.</video>`;
|
||||
} else if (url.match(/(youtu\.be\/|youtube\.com\/watch\?v=)/)) {
|
||||
let videoId;
|
||||
if (url.includes('youtu.be/')) {
|
||||
videoId = url.split('youtu.be/')[1];
|
||||
} else if (url.includes('watch?v=')) {
|
||||
const urlParams = new URLSearchParams(
|
||||
url.split('?')[1]
|
||||
);
|
||||
videoId = urlParams.get('v');
|
||||
}
|
||||
return `<iframe width="100%" class="c-video" src="https://www.youtube.com/embed/${videoId}" frameborder="0" allowfullscreen></iframe>`;
|
||||
} else {
|
||||
return `<a href="${url}" target="_blank">${url}</a>`;
|
||||
}
|
||||
})
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
return this.sanitizer.bypassSecurityTrustHtml(parsedContent);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('input')
|
||||
@HostListener('ngModelChange')
|
||||
private _resizeMessageInput(): void {
|
||||
|
||||
this._ngZone.runOutsideAngular(() => {
|
||||
setTimeout(() => {
|
||||
|
||||
this.messageInput.nativeElement.style.height = 'auto';
|
||||
|
||||
|
||||
this._changeDetectorRef.detectChanges();
|
||||
|
||||
|
||||
this.messageInput.nativeElement.style.height = `${this.messageInput.nativeElement.scrollHeight}px`;
|
||||
|
||||
|
||||
this._changeDetectorRef.detectChanges();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setupRecognitionEvents(): void {
|
||||
this.recognition.onresult = (event: any) => {
|
||||
let interimTranscript = '';
|
||||
@@ -220,9 +218,9 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!this.userEdited) {
|
||||
this.messageInput.nativeElement.value = this.finalTranscript + interimTranscript;
|
||||
this.messageInput.nativeElement.value =
|
||||
this.finalTranscript + interimTranscript;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -235,7 +233,6 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
toggleSpeechRecognition(): void {
|
||||
this.finalTranscript = '';
|
||||
if (this.isListening) {
|
||||
@@ -248,43 +245,32 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleUserInput(event: Event): void {
|
||||
this.userEdited = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
|
||||
openContactInfo(): void {
|
||||
|
||||
this.drawerOpened = true;
|
||||
|
||||
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
resetChat(): void {
|
||||
this._chatService.resetChat();
|
||||
|
||||
|
||||
this.drawerOpened = false;
|
||||
|
||||
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
toggleMuteNotifications(): void {
|
||||
|
||||
this.chat.muted = !this.chat.muted;
|
||||
|
||||
|
||||
this._chatService.updateChat(this.chat.id, this.chat).subscribe();
|
||||
}
|
||||
|
||||
@@ -292,9 +278,10 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
||||
return item.id || index;
|
||||
}
|
||||
|
||||
|
||||
detectSystemTheme() {
|
||||
const darkSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const darkSchemeMedia = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
);
|
||||
this.darkMode = darkSchemeMedia.matches;
|
||||
|
||||
darkSchemeMedia.addEventListener('change', (event) => {
|
||||
@@ -312,10 +299,10 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
||||
sendMessage(): void {
|
||||
const messageContent = this.messageInput.nativeElement.value.trim();
|
||||
|
||||
if (messageContent) {
|
||||
|
||||
if (messageContent) {
|
||||
this.messageInput.nativeElement.value = '';
|
||||
this._chatService.sendPrivateMessage(messageContent)
|
||||
this._chatService
|
||||
.sendPrivateMessage(messageContent)
|
||||
.then(() => {
|
||||
this.messageInput.nativeElement.value = '';
|
||||
this.finalTranscript = '';
|
||||
@@ -336,5 +323,4 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
||||
toggleEmojiPicker() {
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
) {
|
||||
<div
|
||||
class="text-secondary sticky top-0 z-10 -mt-px border-b border-t bg-gray-100 px-6 py-1 font-medium uppercase dark:bg-gray-900 md:px-8"
|
||||
(click)="openChat(contact)">
|
||||
(click)="openChat(contact)"
|
||||
>
|
||||
{{ contact.name.charAt(0) }}
|
||||
</div>
|
||||
}
|
||||
@@ -35,7 +36,7 @@
|
||||
<div
|
||||
class="z-20 flex cursor-pointer items-center border-b px-6 py-4 dark:hover:bg-hover hover:bg-gray-100 md:px-8"
|
||||
(click)="openChat(contact)"
|
||||
>
|
||||
>
|
||||
<div
|
||||
class="flex h-10 w-10 flex-0 items-center justify-center overflow-hidden rounded-full"
|
||||
>
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatDrawer } from '@angular/material/sidenav';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Contact } from '../chat.types';
|
||||
import { ChatService } from '../chat.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { ChatService } from '../chat.service';
|
||||
import { Contact } from '../chat.types';
|
||||
|
||||
@Component({
|
||||
selector: 'chat-new-chat',
|
||||
@@ -30,8 +30,10 @@ export class NewChatComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _chatService: ChatService, private router: Router) {}
|
||||
|
||||
constructor(
|
||||
private _chatService: ChatService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Contacts
|
||||
@@ -42,14 +44,12 @@ export class NewChatComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next(null);
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
|
||||
trackByFn(index: number, item: any): any {
|
||||
return item.id || index;
|
||||
}
|
||||
@@ -65,8 +65,7 @@ export class NewChatComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
complete: () => {
|
||||
this.drawer.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatDrawer } from '@angular/material/sidenav';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Profile } from '../chat.types';
|
||||
import { ChatService } from '../chat.service';
|
||||
import { Profile } from '../chat.types';
|
||||
|
||||
@Component({
|
||||
selector: 'chat-profile',
|
||||
|
||||
239
src/app/components/event-list/event-list.component.html
Normal file
239
src/app/components/event-list/event-list.component.html
Normal file
@@ -0,0 +1,239 @@
|
||||
|
||||
<div
|
||||
class=""
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="2"
|
||||
[infiniteScrollThrottle]="500"
|
||||
(scrolled)="loadMoreEvents()"
|
||||
[scrollWindow]="true"
|
||||
>
|
||||
<angor-card class="mb-8 flex w-full flex-col" #expandableComments="angorCard"
|
||||
*ngFor="let event of events$ | async let i = index">
|
||||
<div class="mx-6 mb-4 mt-6 flex items-center sm:mx-8">
|
||||
<img class="mr-4 h-10 w-10 rounded-full"
|
||||
[src]="event.picture || 'images/avatars/avatar-placeholder.png'"
|
||||
onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
|
||||
alt="{{ event.username }}" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-semibold leading-none">{{
|
||||
event.username
|
||||
}}</span>
|
||||
<span class="text-secondary mt-1 text-sm leading-none">{{
|
||||
getTimeFromNow(event)
|
||||
}}</span>
|
||||
</div>
|
||||
<button class="-mr-4 ml-auto" mat-icon-button [matMenuTriggerFor]="postCardMenu02">
|
||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:ellipsis-vertical'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #postCardMenu02="matMenu">
|
||||
<button mat-menu-item>
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="mr-3 icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:arrow-up-tray'"></mat-icon>
|
||||
<span>Save post</span>
|
||||
</span>
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="mr-3 icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:eye-slash'"></mat-icon>
|
||||
<span>Hide post</span>
|
||||
</span>
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="mr-3 icon-size-5" [svgIcon]="'heroicons_solid:clock'"></mat-icon>
|
||||
<span>Snooze for 30 days</span>
|
||||
</span>
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="mr-3 icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:minus-circle'"></mat-icon>
|
||||
<span>Hide all</span>
|
||||
</span>
|
||||
</button>
|
||||
<mat-divider class="my-2"></mat-divider>
|
||||
<button mat-menu-item>
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="mr-3 icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:exclamation-triangle'"></mat-icon>
|
||||
<span>Report post</span>
|
||||
</span>
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="mr-3 icon-size-5" [svgIcon]="'heroicons_solid:bell'"></mat-icon>
|
||||
<span>Turn on notifications for this post</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
<div class="mx-6 mb-6 mt-2 sm:mx-8" [innerHTML]="parseContent(event.content)"></div>
|
||||
<div class="relative mb-4">
|
||||
<!-- image or video -->
|
||||
</div>
|
||||
<div class="mx-3 flex items-center sm:mx-5">
|
||||
<button class="mr-1 px-3" mat-button (click)="toggleLike(event)">
|
||||
<mat-icon class="text-red-500 icon-size-5" [ngClass]="{ 'heart-beat': event.likedByMe }"
|
||||
[svgIcon]="
|
||||
event.likedByMe
|
||||
? 'heroicons_solid:heart'
|
||||
: 'heroicons_outline:heart'
|
||||
">
|
||||
</mat-icon>
|
||||
<span class="ml-2">{{ event.likeCount }} Like</span>
|
||||
</button>
|
||||
|
||||
|
||||
<button class="mr-1 px-3" mat-button (click)="
|
||||
expandableComments.expanded = !expandableComments.expanded
|
||||
">
|
||||
<mat-icon class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:chat-bubble-left-ellipsis'"></mat-icon>
|
||||
<span class="ml-2">Comment</span>
|
||||
</button>
|
||||
<button class="mr-1 px-3" mat-button>
|
||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:share'"></mat-icon>
|
||||
<span class="ml-2">Share</span>
|
||||
</button>
|
||||
</div>
|
||||
<hr class="mx-6 mb-6 mt-4 border-b sm:mx-8" />
|
||||
<div class="mx-6 mb-4 flex flex-col sm:mx-8 sm:mb-6 sm:flex-row sm:items-center">
|
||||
<div class="flex items-center">
|
||||
<img class="text-card m-0.5 h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="images/avatars/avatar-placeholder.png" alt="Card cover image" />
|
||||
<img class="text-card m-0.5 -ml-3 h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="images/avatars/avatar-placeholder.png" alt="Card cover image" />
|
||||
<img class="text-card m-0.5 -ml-3 h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="images/avatars/avatar-placeholder.png" alt="Card cover image" />
|
||||
<img class="text-card m-0.5 -ml-3 h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="images/avatars/avatar-placeholder.png" alt="Card cover image" />
|
||||
<div class="ml-3 text-md tracking-tight">
|
||||
⚡ {{ event.zapCount }} zap
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden flex-auto sm:flex"></div>
|
||||
<div class="mt-4 flex items-center sm:mt-0">
|
||||
<button class="-ml-2 mr-1 px-3 sm:ml-0" mat-button>
|
||||
{{ event.repostCount }} shares
|
||||
</button>
|
||||
<button class="px-3 sm:-mr-4" mat-button (click)="
|
||||
expandableComments.expanded =
|
||||
!expandableComments.expanded
|
||||
">
|
||||
<span class="mr-1">{{ event.replyCount }} Comments</span>
|
||||
<mat-icon class="rotate-0 transition-transform duration-150 ease-in-out icon-size-5"
|
||||
[ngClass]="{
|
||||
'rotate-180': expandableComments.expanded,
|
||||
}" [svgIcon]="'heroicons_mini:chevron-down'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container angorCardExpansion>
|
||||
<hr class="m-0 border-b" />
|
||||
<div class="mx-4 mb-3 mt-6 flex flex-col sm:mx-8">
|
||||
<div class="flex items-start">
|
||||
<img class="mr-5 h-12 w-12 rounded-full object-cover" [src]="
|
||||
currentUserMetadata?.picture ||
|
||||
'images/avatars/avatar-placeholder.png'
|
||||
" onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
|
||||
alt="{{
|
||||
currentUserMetadata?.display_name ||
|
||||
currentUserMetadata?.name ||
|
||||
'Avatar'
|
||||
}}" />
|
||||
|
||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||
<textarea
|
||||
[ngModel]="getComment(i)"
|
||||
(ngModelChange)="setComment(i, $event)"
|
||||
placeholder="Write a comment..."
|
||||
matInput>
|
||||
</textarea>
|
||||
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<button mat-icon-button (click)="toggleCommentEmojiPicker(i)">
|
||||
<mat-icon class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:face-smile'"></mat-icon>
|
||||
</button>
|
||||
<div *ngIf="eventStates[i]?.showEmojiPicker" class="emoji-picker-container-global">
|
||||
<emoji-mart (emojiClick)="addEmojiToComment($event, i)" [darkMode]="darkMode"></emoji-mart>
|
||||
</div>
|
||||
<button mat-icon-button>
|
||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:photo'"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button>
|
||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:sparkles'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button mat-button (click)="sendComment(event, i)">
|
||||
<mat-icon [svgIcon]="'heroicons_solid:paper-airplane'"></mat-icon>
|
||||
<span>Send</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="max-h-120 overflow-y-auto">
|
||||
<div class="relative mx-4 my-6 flex flex-col sm:mx-8">
|
||||
<div class="flex items-start mb-4" *ngFor="let reply of event.replies">
|
||||
<img
|
||||
class="mr-4 h-8 w-8 rounded-full"
|
||||
[src]="reply.picture || 'images/avatars/avatar-placeholder.png'"
|
||||
onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
|
||||
alt="{{ reply.username }}"
|
||||
/>
|
||||
<div class="mt-0.5 flex flex-col">
|
||||
<span>
|
||||
<b>{{ reply.username }}: </b>
|
||||
{{ reply.content }}
|
||||
</span>
|
||||
<div
|
||||
class="text-secondary mt-2 flex items-center text-sm"
|
||||
>
|
||||
<span
|
||||
class="mr-2 cursor-pointer hover:underline"
|
||||
>Like</span
|
||||
>
|
||||
<span
|
||||
class="mr-2 cursor-pointer hover:underline"
|
||||
>Reply</span
|
||||
>
|
||||
<span
|
||||
class="mr-2 cursor-pointer hover:underline"
|
||||
>Hide replies</span
|
||||
>
|
||||
<span class="mr-2">•</span>
|
||||
<span>{{getTimeFromNow(reply) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-container>
|
||||
</angor-card>
|
||||
|
||||
<div *ngIf="isLoading" class="loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
Loading events...
|
||||
</div>
|
||||
|
||||
<button
|
||||
*ngIf="!noMoreEvents && !isLoading"
|
||||
class="load-more-btn"
|
||||
(click)="loadMoreEvents()"
|
||||
>
|
||||
Load More Events
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="noMoreEvents" class="no-more-events">No more events to load.</div>
|
||||
158
src/app/components/event-list/event-list.component.scss
Normal file
158
src/app/components/event-list/event-list.component.scss
Normal file
@@ -0,0 +1,158 @@
|
||||
:host {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
|
||||
.spinner {
|
||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||
border-left-color: #009fb5;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 20px 0;
|
||||
|
||||
.event-item {
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.event-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.profile-picture {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
border: 2px solid #009fb5;
|
||||
}
|
||||
|
||||
.event-info {
|
||||
.username {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 0.9em;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-content {
|
||||
margin: 10px 0;
|
||||
font-size: 1.1em;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.event-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #009fb5;
|
||||
font-size: 1.1em;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #007f91;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-replies {
|
||||
margin-top: 15px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding-top: 10px;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
.reply-item {
|
||||
margin: 5px 0;
|
||||
font-size: 0.9em;
|
||||
|
||||
.reply-username {
|
||||
font-weight: bold;
|
||||
color: #009fb5;
|
||||
}
|
||||
|
||||
.reply-content {
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-more-events {
|
||||
text-align: center;
|
||||
color: #555;
|
||||
margin: 20px 0;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.load-more-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 1.1em;
|
||||
background-color: #009fb5;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #007f91;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
252
src/app/components/event-list/event-list.component.ts
Normal file
252
src/app/components/event-list/event-list.component.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { PaginatedEventService } from 'app/services/event.service';
|
||||
import { NewEvent } from 'app/types/NewEvent';
|
||||
import { AngorCardComponent } from '@angor/components/card';
|
||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { NgClass, CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSlideToggle } from '@angular/material/slide-toggle';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { PickerComponent } from '@ctrl/ngx-emoji-mart';
|
||||
import { QRCodeModule } from 'angularx-qrcode';
|
||||
import { SafeUrlPipe } from 'app/shared/pipes/safe-url.pipe';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
|
||||
@Component({
|
||||
selector: 'app-event-list',
|
||||
templateUrl: './event-list.component.html',
|
||||
styleUrls: ['./event-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: true,
|
||||
imports: [
|
||||
RouterLink,
|
||||
AngorCardComponent,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
TextFieldModule,
|
||||
MatDividerModule,
|
||||
MatTooltipModule,
|
||||
NgClass,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
QRCodeModule,
|
||||
PickerComponent,
|
||||
MatSlideToggle,
|
||||
SafeUrlPipe,
|
||||
MatProgressSpinnerModule,
|
||||
InfiniteScrollModule,
|
||||
]
|
||||
})
|
||||
export class EventListComponent implements OnInit, OnDestroy {
|
||||
@Input() pubkeys: string[] = [];
|
||||
@Input() currentUserMetadata: any;
|
||||
|
||||
events$: Observable<NewEvent[]>;
|
||||
eventStates: { showEmojiPicker: boolean; comment: string }[] = [];
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
isLoading = false;
|
||||
noMoreEvents = false;
|
||||
|
||||
constructor(
|
||||
private paginatedEventService: PaginatedEventService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private sanitizer: DomSanitizer
|
||||
) {
|
||||
this.events$ = this.paginatedEventService.getEventStream();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.resetAll();
|
||||
}
|
||||
|
||||
|
||||
|
||||
subscribeToEvents(): void {
|
||||
this.unsubscribeAll();
|
||||
|
||||
|
||||
if (!this.pubkeys || this.pubkeys.length === 0) {
|
||||
console.warn('No public keys provided');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.paginatedEventService.subscribeToEvents(this.pubkeys)
|
||||
.then(() => {
|
||||
console.log('Subscribed to events for the new user.');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error subscribing to events:', error);
|
||||
});
|
||||
|
||||
|
||||
const eventSub = this.events$.subscribe(events => {
|
||||
const relevantEvents = events.filter(event => this.pubkeys.includes(event.pubkey));
|
||||
|
||||
this.eventStates = relevantEvents.map(() => ({
|
||||
showEmojiPicker: false,
|
||||
comment: ''
|
||||
}));
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.subscriptions.push(eventSub);
|
||||
}
|
||||
|
||||
|
||||
|
||||
resetAll(): void {
|
||||
this.unsubscribeAll();
|
||||
this.clearComponentState();
|
||||
this.paginatedEventService.clearEvents();
|
||||
this.subscribeToEvents();
|
||||
this.loadInitialEvents();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
unsubscribeAll(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
clearComponentState(): void {
|
||||
this.eventStates = [];
|
||||
this.isLoading = false;
|
||||
this.noMoreEvents = false;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
loadInitialEvents(): void {
|
||||
if (this.pubkeys.length === 0) {
|
||||
console.warn('No pubkeys provided');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.paginatedEventService.loadMoreEvents(this.pubkeys).finally(() => {
|
||||
this.isLoading = false;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
loadMoreEvents(): void {
|
||||
if (!this.isLoading && !this.noMoreEvents) {
|
||||
this.isLoading = true;
|
||||
this.paginatedEventService.loadMoreEvents(this.pubkeys).finally(() => {
|
||||
this.isLoading = false;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
getComment(index: number): string {
|
||||
return this.eventStates[index]?.comment || '';
|
||||
}
|
||||
|
||||
setComment(index: number, value: string): void {
|
||||
if (this.eventStates[index]) {
|
||||
this.eventStates[index].comment = value;
|
||||
}
|
||||
}
|
||||
|
||||
getSanitizedContent(content: string): SafeHtml {
|
||||
return this.sanitizer.bypassSecurityTrustHtml(content);
|
||||
}
|
||||
|
||||
sendLike(event: NewEvent): void {
|
||||
if (!event.likedByMe) {
|
||||
this.paginatedEventService.sendLikeEvent(event).then(() => {
|
||||
event.likedByMe = true;
|
||||
event.likeCount++;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}).catch(error => console.error('Failed to send like:', error));
|
||||
}
|
||||
}
|
||||
|
||||
toggleLike(event: NewEvent): void {
|
||||
this.sendLike(event);
|
||||
}
|
||||
|
||||
toggleCommentEmojiPicker(index: number): void {
|
||||
this.eventStates[index].showEmojiPicker = !this.eventStates[index].showEmojiPicker;
|
||||
}
|
||||
|
||||
addEmojiToComment(event: any, index: number): void {
|
||||
this.eventStates[index].comment += event.emoji.native;
|
||||
this.eventStates[index].showEmojiPicker = false;
|
||||
}
|
||||
|
||||
sendComment(event: NewEvent, index: number): void {
|
||||
const comment = this.eventStates[index].comment;
|
||||
if (comment.trim() !== '') {
|
||||
this.paginatedEventService.sendReplyEvent(event, comment).then(() => {
|
||||
this.eventStates[index].comment = '';
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
trackById(index: number, item: NewEvent): string {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeAll();
|
||||
}
|
||||
|
||||
getTimeFromNow(event: NewEvent): string {
|
||||
return event.fromNow;
|
||||
}
|
||||
|
||||
parseContent(content: string): SafeHtml {
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
const cleanedContent = content.replace(/["]+/g, '');
|
||||
const parsedContent = cleanedContent
|
||||
.replace(urlRegex, (url) => {
|
||||
if (url.match(/\.(jpeg|jpg|gif|png|bmp|svg|webp|tiff)$/) != null) {
|
||||
return `<img src="${url}" alt="Image" width="100%" class="c-img">`;
|
||||
} else if (url.match(/\.(mp4|webm)$/) != null) {
|
||||
return `<video controls width="100%" class="c-video"><source src="${url}" type="video/mp4">Your browser does not support the video tag.</video>`;
|
||||
} else if (url.match(/(youtu\.be\/|youtube\.com\/watch\?v=)/)) {
|
||||
let videoId;
|
||||
if (url.includes('youtu.be/')) {
|
||||
videoId = url.split('youtu.be/')[1];
|
||||
} else if (url.includes('watch?v=')) {
|
||||
const urlParams = new URLSearchParams(url.split('?')[1]);
|
||||
videoId = urlParams.get('v');
|
||||
}
|
||||
return `<iframe width="100%" class="c-video" src="https://www.youtube.com/embed/${videoId}" frameborder="0" allowfullscreen></iframe>`;
|
||||
} else {
|
||||
return `<a href="${url}" target="_blank">${url}</a>`;
|
||||
}
|
||||
})
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
return this.sanitizer.bypassSecurityTrustHtml(parsedContent);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,37 @@
|
||||
<div class="absolute inset-0 flex min-w-0 flex-col overflow-y-auto">
|
||||
<!-- Header -->
|
||||
<div class="dark relative flex-0 overflow-hidden bg-gray-800 px-4 py-8 sm:p-16">
|
||||
<div
|
||||
class="dark relative flex-0 overflow-hidden bg-gray-800 px-4 py-8 sm:p-16"
|
||||
>
|
||||
<!-- Background -->
|
||||
<svg class="pointer-events-none absolute inset-0" viewBox="0 0 960 540" width="100%" height="100%"
|
||||
preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="text-gray-700 opacity-25" fill="none" stroke="currentColor" stroke-width="100">
|
||||
<svg
|
||||
class="pointer-events-none absolute inset-0"
|
||||
viewBox="0 0 960 540"
|
||||
width="100%"
|
||||
height="100%"
|
||||
preserveAspectRatio="xMidYMax slice"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
class="text-gray-700 opacity-25"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="100"
|
||||
>
|
||||
<circle r="234" cx="196" cy="23"></circle>
|
||||
<circle r="234" cx="790" cy="491"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="relative z-10 flex flex-col items-center">
|
||||
<h2 class="text-xl font-semibold">Explore Projects</h2>
|
||||
<div class="mt-1 text-center text-4xl font-extrabold leading-tight tracking-tight sm:text-7xl">
|
||||
<div
|
||||
class="mt-1 text-center text-4xl font-extrabold leading-tight tracking-tight sm:text-7xl"
|
||||
>
|
||||
What’s your next investment?
|
||||
</div>
|
||||
<div class="text-secondary mt-6 max-w-2xl text-center tracking-tight sm:text-2xl">
|
||||
<div
|
||||
class="text-secondary mt-6 max-w-2xl text-center tracking-tight sm:text-2xl"
|
||||
>
|
||||
Check out our projects and find your next investment
|
||||
opportunity.
|
||||
</div>
|
||||
@@ -23,110 +40,188 @@
|
||||
|
||||
<!-- Main -->
|
||||
<div class="p-6 sm:p-10">
|
||||
<div class="mx-auto flex w-full max-w-xs flex-auto flex-col sm:max-w-5xl">
|
||||
<div
|
||||
class="mx-auto flex w-full max-w-xs flex-auto flex-col sm:max-w-5xl"
|
||||
>
|
||||
<!-- Filters -->
|
||||
<div class="flex w-full max-w-xs flex-col items-center justify-between sm:max-w-none sm:flex-row">
|
||||
<div
|
||||
class="flex w-full max-w-xs flex-col items-center justify-between sm:max-w-none sm:flex-row"
|
||||
>
|
||||
<!-- Search bar with clear button -->
|
||||
<div class="flex items-center space-x-2 w-full sm:w-auto">
|
||||
<mat-form-field class="mt-4 w-full sm:w-80" [subscriptSizing]="'dynamic'">
|
||||
<mat-icon matPrefix class="icon-size-5" [svgIcon]="'heroicons_solid:magnifying-glass'"></mat-icon>
|
||||
<input (keyup.enter)="filterByQuery(query.value)" placeholder="Search ..." matInput #query />
|
||||
<div class="flex w-full items-center space-x-2 sm:w-auto">
|
||||
<mat-form-field
|
||||
class="mt-4 w-full sm:w-80"
|
||||
[subscriptSizing]="'dynamic'"
|
||||
>
|
||||
<mat-icon
|
||||
matPrefix
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:magnifying-glass'"
|
||||
></mat-icon>
|
||||
<input
|
||||
(keyup.enter)="filterByQuery(query.value)"
|
||||
placeholder="Search ..."
|
||||
matInput
|
||||
#query
|
||||
/>
|
||||
</mat-form-field>
|
||||
<!-- Clear search button -->
|
||||
<button mat-icon-button color="warn" *ngIf="showCloseSearchButton" (click)="resetSearch(query)" class="mt-4">
|
||||
<mat-icon [svgIcon]="'heroicons_solid:x-mark'"></mat-icon>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="warn"
|
||||
*ngIf="showCloseSearchButton"
|
||||
(click)="resetSearch(query)"
|
||||
class="mt-4"
|
||||
>
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_solid:x-mark'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="success" *ngIf="!showCloseSearchButton" (click)="filterByQuery(query.value)" class="mt-4">
|
||||
<mat-icon [svgIcon]="'heroicons_solid:magnifying-glass'"></mat-icon>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="success"
|
||||
*ngIf="!showCloseSearchButton"
|
||||
(click)="filterByQuery(query.value)"
|
||||
class="mt-4"
|
||||
>
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_solid:magnifying-glass'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Toggle completed -->
|
||||
<mat-slide-toggle class="mt-8 sm:ml-auto sm:mt-0" [color]="'primary'" (change)="toggleCompleted($event)">
|
||||
<mat-slide-toggle
|
||||
class="mt-8 sm:ml-auto sm:mt-0"
|
||||
[color]="'primary'"
|
||||
(change)="toggleCompleted($event)"
|
||||
>
|
||||
Hide completed
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto flex w-full flex-auto flex-col sm:max-w-5xl">
|
||||
<!-- Project Cards -->
|
||||
<div class="mt-10 grid w-full min-w-0 grid-cols-1 gap-6 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2">
|
||||
<div
|
||||
class="mt-10 grid w-full min-w-0 grid-cols-1 gap-6 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2"
|
||||
>
|
||||
<!-- Loop through projects and render cards -->
|
||||
<ng-container *ngFor="let project of filteredProjects">
|
||||
<angor-card class="filter-info flex w-full flex-col">
|
||||
<div class="flex h-32">
|
||||
<img class="object-cover" [src]="
|
||||
<img
|
||||
class="object-cover"
|
||||
[src]="
|
||||
getSafeUrl(project?.banner, true) ||
|
||||
'images/pages/profile/cover.jpg'
|
||||
" onerror="this.onerror=null; this.src='/images/pages/profile/cover.jpg';"
|
||||
alt="Card cover image" />
|
||||
"
|
||||
onerror="this.onerror=null; this.src='/images/pages/profile/cover.jpg';"
|
||||
alt="Card cover image"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex px-8">
|
||||
<div class="bg-card -mt-12 rounded-full p-1">
|
||||
<img class="h-24 w-24 rounded-full object-cover" [src]="
|
||||
<img
|
||||
class="h-24 w-24 rounded-full object-cover"
|
||||
[src]="
|
||||
getSafeUrl(project?.picture, false) ||
|
||||
'images/avatars/avatar-placeholder.png'
|
||||
" onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
|
||||
alt="Project logo" />
|
||||
"
|
||||
onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
|
||||
alt="Project logo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col px-8 pb-6 pt-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="mr-4 min-w-0 flex-1">
|
||||
@if (project.displayName || project.name) {
|
||||
<div class="truncate text-2xl font-semibold leading-tight" role="button" (click)="
|
||||
<div
|
||||
class="truncate text-2xl font-semibold leading-tight"
|
||||
role="button"
|
||||
(click)="
|
||||
goToProjectDetails(project)
|
||||
">
|
||||
{{
|
||||
project.displayName ||
|
||||
project.nostrPubKey
|
||||
}}
|
||||
</div>
|
||||
"
|
||||
>
|
||||
{{
|
||||
project.displayName ||
|
||||
project.nostrPubKey
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
@if (
|
||||
!project.name && !project.displayName
|
||||
!project.name && !project.displayName
|
||||
) {
|
||||
<div class="truncate text-2xl font-semibold leading-tight">
|
||||
{{
|
||||
project.displayName ||
|
||||
project.nostrPubKey
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
class="truncate text-2xl font-semibold leading-tight"
|
||||
>
|
||||
{{
|
||||
project.displayName ||
|
||||
project.nostrPubKey
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
<div class="text-secondary mt-1 truncate leading-tight">
|
||||
<div
|
||||
class="text-secondary mt-1 truncate leading-tight"
|
||||
>
|
||||
{{
|
||||
project.about ||
|
||||
'No description available'
|
||||
project.about ||
|
||||
'No description available'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
@if (project.displayName || project.name) {
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full border">
|
||||
<button mat-icon-button (click)="
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full border"
|
||||
>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="
|
||||
openChat(project.nostrPubKey)
|
||||
">
|
||||
<mat-icon class="icon-size-5" [svgIcon]="
|
||||
"
|
||||
>
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="
|
||||
'heroicons_outline:chat-bubble-left-right'
|
||||
"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
"
|
||||
></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<hr class="my-6 w-full border-t" />
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-secondary mr-3 text-md font-medium">
|
||||
<div
|
||||
class="text-secondary mr-3 text-md font-medium"
|
||||
>
|
||||
{{ project.totalInvestmentsCount || 0 }}
|
||||
investors
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<ng-container *ngFor="
|
||||
let investor of [].constructor(project.totalInvestmentsCount || 0);
|
||||
let i = index
|
||||
">
|
||||
<ng-container
|
||||
*ngFor="
|
||||
let investor of [].constructor(
|
||||
project.totalInvestmentsCount ||
|
||||
0
|
||||
);
|
||||
let i = index
|
||||
"
|
||||
>
|
||||
<ng-container *ngIf="i < 10">
|
||||
<img class="text-card ring-bg-card m-0.5 h-6 w-6 rounded-full ring-2"
|
||||
<img
|
||||
class="text-card ring-bg-card m-0.5 h-6 w-6 rounded-full ring-2"
|
||||
[ngClass]="{
|
||||
'-ml-3': project.totalInvestmentsCount > 1 && i > 0
|
||||
}" [src]="'images/avatars/avatar-placeholder.png'"
|
||||
alt="Investor avatar {{ i + 1 }}" />
|
||||
'-ml-3':
|
||||
project.totalInvestmentsCount >
|
||||
1 && i > 0,
|
||||
}"
|
||||
[src]="
|
||||
'images/avatars/avatar-placeholder.png'
|
||||
"
|
||||
alt="Investor avatar {{
|
||||
i + 1
|
||||
}}"
|
||||
/>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -135,18 +230,32 @@
|
||||
</angor-card>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="filteredProjects.length ==0">
|
||||
<div class="flex flex-auto flex-col items-center justify-center bg-gray-100 dark:bg-transparent">
|
||||
<mat-icon class="icon-size-24"
|
||||
[svgIcon]="'heroicons_outline:archive-box-x-mark'"></mat-icon>
|
||||
<div class="text-secondary mt-4 text-2xl font-semibold tracking-tight">
|
||||
<ng-container *ngIf="filteredProjects.length == 0">
|
||||
<div
|
||||
class="flex flex-auto flex-col items-center justify-center bg-gray-100 dark:bg-transparent"
|
||||
>
|
||||
<mat-icon
|
||||
class="icon-size-24"
|
||||
[svgIcon]="'heroicons_outline:archive-box-x-mark'"
|
||||
></mat-icon>
|
||||
<div
|
||||
class="text-secondary mt-4 text-2xl font-semibold tracking-tight"
|
||||
>
|
||||
No project
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- Load More Button -->
|
||||
<div *ngIf="filteredProjects.length >0" class="mt-10 flex justify-center">
|
||||
<button mat-raised-button color="primary" (click)="loadProjects()" [disabled]="loading">
|
||||
<div
|
||||
*ngIf="filteredProjects.length > 0"
|
||||
class="mt-10 flex justify-center"
|
||||
>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="loadProjects()"
|
||||
[disabled]="loading"
|
||||
>
|
||||
{{ loading ? 'Loading...' : 'Load More Projects' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { Component, OnInit, OnDestroy, ViewEncapsulation, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ProjectsService } from '../../services/projects.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AngorCardComponent } from '@angor/components/card';
|
||||
import { AngorFindByKeyPipe } from '@angor/pipes/find-by-key';
|
||||
import { CdkScrollable } from '@angular/cdk/scrolling';
|
||||
import {
|
||||
CommonModule,
|
||||
I18nPluralPipe,
|
||||
NgClass,
|
||||
PercentPipe,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -11,18 +23,16 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { AngorCardComponent } from '@angor/components/card';
|
||||
import { AngorFindByKeyPipe } from '@angor/pipes/find-by-key';
|
||||
import { CdkScrollable } from '@angular/cdk/scrolling';
|
||||
import { NgClass, PercentPipe, I18nPluralPipe, CommonModule } from '@angular/common';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { Project } from 'app/interface/project.interface';
|
||||
import { IndexedDBService } from 'app/services/indexed-db.service';
|
||||
import { MetadataService } from 'app/services/metadata.service';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { IndexedDBService } from 'app/services/indexed-db.service';
|
||||
import { Project } from 'app/interface/project.interface';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { Contact } from '../chat/chat.types';
|
||||
import { ProjectsService } from '../../services/projects.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { ChatService } from '../chat/chat.service';
|
||||
import { Contact } from '../chat/chat.types';
|
||||
|
||||
@Component({
|
||||
selector: 'explore',
|
||||
@@ -30,10 +40,23 @@ import { ChatService } from '../chat/chat.service';
|
||||
templateUrl: './explore.component.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
imports: [
|
||||
MatButtonModule, RouterLink, MatIconModule, AngorCardComponent,
|
||||
CdkScrollable, MatFormFieldModule, MatSelectModule, MatOptionModule,
|
||||
MatInputModule, MatSlideToggleModule, NgClass, MatTooltipModule,
|
||||
MatProgressBarModule, AngorFindByKeyPipe, PercentPipe, I18nPluralPipe, CommonModule
|
||||
MatButtonModule,
|
||||
RouterLink,
|
||||
MatIconModule,
|
||||
AngorCardComponent,
|
||||
CdkScrollable,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatOptionModule,
|
||||
MatInputModule,
|
||||
MatSlideToggleModule,
|
||||
NgClass,
|
||||
MatTooltipModule,
|
||||
MatProgressBarModule,
|
||||
AngorFindByKeyPipe,
|
||||
PercentPipe,
|
||||
I18nPluralPipe,
|
||||
CommonModule,
|
||||
],
|
||||
})
|
||||
export class ExploreComponent implements OnInit, OnDestroy {
|
||||
@@ -45,7 +68,6 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
||||
filteredProjects: Project[] = [];
|
||||
showCloseSearchButton: boolean = false;
|
||||
|
||||
|
||||
constructor(
|
||||
private projectService: ProjectsService,
|
||||
private router: Router,
|
||||
@@ -55,7 +77,7 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private sanitizer: DomSanitizer,
|
||||
private _chatService: ChatService
|
||||
) { }
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.loadInitialProjects();
|
||||
@@ -96,21 +118,26 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
||||
this.filteredProjects = [...this.projects];
|
||||
this.stateService.setProjects(this.projects);
|
||||
|
||||
const pubkeys = projects.map(p => p.nostrPubKey);
|
||||
const pubkeys = projects.map((p) => p.nostrPubKey);
|
||||
await this.loadMetadataForProjects(pubkeys);
|
||||
|
||||
} catch (error) {
|
||||
this.handleError('Error fetching projects from service');
|
||||
}
|
||||
}
|
||||
private subscribeToMetadataUpdates(): void {
|
||||
this.indexedDBService.getMetadataStream()
|
||||
this.indexedDBService
|
||||
.getMetadataStream()
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((updatedMetadata: any) => {
|
||||
if (updatedMetadata) {
|
||||
const projectToUpdate = this.projects.find(p => p.nostrPubKey === updatedMetadata.pubkey);
|
||||
const projectToUpdate = this.projects.find(
|
||||
(p) => p.nostrPubKey === updatedMetadata.pubkey
|
||||
);
|
||||
if (projectToUpdate) {
|
||||
this.updateProjectMetadata(projectToUpdate, updatedMetadata.metadata);
|
||||
this.updateProjectMetadata(
|
||||
projectToUpdate,
|
||||
updatedMetadata.metadata
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -118,14 +145,15 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
||||
|
||||
private getProjectsWithoutMetadata(): string[] {
|
||||
return this.projects
|
||||
.filter(project => !project.displayName || !project.about)
|
||||
.map(project => project.nostrPubKey);
|
||||
.filter((project) => !project.displayName || !project.about)
|
||||
.map((project) => project.nostrPubKey);
|
||||
}
|
||||
|
||||
private async loadMetadataForProjects(pubkeys: string[]): Promise<void> {
|
||||
const metadataPromises = pubkeys.map(async (pubkey) => {
|
||||
// Check cache first
|
||||
const cachedMetadata = await this.indexedDBService.getUserMetadata(pubkey);
|
||||
const cachedMetadata =
|
||||
await this.indexedDBService.getUserMetadata(pubkey);
|
||||
if (cachedMetadata) {
|
||||
return { pubkey, metadata: cachedMetadata };
|
||||
}
|
||||
@@ -136,13 +164,15 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Filter out nulls (which represent pubkeys without cached metadata)
|
||||
const missingPubkeys = metadataResults
|
||||
.filter(result => result === null)
|
||||
.filter((result) => result === null)
|
||||
.map((_, index) => pubkeys[index]);
|
||||
|
||||
// Update projects that have cached metadata
|
||||
metadataResults.forEach(result => {
|
||||
metadataResults.forEach((result) => {
|
||||
if (result && result.metadata) {
|
||||
const project = this.projects.find(p => p.nostrPubKey === result.pubkey);
|
||||
const project = this.projects.find(
|
||||
(p) => p.nostrPubKey === result.pubkey
|
||||
);
|
||||
if (project) {
|
||||
this.updateProjectMetadata(project, result.metadata);
|
||||
}
|
||||
@@ -151,80 +181,103 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Fetch metadata for pubkeys that are not cached
|
||||
if (missingPubkeys.length > 0) {
|
||||
await this.metadataService.fetchMetadataForMultipleKeys(missingPubkeys)
|
||||
await this.metadataService
|
||||
.fetchMetadataForMultipleKeys(missingPubkeys)
|
||||
.then((metadataList: any[]) => {
|
||||
metadataList.forEach(metadata => {
|
||||
const project = this.projects.find(p => p.nostrPubKey === metadata.pubkey);
|
||||
metadataList.forEach((metadata) => {
|
||||
const project = this.projects.find(
|
||||
(p) => p.nostrPubKey === metadata.pubkey
|
||||
);
|
||||
if (project) {
|
||||
this.updateProjectMetadata(project, metadata);
|
||||
}
|
||||
});
|
||||
this.changeDetectorRef.detectChanges();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching metadata for projects:', error);
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Error fetching metadata for projects:',
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async loadProjects(): Promise<void> {
|
||||
if (this.loading || this.errorMessage === 'No more projects found') return;
|
||||
if (this.loading || this.errorMessage === 'No more projects found')
|
||||
return;
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.projectService.fetchProjects().then(async (projects: Project[]) => {
|
||||
if (projects.length === 0 && this.projects.length === 0) {
|
||||
this.errorMessage = 'No projects found';
|
||||
} else if (projects.length === 0) {
|
||||
this.errorMessage = 'No more projects found';
|
||||
} else {
|
||||
this.projects = [...this.projects, ...projects];
|
||||
this.filteredProjects = [...this.projects];
|
||||
this.projectService
|
||||
.fetchProjects()
|
||||
.then(async (projects: Project[]) => {
|
||||
if (projects.length === 0 && this.projects.length === 0) {
|
||||
this.errorMessage = 'No projects found';
|
||||
} else if (projects.length === 0) {
|
||||
this.errorMessage = 'No more projects found';
|
||||
} else {
|
||||
this.projects = [...this.projects, ...projects];
|
||||
this.filteredProjects = [...this.projects];
|
||||
|
||||
const pubkeys = projects.map(project => project.nostrPubKey);
|
||||
const pubkeys = projects.map(
|
||||
(project) => project.nostrPubKey
|
||||
);
|
||||
|
||||
await this.loadMetadataForProjects(pubkeys);
|
||||
await this.loadMetadataForProjects(pubkeys);
|
||||
|
||||
this.stateService.setProjects(this.projects);
|
||||
this.stateService.setProjects(this.projects);
|
||||
|
||||
this.projects.forEach(project => this.subscribeToProjectMetadata(project));
|
||||
}
|
||||
this.loading = false;
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}).catch((error: any) => {
|
||||
console.error('Error fetching projects:', error);
|
||||
this.errorMessage = 'Error fetching projects. Please try again later.';
|
||||
this.loading = false;
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
this.projects.forEach((project) =>
|
||||
this.subscribeToProjectMetadata(project)
|
||||
);
|
||||
}
|
||||
this.loading = false;
|
||||
this.changeDetectorRef.detectChanges();
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Error fetching projects:', error);
|
||||
this.errorMessage =
|
||||
'Error fetching projects. Please try again later.';
|
||||
this.loading = false;
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
async loadMetadataForProject(project: Project): Promise<void> {
|
||||
try {
|
||||
const metadata = await this.metadataService.fetchMetadataWithCache(project.nostrPubKey);
|
||||
const metadata = await this.metadataService.fetchMetadataWithCache(
|
||||
project.nostrPubKey
|
||||
);
|
||||
if (metadata) {
|
||||
this.updateProjectMetadata(project, metadata);
|
||||
} else {
|
||||
console.warn(`No metadata found for project ${project.nostrPubKey}`);
|
||||
console.warn(
|
||||
`No metadata found for project ${project.nostrPubKey}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching metadata for project ${project.nostrPubKey}:`, error);
|
||||
console.error(
|
||||
`Error fetching metadata for project ${project.nostrPubKey}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateProjectMetadata(project: Project, metadata: any): void {
|
||||
|
||||
const updatedProject: Project = {
|
||||
...project,
|
||||
displayName: metadata.name || '',
|
||||
about: metadata.about ? metadata.about.replace(/<\/?[^>]+(>|$)/g, '') : '',
|
||||
about: metadata.about
|
||||
? metadata.about.replace(/<\/?[^>]+(>|$)/g, '')
|
||||
: '',
|
||||
picture: metadata.picture || '',
|
||||
banner: metadata.banner || ''
|
||||
banner: metadata.banner || '',
|
||||
};
|
||||
|
||||
const index = this.projects.findIndex(p => p.projectIdentifier === project.projectIdentifier);
|
||||
const index = this.projects.findIndex(
|
||||
(p) => p.projectIdentifier === project.projectIdentifier
|
||||
);
|
||||
if (index !== -1) {
|
||||
this.projects[index] = updatedProject;
|
||||
this.projects = [...this.projects];
|
||||
@@ -235,11 +288,18 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
subscribeToProjectMetadata(project: Project): void {
|
||||
this.metadataService.getMetadataStream()
|
||||
this.metadataService
|
||||
.getMetadataStream()
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((updatedMetadata: any) => {
|
||||
if (updatedMetadata && updatedMetadata.pubkey === project.nostrPubKey) {
|
||||
this.updateProjectMetadata(project, updatedMetadata.metadata);
|
||||
if (
|
||||
updatedMetadata &&
|
||||
updatedMetadata.pubkey === project.nostrPubKey
|
||||
) {
|
||||
this.updateProjectMetadata(
|
||||
project,
|
||||
updatedMetadata.metadata
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -247,7 +307,8 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
||||
goToProjectDetails(project: Project): void {
|
||||
this.loading = true;
|
||||
|
||||
this.projectService.fetchAndSaveProjectStats(project.projectIdentifier)
|
||||
this.projectService
|
||||
.fetchAndSaveProjectStats(project.projectIdentifier)
|
||||
.then((stats) => {
|
||||
if (stats) {
|
||||
this.navigateToProfile(project.nostrPubKey);
|
||||
@@ -278,17 +339,30 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
||||
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
|
||||
this.filteredProjects = this.projects.filter(project => {
|
||||
this.filteredProjects = this.projects.filter((project) => {
|
||||
return (
|
||||
(project.displayName && project.displayName.toLowerCase().includes(lowerCaseQuery)) ||
|
||||
(project.about && project.about.toLowerCase().includes(lowerCaseQuery)) ||
|
||||
(project.displayName && project.displayName.toLowerCase().includes(lowerCaseQuery)) ||
|
||||
(project.nostrPubKey && project.nostrPubKey.toLowerCase().includes(lowerCaseQuery)) ||
|
||||
(project.projectIdentifier && project.projectIdentifier.toLowerCase().includes(lowerCaseQuery))
|
||||
(project.displayName &&
|
||||
project.displayName
|
||||
.toLowerCase()
|
||||
.includes(lowerCaseQuery)) ||
|
||||
(project.about &&
|
||||
project.about.toLowerCase().includes(lowerCaseQuery)) ||
|
||||
(project.displayName &&
|
||||
project.displayName
|
||||
.toLowerCase()
|
||||
.includes(lowerCaseQuery)) ||
|
||||
(project.nostrPubKey &&
|
||||
project.nostrPubKey
|
||||
.toLowerCase()
|
||||
.includes(lowerCaseQuery)) ||
|
||||
(project.projectIdentifier &&
|
||||
project.projectIdentifier
|
||||
.toLowerCase()
|
||||
.includes(lowerCaseQuery))
|
||||
);
|
||||
});
|
||||
|
||||
this.showCloseSearchButton = this.projects.length > 0 ;
|
||||
this.showCloseSearchButton = this.projects.length > 0;
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
@@ -299,9 +373,7 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
||||
this.showCloseSearchButton = false;
|
||||
}
|
||||
|
||||
toggleCompleted(event: any): void {
|
||||
|
||||
}
|
||||
toggleCompleted(event: any): void {}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._unsubscribeAll.next(null);
|
||||
@@ -319,7 +391,9 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
||||
if (url && typeof url === 'string' && this.isImageUrl(url)) {
|
||||
return this.sanitizer.bypassSecurityTrustUrl(url);
|
||||
} else {
|
||||
const defaultImage = isBanner ? '/images/pages/profile/cover.jpg' : 'images/avatars/avatar-placeholder.png';
|
||||
const defaultImage = isBanner
|
||||
? '/images/pages/profile/cover.jpg'
|
||||
: 'images/avatars/avatar-placeholder.png';
|
||||
return this.sanitizer.bypassSecurityTrustUrl(defaultImage);
|
||||
}
|
||||
}
|
||||
@@ -330,26 +404,34 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
||||
|
||||
async openChat(publicKey: string): Promise<void> {
|
||||
try {
|
||||
const metadata = await this.metadataService.fetchMetadataWithCache(publicKey);
|
||||
const metadata =
|
||||
await this.metadataService.fetchMetadataWithCache(publicKey);
|
||||
|
||||
if (metadata) {
|
||||
const contact: Contact = {
|
||||
pubKey: publicKey,
|
||||
name: metadata.name || 'Unknown',
|
||||
picture: metadata.picture || '/images/avatars/avatar-placeholder.png',
|
||||
picture:
|
||||
metadata.picture ||
|
||||
'/images/avatars/avatar-placeholder.png',
|
||||
about: metadata.about || '',
|
||||
displayName: metadata.displayName || metadata.name || 'Unknown',
|
||||
displayName:
|
||||
metadata.displayName || metadata.name || 'Unknown',
|
||||
};
|
||||
|
||||
this._chatService.getChatById(contact.pubKey, contact).subscribe((chat) => {
|
||||
this.router.navigate(['/chat', contact.pubKey]);
|
||||
});
|
||||
this._chatService
|
||||
.getChatById(contact.pubKey, contact)
|
||||
.subscribe((chat) => {
|
||||
this.router.navigate(['/chat', contact.pubKey]);
|
||||
});
|
||||
} else {
|
||||
console.error('No metadata found for the public key:', publicKey);
|
||||
console.error(
|
||||
'No metadata found for the public key:',
|
||||
publicKey
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error opening chat:', error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ExploreComponent } from 'app/components/explore/explore.component';
|
||||
|
||||
export default [
|
||||
{
|
||||
path : '',
|
||||
path: '',
|
||||
component: ExploreComponent,
|
||||
},
|
||||
] as Routes;
|
||||
|
||||
@@ -3,10 +3,18 @@
|
||||
<div class="prose prose-sm mx-auto max-w-none">
|
||||
<h1>Angor Hub</h1>
|
||||
<p>
|
||||
Angor Hub is a Nostr client that is customized around the Angor protocol, a decentralized crowdfunding platform. Leveraging the power of Nostr the platform allows you to explore projects that are raising funds using Angor, engage with investors, and connect directly with founders.
|
||||
Angor Hub is a Nostr client that is customized around the Angor
|
||||
protocol, a decentralized crowdfunding platform. Leveraging the
|
||||
power of Nostr the platform allows you to explore projects that
|
||||
are raising funds using Angor, engage with investors, and
|
||||
connect directly with founders.
|
||||
</p>
|
||||
<p>
|
||||
Whether you're an investor looking for the next big opportunity or a project founder seeking funding, Angor Hub offers the tools you need to succeed. From project pages, secure messaging to group channels, Angor Hub ensures seamless interaction within a decentralized Nostr.
|
||||
Whether you're an investor looking for the next big opportunity
|
||||
or a project founder seeking funding, Angor Hub offers the tools
|
||||
you need to succeed. From project pages, secure messaging to
|
||||
group channels, Angor Hub ensures seamless interaction within a
|
||||
decentralized Nostr.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { RouterLink } from '@angular/router';
|
||||
templateUrl: './home.component.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: true,
|
||||
imports: [MatButtonModule, RouterLink, MatIconModule],
|
||||
imports: [MatButtonModule, RouterLink, MatIconModule ],
|
||||
})
|
||||
export class LandingHomeComponent {
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
51
src/app/components/profile/profile.component.scss
Normal file
51
src/app/components/profile/profile.component.scss
Normal file
@@ -0,0 +1,51 @@
|
||||
.emoji-picker-container-global {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 9999;
|
||||
width: 350px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.heart-beat {
|
||||
animation: heartBeatAnimation 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes heartBeatAnimation {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
30% {
|
||||
transform: scale(2);
|
||||
}
|
||||
60% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
|
||||
.spinner {
|
||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||
border-left-color: #009fb5;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,63 @@
|
||||
import { AngorCardComponent } from '@angor/components/card';
|
||||
import { AngorConfigService } from '@angor/services/config';
|
||||
import { AngorConfirmationService } from '@angor/services/confirmation';
|
||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { CommonModule, NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ViewEncapsulation,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
OnDestroy
|
||||
|
||||
ViewChild,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { AngorCardComponent } from '@angor/components/card';
|
||||
import { SignerService } from 'app/services/signer.service';
|
||||
import { MetadataService } from 'app/services/metadata.service';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { IndexedDBService } from 'app/services/indexed-db.service';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { SocialService } from 'app/services/social.service';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { LightningInvoice, LightningResponse } from 'app/types/post';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSlideToggle } from '@angular/material/slide-toggle';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { LightningService } from 'app/services/lightning.service';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { PickerComponent } from '@ctrl/ngx-emoji-mart';
|
||||
import { bech32 } from '@scure/base';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { QRCodeModule } from 'angularx-qrcode';
|
||||
import { Clipboard } from '@angular/cdk/clipboard';
|
||||
import { SendDialogComponent } from './zap/send-dialog/send-dialog.component';
|
||||
import { PaginatedEventService } from 'app/services/event.service';
|
||||
import { IndexedDBService } from 'app/services/indexed-db.service';
|
||||
import { LightningService } from 'app/services/lightning.service';
|
||||
import { MetadataService } from 'app/services/metadata.service';
|
||||
import { SignerService } from 'app/services/signer.service';
|
||||
import { SocialService } from 'app/services/social.service';
|
||||
import { SafeUrlPipe } from 'app/shared/pipes/safe-url.pipe';
|
||||
import { Paginator } from 'app/shared/utils';
|
||||
import { LightningInvoice, LightningResponse, Post } from 'app/types/post';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import { NostrEvent } from 'nostr-tools';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { EventListComponent } from '../event-list/event-list.component';
|
||||
import { ReceiveDialogComponent } from './zap/receive-dialog/receive-dialog.component';
|
||||
import { SendDialogComponent } from './zap/send-dialog/send-dialog.component';
|
||||
|
||||
interface Chip {
|
||||
color?: string;
|
||||
selected?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'profile',
|
||||
templateUrl: './profile.component.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
styleUrls: ['./profile.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: true,
|
||||
imports: [
|
||||
RouterLink,
|
||||
@@ -56,28 +74,54 @@ import { ReceiveDialogComponent } from './zap/receive-dialog/receive-dialog.comp
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
QRCodeModule,
|
||||
PickerComponent,
|
||||
MatSlideToggle,
|
||||
|
||||
SafeUrlPipe,
|
||||
MatProgressSpinnerModule,
|
||||
InfiniteScrollModule,
|
||||
EventListComponent,
|
||||
],
|
||||
})
|
||||
export class ProfileComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('eventInput', { static: false }) eventInput: ElementRef;
|
||||
@ViewChild('commentInput') commentInput: ElementRef;
|
||||
|
||||
darkMode: boolean = false;
|
||||
isLoading: boolean = true;
|
||||
errorMessage: string | null = null;
|
||||
metadata: any;
|
||||
currentUserMetadata: any;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
private userPubKey;
|
||||
private routePubKey;
|
||||
public currentUserPubKey: string;
|
||||
public routePubKey;
|
||||
followers: any[] = [];
|
||||
following: any[] = [];
|
||||
allPublicKeys: string[] = [];
|
||||
suggestions: { pubkey: string, metadata: any }[] = [];
|
||||
suggestions: { pubkey: string; metadata: any }[] = [];
|
||||
isCurrentUserProfile: Boolean = false;
|
||||
isFollowing = false;
|
||||
|
||||
showEmojiPicker = false;
|
||||
showCommentEmojiPicker = false;
|
||||
lightningResponse: LightningResponse | null = null;
|
||||
lightningInvoice: LightningInvoice | null = null;
|
||||
sats: string;
|
||||
paymentInvoice: string = '';
|
||||
invoiceAmount: string = '?';
|
||||
isLiked = false;
|
||||
isPreview = false;
|
||||
posts: Post[] = [];
|
||||
likes: any[] = [];
|
||||
|
||||
paginator: Paginator;
|
||||
|
||||
myLikes: NostrEvent[] = [];
|
||||
myLikedNoteIds: string[] = [];
|
||||
|
||||
isLoadingPosts: boolean = true;
|
||||
noEventsMessage: string = '';
|
||||
loadingTimeout: any;
|
||||
|
||||
|
||||
constructor(
|
||||
@@ -90,52 +134,81 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
private _socialService: SocialService,
|
||||
private snackBar: MatSnackBar,
|
||||
private lightning: LightningService,
|
||||
private _dialog: MatDialog // Add MatDialog here
|
||||
private _dialog: MatDialog,
|
||||
private _angorConfigService: AngorConfigService,
|
||||
private _angorConfirmationService: AngorConfirmationService,
|
||||
private eventService: PaginatedEventService
|
||||
) {
|
||||
let baseTimeDiff = 12000;
|
||||
let since = 0;
|
||||
|
||||
) { }
|
||||
this.paginator = new Paginator(0, since, (baseTimeDiff = baseTimeDiff));
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this._angorConfigService.config$.subscribe((config) => {
|
||||
if (config.scheme === 'auto') {
|
||||
this.detectSystemTheme();
|
||||
} else {
|
||||
this.darkMode = config.scheme === 'dark';
|
||||
}
|
||||
});
|
||||
this._route.paramMap.subscribe((params) => {
|
||||
const routePubKey = params.get('pubkey');
|
||||
this.routePubKey = routePubKey;
|
||||
const userPubKey = this._signerService.getPublicKey();
|
||||
this.isCurrentUserProfile = routePubKey === userPubKey;
|
||||
const pubKeyToLoad = routePubKey || userPubKey;
|
||||
this.loadProfile(pubKeyToLoad);
|
||||
const currentUserPubKey = this._signerService.getPublicKey();
|
||||
this.currentUserPubKey = currentUserPubKey;
|
||||
if (routePubKey || currentUserPubKey) {
|
||||
this.isCurrentUserProfile = routePubKey === currentUserPubKey;
|
||||
}
|
||||
|
||||
this.routePubKey = routePubKey || currentUserPubKey;
|
||||
this.loadProfile(this.routePubKey);
|
||||
if (!routePubKey) {
|
||||
this.isCurrentUserProfile = true;
|
||||
}
|
||||
this.loadCurrentUserProfile();
|
||||
|
||||
|
||||
});
|
||||
|
||||
this._indexedDBService.getMetadataStream()
|
||||
this._indexedDBService
|
||||
.getMetadataStream()
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((updatedMetadata) => {
|
||||
if (updatedMetadata && updatedMetadata.pubkey === this.userPubKey) {
|
||||
if (
|
||||
updatedMetadata &&
|
||||
updatedMetadata.pubkey === this.currentUserPubKey
|
||||
) {
|
||||
this.currentUserMetadata = updatedMetadata.metadata;
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
});
|
||||
if (this.routePubKey) {
|
||||
this._indexedDBService.getMetadataStream()
|
||||
this._indexedDBService
|
||||
.getMetadataStream()
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((updatedMetadata) => {
|
||||
if (updatedMetadata && updatedMetadata.pubkey === this.routePubKey) {
|
||||
if (
|
||||
updatedMetadata &&
|
||||
updatedMetadata.pubkey === this.routePubKey
|
||||
) {
|
||||
this.metadata = updatedMetadata.metadata;
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this._socialService.getFollowersObservable()
|
||||
this._socialService
|
||||
.getFollowersObservable()
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((event) => {
|
||||
this.followers.push(event.pubkey);
|
||||
this._changeDetectorRef.detectChanges();
|
||||
});
|
||||
|
||||
this._socialService.getFollowingObservable()
|
||||
this._socialService
|
||||
.getFollowingObservable()
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((event) => {
|
||||
const tags = event.tags.filter((tag) => tag[0] === 'p');
|
||||
@@ -144,8 +217,6 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
this._changeDetectorRef.detectChanges();
|
||||
});
|
||||
|
||||
this.updateSuggestionList();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -153,6 +224,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
|
||||
async loadProfile(publicKey: string): Promise<void> {
|
||||
this.isLoading = true;
|
||||
this.errorMessage = null;
|
||||
@@ -160,6 +232,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
this.metadata = null;
|
||||
this.followers = [];
|
||||
this.following = [];
|
||||
|
||||
this._changeDetectorRef.detectChanges();
|
||||
|
||||
if (!publicKey) {
|
||||
@@ -170,27 +243,20 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const userMetadata = await this._metadataService.fetchMetadataWithCache(publicKey);
|
||||
if (userMetadata) {
|
||||
this.metadata = userMetadata;
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
await this._socialService.getFollowers(publicKey);
|
||||
|
||||
this.followers = await this._socialService.getFollowers(publicKey);
|
||||
const currentUserPubKey = this._signerService.getPublicKey();
|
||||
this.isFollowing = this.followers.includes(currentUserPubKey);
|
||||
|
||||
await this._socialService.getFollowing(publicKey);
|
||||
|
||||
this._metadataService.getMetadataStream()
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((updatedMetadata) => {
|
||||
if (updatedMetadata && updatedMetadata.pubkey === publicKey) {
|
||||
this.metadata = updatedMetadata;
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
});
|
||||
|
||||
this.following = await this._socialService.getFollowing(publicKey);
|
||||
this._changeDetectorRef.detectChanges();
|
||||
} catch (error) {
|
||||
console.error('Failed to load profile data:', error);
|
||||
this.errorMessage = 'Failed to load profile data. Please try again later.';
|
||||
@@ -201,25 +267,23 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async loadCurrentUserProfile(): Promise<void> {
|
||||
try {
|
||||
this.currentUserMetadata = null;
|
||||
this.currentUserPubKey = this._signerService.getPublicKey();
|
||||
|
||||
|
||||
const currentUserMetadata = await this._metadataService.fetchMetadataWithCache(
|
||||
this.currentUserPubKey
|
||||
);
|
||||
|
||||
this.userPubKey = this._signerService.getPublicKey();
|
||||
const currentUserMetadata = await this._metadataService.fetchMetadataWithCache(this.userPubKey);
|
||||
if (currentUserMetadata) {
|
||||
this.currentUserMetadata = currentUserMetadata;
|
||||
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
this._metadataService.getMetadataStream()
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((updatedMetadata) => {
|
||||
if (updatedMetadata && updatedMetadata.pubkey === this.userPubKey) {
|
||||
this.currentUserMetadata = updatedMetadata;
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this._changeDetectorRef.detectChanges();
|
||||
} catch (error) {
|
||||
console.error('Failed to load profile data:', error);
|
||||
this.errorMessage = 'Failed to load profile data. Please try again later.';
|
||||
@@ -229,15 +293,8 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private updateSuggestionList(): void {
|
||||
this._indexedDBService.getSuggestionUsers().then((suggestions) => {
|
||||
this.suggestions = suggestions;
|
||||
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}).catch((error) => {
|
||||
console.error('Error updating suggestion list:', error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
getSafeUrl(url: string): SafeUrl {
|
||||
return this._sanitizer.bypassSecurityTrustUrl(url);
|
||||
@@ -246,7 +303,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
async toggleFollow(): Promise<void> {
|
||||
try {
|
||||
const userPubKey = this._signerService.getPublicKey();
|
||||
const routePubKey = this.routePubKey || this.userPubKey;
|
||||
const routePubKey = this.routePubKey || this.currentUserPubKey;
|
||||
|
||||
if (!routePubKey || !userPubKey) {
|
||||
console.error('Public key missing. Unable to toggle follow.');
|
||||
@@ -257,7 +314,9 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
await this._socialService.unfollow(routePubKey);
|
||||
console.log(`Unfollowed ${routePubKey}`);
|
||||
|
||||
this.followers = this.followers.filter(pubkey => pubkey !== userPubKey);
|
||||
this.followers = this.followers.filter(
|
||||
(pubkey) => pubkey !== userPubKey
|
||||
);
|
||||
} else {
|
||||
await this._socialService.follow(routePubKey);
|
||||
console.log(`Followed ${routePubKey}`);
|
||||
@@ -268,19 +327,15 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
this.isFollowing = !this.isFollowing;
|
||||
|
||||
this._changeDetectorRef.detectChanges();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle follow:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, { duration: 1300 });
|
||||
}
|
||||
|
||||
|
||||
|
||||
getLightningInfo() {
|
||||
let lightningAddress = '';
|
||||
if (this.metadata?.lud06) {
|
||||
@@ -292,19 +347,29 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
const data = new Uint8Array(bech32.fromWords(words));
|
||||
lightningAddress = new TextDecoder().decode(Uint8Array.from(data));
|
||||
} else if (this.metadata?.lud16) {
|
||||
lightningAddress = this.lightning.getLightningAddress(this.metadata.lud16);
|
||||
lightningAddress = this.lightning.getLightningAddress(
|
||||
this.metadata.lud16
|
||||
);
|
||||
}
|
||||
if (lightningAddress !== '') {
|
||||
this.lightning.getLightning(lightningAddress).subscribe((response) => {
|
||||
this.lightningResponse = response;
|
||||
if (this.lightningResponse.status === 'Failed') {
|
||||
this.openSnackBar('Failed to lookup lightning address', 'dismiss');
|
||||
} else if (this.lightningResponse.callback) {
|
||||
this.openZapDialog(); // Open dialog when callback is available
|
||||
} else {
|
||||
this.openSnackBar("couldn't find user's lightning address", 'dismiss');
|
||||
}
|
||||
});
|
||||
this.lightning
|
||||
.getLightning(lightningAddress)
|
||||
.subscribe((response) => {
|
||||
this.lightningResponse = response;
|
||||
if (this.lightningResponse.status === 'Failed') {
|
||||
this.openSnackBar(
|
||||
'Failed to lookup lightning address',
|
||||
'dismiss'
|
||||
);
|
||||
} else if (this.lightningResponse.callback) {
|
||||
this.openZapDialog();
|
||||
} else {
|
||||
this.openSnackBar(
|
||||
"couldn't find user's lightning address",
|
||||
'dismiss'
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.openSnackBar('No lightning address found', 'dismiss');
|
||||
}
|
||||
@@ -322,7 +387,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
this._dialog.open(SendDialogComponent, {
|
||||
width: '405px',
|
||||
maxHeight: '90vh',
|
||||
data: this.metadata
|
||||
data: this.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -330,8 +395,95 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
this._dialog.open(ReceiveDialogComponent, {
|
||||
width: '405px',
|
||||
maxHeight: '90vh',
|
||||
data: this.metadata
|
||||
data: this.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
toggleLike() {
|
||||
this.isLiked = !this.isLiked;
|
||||
|
||||
if (this.isLiked) {
|
||||
setTimeout(() => {
|
||||
this.isLiked = false;
|
||||
this.isLiked = true;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
addEmoji(event: any) {
|
||||
this.eventInput.nativeElement.value += event.emoji.native;
|
||||
this.showEmojiPicker = false;
|
||||
}
|
||||
|
||||
toggleEmojiPicker() {
|
||||
this.showCommentEmojiPicker = false;
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
}
|
||||
|
||||
addEmojiTocomment(event: any) {
|
||||
this.commentInput.nativeElement.value += event.emoji.native;
|
||||
this.showCommentEmojiPicker = false;
|
||||
}
|
||||
|
||||
toggleCommentEmojiPicker() {
|
||||
this.showEmojiPicker = false;
|
||||
this.showCommentEmojiPicker = !this.showCommentEmojiPicker;
|
||||
}
|
||||
|
||||
detectSystemTheme() {
|
||||
const darkSchemeMedia = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
);
|
||||
this.darkMode = darkSchemeMedia.matches;
|
||||
|
||||
darkSchemeMedia.addEventListener('change', (event) => {
|
||||
this.darkMode = event.matches;
|
||||
});
|
||||
}
|
||||
|
||||
openConfirmationDialog(): void {
|
||||
const dialogRef = this._angorConfirmationService.open({
|
||||
title: 'Share Event',
|
||||
message:
|
||||
'Are you sure you want to share this event on your profile? <span class="font-medium">This action is permanent and cannot be undone.</span>',
|
||||
icon: {
|
||||
show: true,
|
||||
name: 'heroicons_solid:share',
|
||||
color: 'primary',
|
||||
},
|
||||
actions: {
|
||||
confirm: {
|
||||
show: true,
|
||||
label: 'Yes, Share',
|
||||
color: 'primary',
|
||||
},
|
||||
cancel: {
|
||||
show: true,
|
||||
label: 'Cancel',
|
||||
},
|
||||
},
|
||||
dismissible: true,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
console.log(result);
|
||||
});
|
||||
}
|
||||
|
||||
togglePreview() {
|
||||
this.isPreview = !this.isPreview;
|
||||
}
|
||||
|
||||
sendEvent() {
|
||||
if (this.eventInput.nativeElement.value != '') {
|
||||
this.eventService
|
||||
.sendTextEvent(this.eventInput.nativeElement.value)
|
||||
.then(() => {
|
||||
this._changeDetectorRef.markForCheck();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to send Event:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
<div class="absolute right-0 top-0 pr-4 pt-4">
|
||||
<button mat-icon-button [matDialogClose]="undefined">
|
||||
<mat-icon
|
||||
class="text-secondary"
|
||||
[svgIcon]="'heroicons_outline:x-mark'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<h2>⚡ Receive Zap</h2>
|
||||
<mat-dialog-content *ngIf="!displayQRCode">
|
||||
<div class="preset-buttons">
|
||||
<button mat-mini-fab color="primary" *ngFor="let button of zapButtons" (click)="invoiceAmount = button.value">
|
||||
<button
|
||||
mat-mini-fab
|
||||
color="primary"
|
||||
*ngFor="let button of zapButtons"
|
||||
(click)="invoiceAmount = button.value"
|
||||
>
|
||||
<mat-icon>{{ button.icon }}</mat-icon>
|
||||
<span>{{ button.label }}</span>
|
||||
</button>
|
||||
@@ -9,12 +22,14 @@
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="outline" class="sats-input">
|
||||
<mat-label>Zap Amount</mat-label>
|
||||
<input matInput [(ngModel)]="invoiceAmount" placeholder="e.g., 100" type="number" />
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="invoiceAmount"
|
||||
placeholder="e.g., 100"
|
||||
type="number"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-icon-button color="warn" (click)="closeDialog()" [matTooltip]="'Close'">
|
||||
<mat-icon [svgIcon]="'heroicons_solid:x-mark'"></mat-icon>
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="generateInvoice()">
|
||||
Generate Invoice
|
||||
</button>
|
||||
@@ -25,14 +40,22 @@
|
||||
<div *ngIf="displayQRCode" class="qrcode">
|
||||
<span>Scan with phone to pay ({{ invoiceAmount }} sats)</span>
|
||||
<mat-divider></mat-divider>
|
||||
<qrcode [qrdata]="lightningInvoice" [matTooltip]="'Lightning Invoice'" class="qrcode-image" [errorCorrectionLevel]="'M'"></qrcode>
|
||||
<qrcode
|
||||
[qrdata]="lightningInvoice"
|
||||
[matTooltip]="'Lightning Invoice'"
|
||||
class="qrcode-image"
|
||||
[errorCorrectionLevel]="'M'"
|
||||
></qrcode>
|
||||
|
||||
<mat-dialog-actions align="center">
|
||||
<button mat-icon-button color="warn" (click)="closeDialog()" [matTooltip]="'Close'">
|
||||
<mat-icon [svgIcon]="'heroicons_solid:x-mark'"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="copyInvoice()" [matTooltip]="'Copy Invoice'">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:clipboard-document'"></mat-icon>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="copyInvoice()"
|
||||
[matTooltip]="'Copy Invoice'"
|
||||
>
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_outline:clipboard-document'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
gap: 15px;
|
||||
justify-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.preset-buttons button {
|
||||
.preset-buttons button {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
width: 70px;
|
||||
@@ -16,24 +16,23 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-height: 60px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sats-input {
|
||||
.sats-input {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.lightning-buttons {
|
||||
.lightning-buttons {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.qrcode {
|
||||
.qrcode {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-image {
|
||||
.qrcode-image {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Clipboard } from '@angular/cdk/clipboard';
|
||||
import { MatDialogActions, MatDialogContent, MatDialogRef } from '@angular/material/dialog';
|
||||
import { webln } from '@getalby/sdk';
|
||||
import { NgClass, CommonModule } from '@angular/common';
|
||||
import { CommonModule, NgClass } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOption } from '@angular/material/core';
|
||||
import {
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatDivider } from '@angular/material/divider';
|
||||
import { MatLabel, MatFormField, MatFormFieldModule } from '@angular/material/form-field';
|
||||
import {
|
||||
MatFormField,
|
||||
MatFormFieldModule,
|
||||
MatLabel,
|
||||
} from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { webln } from '@getalby/sdk';
|
||||
import { QRCodeModule } from 'angularx-qrcode';
|
||||
import { SettingsIndexerComponent } from 'app/components/settings/indexer/indexer.component';
|
||||
import { SettingsNetworkComponent } from 'app/components/settings/network/network.component';
|
||||
@@ -23,112 +32,116 @@ import { SettingsRelayComponent } from 'app/components/settings/relay/relay.comp
|
||||
import { SettingsSecurityComponent } from 'app/components/settings/security/security.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-receive-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatSidenavModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
NgClass,
|
||||
SettingsProfileComponent,
|
||||
SettingsSecurityComponent,
|
||||
SettingsNotificationsComponent,
|
||||
SettingsRelayComponent,
|
||||
SettingsNetworkComponent,
|
||||
SettingsIndexerComponent,
|
||||
FormsModule,
|
||||
MatOption,
|
||||
MatLabel,
|
||||
MatFormField,
|
||||
ReactiveFormsModule,
|
||||
CommonModule,
|
||||
MatSelectModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
QRCodeModule,
|
||||
MatDivider,
|
||||
MatTooltip
|
||||
],
|
||||
templateUrl: './receive-dialog.component.html',
|
||||
styleUrls: ['./receive-dialog.component.scss']
|
||||
selector: 'app-receive-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatSidenavModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
NgClass,
|
||||
SettingsProfileComponent,
|
||||
SettingsSecurityComponent,
|
||||
SettingsNotificationsComponent,
|
||||
SettingsRelayComponent,
|
||||
SettingsNetworkComponent,
|
||||
SettingsIndexerComponent,
|
||||
FormsModule,
|
||||
MatOption,
|
||||
MatLabel,
|
||||
MatFormField,
|
||||
ReactiveFormsModule,
|
||||
CommonModule,
|
||||
MatSelectModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
QRCodeModule,
|
||||
MatDivider,
|
||||
MatTooltip,
|
||||
MatDialogClose,
|
||||
],
|
||||
templateUrl: './receive-dialog.component.html',
|
||||
styleUrls: ['./receive-dialog.component.scss'],
|
||||
})
|
||||
export class ReceiveDialogComponent {
|
||||
invoiceAmount: string = '';
|
||||
lightningInvoice: string = '';
|
||||
displayQRCode: boolean = false;
|
||||
nwc: any;
|
||||
invoiceAmount: string = '';
|
||||
lightningInvoice: string = '';
|
||||
displayQRCode: boolean = false;
|
||||
nwc: any;
|
||||
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<ReceiveDialogComponent>,
|
||||
private snackBar: MatSnackBar,
|
||||
private clipboard: Clipboard
|
||||
) {}
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<ReceiveDialogComponent>,
|
||||
private snackBar: MatSnackBar,
|
||||
private clipboard: Clipboard
|
||||
) {}
|
||||
|
||||
zapButtons = [
|
||||
{ icon: 'thumb_up', label: '50', value: 50 },
|
||||
{ icon: 'favorite', label: '100', value: 100 },
|
||||
{ icon: 'emoji_emotions', label: '500', value: 500 },
|
||||
{ icon: 'star', label: '1k', value: 1000 },
|
||||
{ icon: 'celebration', label: '5k', value: 5000 },
|
||||
{ icon: 'rocket', label: '10k', value: 10000 },
|
||||
{ icon: 'local_fire_department', label: '100k', value: 100000 },
|
||||
{ icon: 'flash_on', label: '500k', value: 500000 },
|
||||
{ icon: 'diamond', label: '1M', value: 1000000 }
|
||||
];
|
||||
zapButtons = [
|
||||
{ icon: 'thumb_up', label: '50', value: 50 },
|
||||
{ icon: 'favorite', label: '100', value: 100 },
|
||||
{ icon: 'emoji_emotions', label: '500', value: 500 },
|
||||
{ icon: 'star', label: '1k', value: 1000 },
|
||||
{ icon: 'celebration', label: '5k', value: 5000 },
|
||||
{ icon: 'rocket', label: '10k', value: 10000 },
|
||||
{ icon: 'local_fire_department', label: '100k', value: 100000 },
|
||||
{ icon: 'flash_on', label: '500k', value: 500000 },
|
||||
{ icon: 'diamond', label: '1M', value: 1000000 },
|
||||
];
|
||||
|
||||
async generateInvoice(): Promise<void> {
|
||||
if (!this.invoiceAmount || Number(this.invoiceAmount) <= 0) {
|
||||
this.openSnackBar('Please enter a valid amount', 'dismiss');
|
||||
return;
|
||||
async generateInvoice(): Promise<void> {
|
||||
if (!this.invoiceAmount || Number(this.invoiceAmount) <= 0) {
|
||||
this.openSnackBar('Please enter a valid amount', 'dismiss');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.nwc = new webln.NostrWebLNProvider({
|
||||
nostrWalletConnectUrl: await this.loadNWCUrl(),
|
||||
});
|
||||
await this.nwc.enable();
|
||||
|
||||
const invoiceResponse = await this.nwc.makeInvoice({
|
||||
amount: Number(this.invoiceAmount),
|
||||
});
|
||||
this.lightningInvoice = invoiceResponse.paymentRequest;
|
||||
|
||||
this.showQRCode();
|
||||
} catch (error) {
|
||||
console.error('Error generating invoice:', error);
|
||||
this.openSnackBar('Failed to generate invoice', 'dismiss');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
this.nwc = new webln.NostrWebLNProvider({ nostrWalletConnectUrl: await this.loadNWCUrl() });
|
||||
await this.nwc.enable();
|
||||
|
||||
const invoiceResponse = await this.nwc.makeInvoice({ amount: Number(this.invoiceAmount) });
|
||||
this.lightningInvoice = invoiceResponse.paymentRequest;
|
||||
|
||||
this.showQRCode();
|
||||
} catch (error) {
|
||||
console.error('Error generating invoice:', error);
|
||||
this.openSnackBar('Failed to generate invoice', 'dismiss');
|
||||
async loadNWCUrl(): Promise<string> {
|
||||
try {
|
||||
const nwc = webln.NostrWebLNProvider.withNewSecret();
|
||||
await nwc.initNWC({ name: 'Angor Hub' });
|
||||
return nwc.getNostrWalletConnectUrl();
|
||||
} catch (error) {
|
||||
console.error('Error initializing NWC:', error);
|
||||
throw new Error('Failed to initialize NWC provider');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadNWCUrl(): Promise<string> {
|
||||
try {
|
||||
const nwc = webln.NostrWebLNProvider.withNewSecret();
|
||||
await nwc.initNWC({ name: 'Angor Hub' });
|
||||
return nwc.getNostrWalletConnectUrl();
|
||||
} catch (error) {
|
||||
console.error('Error initializing NWC:', error);
|
||||
throw new Error('Failed to initialize NWC provider');
|
||||
showQRCode(): void {
|
||||
this.displayQRCode = !this.displayQRCode;
|
||||
}
|
||||
}
|
||||
|
||||
showQRCode(): void {
|
||||
this.displayQRCode = !this.displayQRCode;
|
||||
}
|
||||
|
||||
copyInvoice(): void {
|
||||
if (this.lightningInvoice) {
|
||||
this.clipboard.copy(this.lightningInvoice);
|
||||
this.openSnackBar('Invoice copied', 'dismiss');
|
||||
} else {
|
||||
this.openSnackBar('No invoice available to copy', 'dismiss');
|
||||
copyInvoice(): void {
|
||||
if (this.lightningInvoice) {
|
||||
this.clipboard.copy(this.lightningInvoice);
|
||||
this.openSnackBar('Invoice copied', 'dismiss');
|
||||
} else {
|
||||
this.openSnackBar('No invoice available to copy', 'dismiss');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openSnackBar(message: string, action: string): void {
|
||||
this.snackBar.open(message, action, { duration: 1300 });
|
||||
}
|
||||
openSnackBar(message: string, action: string): void {
|
||||
this.snackBar.open(message, action, { duration: 1300 });
|
||||
}
|
||||
|
||||
closeDialog(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
closeDialog(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
<div class="absolute right-0 top-0 pr-4 pt-4">
|
||||
<button mat-icon-button [matDialogClose]="undefined">
|
||||
<mat-icon
|
||||
class="text-secondary"
|
||||
[svgIcon]="'heroicons_outline:x-mark'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<h1>⚡ Send Zap</h1>
|
||||
<mat-dialog-content *ngIf="!showInvoiceSection || !lightningInvoice">
|
||||
<div class="preset-buttons">
|
||||
<button mat-mini-fab color="primary" *ngFor="let button of zapButtons" (click)="sats = button.value">
|
||||
<button
|
||||
mat-mini-fab
|
||||
color="primary"
|
||||
*ngFor="let button of zapButtons"
|
||||
(click)="sats = button.value"
|
||||
>
|
||||
<mat-icon>{{ button.icon }}</mat-icon>
|
||||
<span>{{ button.label }}</span>
|
||||
</button>
|
||||
@@ -9,13 +22,15 @@
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="outline" class="sats-input">
|
||||
<mat-label>Zap Amount</mat-label>
|
||||
<input matInput [(ngModel)]="sats" placeholder="e.g., 100" type="number" />
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="sats"
|
||||
placeholder="e.g., 100"
|
||||
type="number"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-icon-button color="warn" (click)="closeDialog()" [matTooltip]="'Close'">
|
||||
<mat-icon [svgIcon]="'heroicons_solid:x-mark'"></mat-icon>
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="sendZap()">
|
||||
Create invoice
|
||||
</button>
|
||||
@@ -26,20 +41,30 @@
|
||||
<div *ngIf="displayQRCode" class="qrcode">
|
||||
<span>Scan with phone to pay ({{ invoiceAmount }} sats)</span>
|
||||
<mat-divider></mat-divider>
|
||||
<qrcode [qrdata]="lightningInvoice" [matTooltip]="'Lightning Invoice'" [errorCorrectionLevel]="'M'"
|
||||
class="qrcode-image"></qrcode>
|
||||
<qrcode
|
||||
[qrdata]="lightningInvoice"
|
||||
[matTooltip]="'Lightning Invoice'"
|
||||
[errorCorrectionLevel]="'M'"
|
||||
class="qrcode-image"
|
||||
></qrcode>
|
||||
</div>
|
||||
|
||||
<mat-dialog-actions align="center">
|
||||
<button mat-icon-button color="warn" (click)="closeDialog()" [matTooltip]="'Close'">
|
||||
<mat-icon [svgIcon]="'heroicons_solid:x-mark'"></mat-icon>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="copyInvoice()"
|
||||
[matTooltip]="'Copy Invoice'"
|
||||
>
|
||||
<mat-icon
|
||||
[svgIcon]="'heroicons_outline:clipboard-document'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button (click)="copyInvoice()" [matTooltip]="'Copy Invoice'">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:clipboard-document'"></mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button (click)="payInvoice()" [matTooltip]="'Pay Invoice'">
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="payInvoice()"
|
||||
[matTooltip]="'Pay Invoice'"
|
||||
>
|
||||
<mat-icon color="#f79318" [svgIcon]="'feather:zap'"></mat-icon>
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
gap: 15px;
|
||||
justify-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.preset-buttons button {
|
||||
.preset-buttons button {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
width: 70px;
|
||||
@@ -16,24 +16,23 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-height: 60px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sats-input {
|
||||
.sats-input {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.lightning-buttons {
|
||||
.lightning-buttons {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.qrcode {
|
||||
.qrcode {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-image {
|
||||
.qrcode-image {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { LightningService } from 'app/services/lightning.service';
|
||||
import { Clipboard } from '@angular/cdk/clipboard';
|
||||
import { webln } from '@getalby/sdk';
|
||||
import { decode } from '@gandlaf21/bolt11-decode';
|
||||
import { bech32 } from '@scure/base';
|
||||
import { NgClass, CommonModule } from '@angular/common';
|
||||
import { CommonModule, NgClass } from '@angular/common';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOption } from '@angular/material/core';
|
||||
import { MatLabel, MatFormField, MatFormFieldModule } from '@angular/material/form-field';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatDivider } from '@angular/material/divider';
|
||||
import {
|
||||
MatFormField,
|
||||
MatFormFieldModule,
|
||||
MatLabel,
|
||||
} from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { decode } from '@gandlaf21/bolt11-decode';
|
||||
import { webln } from '@getalby/sdk';
|
||||
import { bech32 } from '@scure/base';
|
||||
import { QRCodeModule } from 'angularx-qrcode';
|
||||
import { SettingsIndexerComponent } from 'app/components/settings/indexer/indexer.component';
|
||||
import { SettingsNetworkComponent } from 'app/components/settings/network/network.component';
|
||||
import { SettingsNotificationsComponent } from 'app/components/settings/notifications/notifications.component';
|
||||
import { SettingsProfileComponent } from 'app/components/settings/profile/profile.component';
|
||||
import { SettingsRelayComponent } from 'app/components/settings/relay/relay.component';
|
||||
import { SettingsSecurityComponent } from 'app/components/settings/security/security.component';
|
||||
import { QRCodeModule } from 'angularx-qrcode';
|
||||
import { MatDivider } from '@angular/material/divider';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { LightningService } from 'app/services/lightning.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-send-dialog',
|
||||
@@ -54,10 +65,11 @@ import { MatTooltip } from '@angular/material/tooltip';
|
||||
QRCodeModule,
|
||||
MatDivider,
|
||||
MatTooltip,
|
||||
MatDialogTitle
|
||||
MatDialogTitle,
|
||||
MatDialogClose,
|
||||
],
|
||||
templateUrl: './send-dialog.component.html',
|
||||
styleUrls: ['./send-dialog.component.scss']
|
||||
styleUrls: ['./send-dialog.component.scss'],
|
||||
})
|
||||
export class SendDialogComponent {
|
||||
sats: string;
|
||||
@@ -66,7 +78,7 @@ export class SendDialogComponent {
|
||||
showInvoiceSection: boolean = false;
|
||||
displayQRCode: boolean = false;
|
||||
invoiceAmount: string = '?';
|
||||
nwc :any;
|
||||
nwc: any;
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<SendDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public metadata: any,
|
||||
@@ -86,7 +98,7 @@ export class SendDialogComponent {
|
||||
{ icon: 'rocket', label: '10k', value: 10000 },
|
||||
{ icon: 'local_fire_department', label: '100k', value: 100000 },
|
||||
{ icon: 'flash_on', label: '500k', value: 500000 },
|
||||
{ icon: 'diamond', label: '1M', value: 1000000 }
|
||||
{ icon: 'diamond', label: '1M', value: 1000000 },
|
||||
];
|
||||
|
||||
getLightningInfo(): void {
|
||||
@@ -100,20 +112,30 @@ export class SendDialogComponent {
|
||||
const data = new Uint8Array(bech32.fromWords(words));
|
||||
lightningAddress = new TextDecoder().decode(Uint8Array.from(data));
|
||||
} else if (this.metadata?.lud16) {
|
||||
lightningAddress = this.lightning.getLightningAddress(this.metadata.lud16);
|
||||
lightningAddress = this.lightning.getLightningAddress(
|
||||
this.metadata.lud16
|
||||
);
|
||||
}
|
||||
|
||||
if (lightningAddress !== '') {
|
||||
this.lightning.getLightning(lightningAddress).subscribe((response) => {
|
||||
this.lightningResponse = response;
|
||||
if (this.lightningResponse.status === 'Failed') {
|
||||
this.openSnackBar('Failed to lookup lightning address', 'dismiss');
|
||||
} else if (this.lightningResponse.callback) {
|
||||
this.showInvoiceSection = true;
|
||||
} else {
|
||||
this.openSnackBar("Couldn't find user's lightning address", 'dismiss');
|
||||
}
|
||||
});
|
||||
this.lightning
|
||||
.getLightning(lightningAddress)
|
||||
.subscribe((response) => {
|
||||
this.lightningResponse = response;
|
||||
if (this.lightningResponse.status === 'Failed') {
|
||||
this.openSnackBar(
|
||||
'Failed to lookup lightning address',
|
||||
'dismiss'
|
||||
);
|
||||
} else if (this.lightningResponse.callback) {
|
||||
this.showInvoiceSection = true;
|
||||
} else {
|
||||
this.openSnackBar(
|
||||
"Couldn't find user's lightning address",
|
||||
'dismiss'
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.openSnackBar('No lightning address found', 'dismiss');
|
||||
}
|
||||
@@ -121,8 +143,9 @@ export class SendDialogComponent {
|
||||
|
||||
getLightningInvoice(amount: string): void {
|
||||
if (this.lightningResponse && this.lightningResponse.callback) {
|
||||
this.lightning.getLightningInvoice(this.lightningResponse.callback, amount)
|
||||
.subscribe(async response => {
|
||||
this.lightning
|
||||
.getLightningInvoice(this.lightningResponse.callback, amount)
|
||||
.subscribe(async (response) => {
|
||||
this.lightningInvoice = response.pr;
|
||||
this.setInvoiceAmount(this.lightningInvoice);
|
||||
this.showInvoiceSection = true;
|
||||
@@ -134,7 +157,9 @@ export class SendDialogComponent {
|
||||
setInvoiceAmount(invoice: string): void {
|
||||
if (invoice) {
|
||||
const decodedInvoice = decode(invoice);
|
||||
const amountSection = decodedInvoice.sections.find((s) => s.name === 'amount');
|
||||
const amountSection = decodedInvoice.sections.find(
|
||||
(s) => s.name === 'amount'
|
||||
);
|
||||
if (amountSection) {
|
||||
this.invoiceAmount = String(Number(amountSection.value) / 1000);
|
||||
}
|
||||
@@ -149,29 +174,32 @@ export class SendDialogComponent {
|
||||
this.getLightningInvoice(String(Number(this.sats) * 1000));
|
||||
}
|
||||
|
||||
|
||||
async payInvoice(): Promise<void> {
|
||||
if (!this.lightningInvoice) {
|
||||
console.error('Lightning invoice is not set');
|
||||
return;
|
||||
}
|
||||
|
||||
const nwc = new webln.NostrWebLNProvider({ nostrWalletConnectUrl: await this.loadNWCUrl() });
|
||||
const nwc = new webln.NostrWebLNProvider({
|
||||
nostrWalletConnectUrl: await this.loadNWCUrl(),
|
||||
});
|
||||
|
||||
nwc.enable()
|
||||
.then(() => {
|
||||
return nwc.sendPayment(this.lightningInvoice);
|
||||
})
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response && response.preimage) {
|
||||
console.log(`Payment successful, preimage: ${response.preimage}`);
|
||||
console.log(
|
||||
`Payment successful, preimage: ${response.preimage}`
|
||||
);
|
||||
this.openSnackBar('Zapped!', 'dismiss');
|
||||
this.dialogRef.close();
|
||||
} else {
|
||||
this.listenForPaymentStatus(nwc);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Payment failed:', error);
|
||||
this.openSnackBar('Failed to pay invoice', 'dismiss');
|
||||
this.listenForPaymentStatus(nwc);
|
||||
@@ -179,13 +207,14 @@ export class SendDialogComponent {
|
||||
}
|
||||
|
||||
loadNWCUrl(): Promise<string> {
|
||||
const nwc = webln.NostrWebLNProvider.withNewSecret();
|
||||
const nwc = webln.NostrWebLNProvider.withNewSecret();
|
||||
|
||||
return nwc.initNWC({ name: 'Angor Hub' })
|
||||
return nwc
|
||||
.initNWC({ name: 'Angor Hub' })
|
||||
.then(() => {
|
||||
return nwc.getNostrWalletConnectUrl();
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error initializing NWC:', error);
|
||||
throw error;
|
||||
});
|
||||
@@ -194,16 +223,19 @@ export class SendDialogComponent {
|
||||
listenForPaymentStatus(nwc): void {
|
||||
const checkPaymentStatus = () => {
|
||||
nwc.sendPayment(this.lightningInvoice)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response && response.preimage) {
|
||||
console.log('Payment confirmed, preimage:', response.preimage);
|
||||
console.log(
|
||||
'Payment confirmed, preimage:',
|
||||
response.preimage
|
||||
);
|
||||
this.openSnackBar('Payment confirmed!', 'dismiss');
|
||||
this.dialogRef.close();
|
||||
} else {
|
||||
setTimeout(checkPaymentStatus, 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error checking payment status:', error);
|
||||
setTimeout(checkPaymentStatus, 5000);
|
||||
});
|
||||
@@ -228,5 +260,4 @@ export class SendDialogComponent {
|
||||
closeDialog(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
<div class="w-full max-w-3xl">
|
||||
<!-- Add Mainnet Indexer -->
|
||||
<div class="w-full mb-8">
|
||||
<div class="mb-8 w-full">
|
||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||
<mat-label>Add Mainnet Indexer</mat-label>
|
||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:link'" matPrefix></mat-icon>
|
||||
<input matInput [(ngModel)]="newMainnetIndexerUrl" placeholder="Mainnet Indexer URL" />
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:link'"
|
||||
matPrefix
|
||||
></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="newMainnetIndexerUrl"
|
||||
placeholder="Mainnet Indexer URL"
|
||||
/>
|
||||
<button mat-icon-button matSuffix (click)="addIndexer('mainnet')">
|
||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:plus-circle'"></mat-icon>
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:plus-circle'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@@ -15,22 +26,43 @@
|
||||
<div class="mt-8">
|
||||
<h3>Mainnet Indexers</h3>
|
||||
<div class="flex flex-col divide-y border-b border-t">
|
||||
<div *ngFor="let indexer of mainnetIndexers; trackBy: trackByFn" class="flex flex-col py-6 sm:flex-row sm:items-center">
|
||||
<div
|
||||
*ngFor="let indexer of mainnetIndexers; trackBy: trackByFn"
|
||||
class="flex flex-col py-6 sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="ml-4">
|
||||
<div class="font-medium">{{ indexer.url }}</div>
|
||||
<div class="text-sm text-gray-500">Primary: {{ indexer.primary ? 'Yes' : 'No' }}</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
Primary: {{ indexer.primary ? 'Yes' : 'No' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center sm:ml-auto sm:mt-0">
|
||||
<button mat-icon-button (click)="setPrimaryIndexer('mainnet', indexer)">
|
||||
<mat-icon *ngIf="indexer.primary; else nonPrimaryIcon" class="text-primary" [svgIcon]="'heroicons_solid:check-circle'"></mat-icon>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="setPrimaryIndexer('mainnet', indexer)"
|
||||
>
|
||||
<mat-icon
|
||||
*ngIf="indexer.primary; else nonPrimaryIcon"
|
||||
class="text-primary"
|
||||
[svgIcon]="'heroicons_solid:check-circle'"
|
||||
></mat-icon>
|
||||
<ng-template #nonPrimaryIcon>
|
||||
<mat-icon class="text-hint" [svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
|
||||
<mat-icon
|
||||
class="text-hint"
|
||||
[svgIcon]="'heroicons_outline:check-circle'"
|
||||
></mat-icon>
|
||||
</ng-template>
|
||||
</button>
|
||||
<button mat-icon-button (click)="removeIndexer('mainnet', indexer)">
|
||||
<mat-icon class="text-hint" [svgIcon]="'heroicons_outline:trash'"></mat-icon>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="removeIndexer('mainnet', indexer)"
|
||||
>
|
||||
<mat-icon
|
||||
class="text-hint"
|
||||
[svgIcon]="'heroicons_outline:trash'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,13 +70,24 @@
|
||||
</div>
|
||||
|
||||
<!-- Add Testnet Indexer -->
|
||||
<div class="w-full mb-8 mt-10">
|
||||
<div class="mb-8 mt-10 w-full">
|
||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||
<mat-label>Add Testnet Indexer</mat-label>
|
||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:link'" matPrefix></mat-icon>
|
||||
<input matInput [(ngModel)]="newTestnetIndexerUrl" placeholder="Testnet Indexer URL" />
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:link'"
|
||||
matPrefix
|
||||
></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="newTestnetIndexerUrl"
|
||||
placeholder="Testnet Indexer URL"
|
||||
/>
|
||||
<button mat-icon-button matSuffix (click)="addIndexer('testnet')">
|
||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:plus-circle'"></mat-icon>
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:plus-circle'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@@ -53,22 +96,43 @@
|
||||
<div class="mt-8">
|
||||
<h3>Testnet Indexers</h3>
|
||||
<div class="flex flex-col divide-y border-b border-t">
|
||||
<div *ngFor="let indexer of testnetIndexers; trackBy: trackByFn" class="flex flex-col py-6 sm:flex-row sm:items-center">
|
||||
<div
|
||||
*ngFor="let indexer of testnetIndexers; trackBy: trackByFn"
|
||||
class="flex flex-col py-6 sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="ml-4">
|
||||
<div class="font-medium">{{ indexer.url }}</div>
|
||||
<div class="text-sm text-gray-500">Primary: {{ indexer.primary ? 'Yes' : 'No' }}</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
Primary: {{ indexer.primary ? 'Yes' : 'No' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center sm:ml-auto sm:mt-0">
|
||||
<button mat-icon-button (click)="setPrimaryIndexer('testnet', indexer)">
|
||||
<mat-icon *ngIf="indexer.primary; else nonPrimaryIcon" class="text-primary" [svgIcon]="'heroicons_solid:check-circle'"></mat-icon>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="setPrimaryIndexer('testnet', indexer)"
|
||||
>
|
||||
<mat-icon
|
||||
*ngIf="indexer.primary; else nonPrimaryIcon"
|
||||
class="text-primary"
|
||||
[svgIcon]="'heroicons_solid:check-circle'"
|
||||
></mat-icon>
|
||||
<ng-template #nonPrimaryIcon>
|
||||
<mat-icon class="text-hint" [svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
|
||||
<mat-icon
|
||||
class="text-hint"
|
||||
[svgIcon]="'heroicons_outline:check-circle'"
|
||||
></mat-icon>
|
||||
</ng-template>
|
||||
</button>
|
||||
<button mat-icon-button (click)="removeIndexer('testnet', indexer)">
|
||||
<mat-icon class="text-hint" [svgIcon]="'heroicons_outline:trash'"></mat-icon>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="removeIndexer('testnet', indexer)"
|
||||
>
|
||||
<mat-icon
|
||||
class="text-hint"
|
||||
[svgIcon]="'heroicons_outline:trash'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AngorAlertComponent } from '@angor/components/alert';
|
||||
import { CommonModule, CurrencyPipe, NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -5,12 +6,7 @@ import {
|
||||
OnInit,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
UntypedFormBuilder,
|
||||
UntypedFormGroup,
|
||||
} from '@angular/forms';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -18,7 +14,6 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { AngorAlertComponent } from '@angor/components/alert';
|
||||
import { IndexerService } from 'app/services/indexer.service';
|
||||
|
||||
@Component({
|
||||
@@ -40,59 +35,76 @@ import { IndexerService } from 'app/services/indexer.service';
|
||||
MatOptionModule,
|
||||
MatButtonModule,
|
||||
CurrencyPipe,
|
||||
CommonModule
|
||||
CommonModule,
|
||||
],
|
||||
})
|
||||
export class SettingsIndexerComponent implements OnInit {
|
||||
mainnetIndexers: Array<{ url: string, primary: boolean }> = [];
|
||||
testnetIndexers: Array<{ url: string, primary: boolean }> = [];
|
||||
newMainnetIndexerUrl: string = '';
|
||||
newTestnetIndexerUrl: string = '';
|
||||
mainnetIndexers: Array<{ url: string; primary: boolean }> = [];
|
||||
testnetIndexers: Array<{ url: string; primary: boolean }> = [];
|
||||
newMainnetIndexerUrl: string = '';
|
||||
newTestnetIndexerUrl: string = '';
|
||||
|
||||
constructor(private indexerService: IndexerService) {}
|
||||
constructor(private indexerService: IndexerService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadIndexers();
|
||||
}
|
||||
|
||||
loadIndexers(): void {
|
||||
this.mainnetIndexers = this.indexerService.getIndexers('mainnet').map(url => ({
|
||||
url,
|
||||
primary: url === this.indexerService.getPrimaryIndexer('mainnet')
|
||||
}));
|
||||
this.testnetIndexers = this.indexerService.getIndexers('testnet').map(url => ({
|
||||
url,
|
||||
primary: url === this.indexerService.getPrimaryIndexer('testnet')
|
||||
}));
|
||||
|
||||
console.log('Mainnet Indexers:', this.mainnetIndexers);
|
||||
console.log('Testnet Indexers:', this.testnetIndexers);
|
||||
}
|
||||
|
||||
|
||||
addIndexer(network: 'mainnet' | 'testnet'): void {
|
||||
if (network === 'mainnet' && this.newMainnetIndexerUrl) {
|
||||
this.indexerService.addIndexer(this.newMainnetIndexerUrl, 'mainnet');
|
||||
this.loadIndexers();
|
||||
this.newMainnetIndexerUrl = '';
|
||||
} else if (network === 'testnet' && this.newTestnetIndexerUrl) {
|
||||
this.indexerService.addIndexer(this.newTestnetIndexerUrl, 'testnet');
|
||||
this.loadIndexers();
|
||||
this.newTestnetIndexerUrl = '';
|
||||
ngOnInit(): void {
|
||||
this.loadIndexers();
|
||||
}
|
||||
}
|
||||
|
||||
removeIndexer(network: 'mainnet' | 'testnet', indexer: { url: string, primary: boolean }): void {
|
||||
this.indexerService.removeIndexer(indexer.url, network);
|
||||
this.loadIndexers();
|
||||
}
|
||||
loadIndexers(): void {
|
||||
this.mainnetIndexers = this.indexerService
|
||||
.getIndexers('mainnet')
|
||||
.map((url) => ({
|
||||
url,
|
||||
primary:
|
||||
url === this.indexerService.getPrimaryIndexer('mainnet'),
|
||||
}));
|
||||
this.testnetIndexers = this.indexerService
|
||||
.getIndexers('testnet')
|
||||
.map((url) => ({
|
||||
url,
|
||||
primary:
|
||||
url === this.indexerService.getPrimaryIndexer('testnet'),
|
||||
}));
|
||||
|
||||
setPrimaryIndexer(network: 'mainnet' | 'testnet', indexer: { url: string, primary: boolean }): void {
|
||||
this.indexerService.setPrimaryIndexer(indexer.url, network);
|
||||
this.loadIndexers();
|
||||
}
|
||||
console.log('Mainnet Indexers:', this.mainnetIndexers);
|
||||
console.log('Testnet Indexers:', this.testnetIndexers);
|
||||
}
|
||||
|
||||
trackByFn(index: number, item: any): any {
|
||||
return item.url;
|
||||
}
|
||||
addIndexer(network: 'mainnet' | 'testnet'): void {
|
||||
if (network === 'mainnet' && this.newMainnetIndexerUrl) {
|
||||
this.indexerService.addIndexer(
|
||||
this.newMainnetIndexerUrl,
|
||||
'mainnet'
|
||||
);
|
||||
this.loadIndexers();
|
||||
this.newMainnetIndexerUrl = '';
|
||||
} else if (network === 'testnet' && this.newTestnetIndexerUrl) {
|
||||
this.indexerService.addIndexer(
|
||||
this.newTestnetIndexerUrl,
|
||||
'testnet'
|
||||
);
|
||||
this.loadIndexers();
|
||||
this.newTestnetIndexerUrl = '';
|
||||
}
|
||||
}
|
||||
|
||||
removeIndexer(
|
||||
network: 'mainnet' | 'testnet',
|
||||
indexer: { url: string; primary: boolean }
|
||||
): void {
|
||||
this.indexerService.removeIndexer(indexer.url, network);
|
||||
this.loadIndexers();
|
||||
}
|
||||
|
||||
setPrimaryIndexer(
|
||||
network: 'mainnet' | 'testnet',
|
||||
indexer: { url: string; primary: boolean }
|
||||
): void {
|
||||
this.indexerService.setPrimaryIndexer(indexer.url, network);
|
||||
this.loadIndexers();
|
||||
}
|
||||
|
||||
trackByFn(index: number, item: any): any {
|
||||
return item.url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +46,18 @@
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="mb-10 mt-11 border-t w-full max-w-3xl"></div>
|
||||
<div class="mb-10 mt-11 w-full max-w-3xl border-t"></div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end w-full max-w-3xl">
|
||||
<div class="flex w-full max-w-3xl items-center justify-end">
|
||||
<button mat-stroked-button type="button" (click)="cancel()">Cancel</button>
|
||||
<button mat-flat-button class="ml-4" type="button" color="primary" (click)="save()">Save</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
class="ml-4"
|
||||
type="button"
|
||||
color="primary"
|
||||
(click)="save()"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { AngorAlertComponent } from '@angor/components/alert';
|
||||
import { NgClass, CurrencyPipe, CommonModule } from '@angular/common';
|
||||
import { CommonModule, CurrencyPipe, NgClass } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -12,49 +17,52 @@ import { MatSelectModule } from '@angular/material/select';
|
||||
import { IndexerService } from 'app/services/indexer.service';
|
||||
|
||||
@Component({
|
||||
selector: 'settings-network',
|
||||
templateUrl: './network.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
AngorAlertComponent,
|
||||
MatRadioModule,
|
||||
NgClass,
|
||||
MatIconModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatOptionModule,
|
||||
MatButtonModule,
|
||||
CurrencyPipe,
|
||||
CommonModule
|
||||
],
|
||||
selector: 'settings-network',
|
||||
templateUrl: './network.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
AngorAlertComponent,
|
||||
MatRadioModule,
|
||||
NgClass,
|
||||
MatIconModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatOptionModule,
|
||||
MatButtonModule,
|
||||
CurrencyPipe,
|
||||
CommonModule,
|
||||
],
|
||||
})
|
||||
export class SettingsNetworkComponent implements OnInit {
|
||||
networkForm: FormGroup;
|
||||
selectedNetwork: 'mainnet' | 'testnet' = 'testnet';
|
||||
constructor(private fb: FormBuilder, private indexerService: IndexerService) {}
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private indexerService: IndexerService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.networkForm = this.fb.group({
|
||||
network: [this.indexerService.getNetwork()]
|
||||
});
|
||||
this.networkForm = this.fb.group({
|
||||
network: [this.indexerService.getNetwork()],
|
||||
});
|
||||
|
||||
this.selectedNetwork = this.indexerService.getNetwork();
|
||||
this.selectedNetwork = this.indexerService.getNetwork();
|
||||
}
|
||||
|
||||
setNetwork(network: 'mainnet' | 'testnet'): void {
|
||||
this.selectedNetwork = network;
|
||||
this.indexerService.setNetwork(this.selectedNetwork);
|
||||
this.selectedNetwork = network;
|
||||
this.indexerService.setNetwork(this.selectedNetwork);
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.indexerService.setNetwork(this.selectedNetwork);
|
||||
}
|
||||
this.indexerService.setNetwork(this.selectedNetwork);
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.selectedNetwork = this.indexerService.getNetwork();
|
||||
this.selectedNetwork = this.indexerService.getNetwork();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,38 +5,78 @@
|
||||
<div class="mt-8 grid w-full grid-cols-1 gap-6">
|
||||
<!-- Mention -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-auto cursor-pointer" (click)="mentionToggle.toggle()">
|
||||
<div
|
||||
class="flex-auto cursor-pointer"
|
||||
(click)="mentionToggle.toggle()"
|
||||
>
|
||||
<div class="font-medium leading-6">Mention</div>
|
||||
<div class="text-secondary text-md">Receive notifications when someone mentions you.</div>
|
||||
<div class="text-secondary text-md">
|
||||
Receive notifications when someone mentions you.
|
||||
</div>
|
||||
</div>
|
||||
<mat-slide-toggle class="ml-2" [color]="'primary'" [formControlName]="'mention'" #mentionToggle></mat-slide-toggle>
|
||||
<mat-slide-toggle
|
||||
class="ml-2"
|
||||
[color]="'primary'"
|
||||
[formControlName]="'mention'"
|
||||
#mentionToggle
|
||||
></mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<!-- Private Message -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-auto cursor-pointer" (click)="privateMessageToggle.toggle()">
|
||||
<div
|
||||
class="flex-auto cursor-pointer"
|
||||
(click)="privateMessageToggle.toggle()"
|
||||
>
|
||||
<div class="font-medium leading-6">Private Message</div>
|
||||
<div class="text-secondary text-md">Receive notifications for private messages.</div>
|
||||
<div class="text-secondary text-md">
|
||||
Receive notifications for private messages.
|
||||
</div>
|
||||
</div>
|
||||
<mat-slide-toggle class="ml-2" [color]="'primary'" [formControlName]="'privateMessage'" #privateMessageToggle></mat-slide-toggle>
|
||||
<mat-slide-toggle
|
||||
class="ml-2"
|
||||
[color]="'primary'"
|
||||
[formControlName]="'privateMessage'"
|
||||
#privateMessageToggle
|
||||
></mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<!-- Zap -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-auto cursor-pointer" (click)="zapToggle.toggle()">
|
||||
<div
|
||||
class="flex-auto cursor-pointer"
|
||||
(click)="zapToggle.toggle()"
|
||||
>
|
||||
<div class="font-medium leading-6">Zap</div>
|
||||
<div class="text-secondary text-md">Receive notifications when you get a zap.</div>
|
||||
<div class="text-secondary text-md">
|
||||
Receive notifications when you get a zap.
|
||||
</div>
|
||||
</div>
|
||||
<mat-slide-toggle class="ml-2" [color]="'primary'" [formControlName]="'zap'" #zapToggle></mat-slide-toggle>
|
||||
<mat-slide-toggle
|
||||
class="ml-2"
|
||||
[color]="'primary'"
|
||||
[formControlName]="'zap'"
|
||||
#zapToggle
|
||||
></mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<!-- New Follower -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-auto cursor-pointer" (click)="followerToggle.toggle()">
|
||||
<div
|
||||
class="flex-auto cursor-pointer"
|
||||
(click)="followerToggle.toggle()"
|
||||
>
|
||||
<div class="font-medium leading-6">New Follower</div>
|
||||
<div class="text-secondary text-md">Receive notifications when someone follows you.</div>
|
||||
<div class="text-secondary text-md">
|
||||
Receive notifications when someone follows you.
|
||||
</div>
|
||||
</div>
|
||||
<mat-slide-toggle class="ml-2" [color]="'primary'" [formControlName]="'follower'" #followerToggle></mat-slide-toggle>
|
||||
<mat-slide-toggle
|
||||
class="ml-2"
|
||||
[color]="'primary'"
|
||||
[formControlName]="'follower'"
|
||||
#followerToggle
|
||||
></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +85,15 @@
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end">
|
||||
<button mat-stroked-button type="button">Cancel</button>
|
||||
<button class="ml-4" mat-flat-button type="button" [color]="'primary'" (click)="saveSettings()">Save</button>
|
||||
<button
|
||||
class="ml-4"
|
||||
mat-flat-button
|
||||
type="button"
|
||||
[color]="'primary'"
|
||||
(click)="saveSettings()"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,9 @@ export class SettingsNotificationsComponent implements OnInit {
|
||||
|
||||
this.notificationsForm = this._formBuilder.group({
|
||||
mention: [savedSettings.includes(this.notificationKinds.mention)],
|
||||
privateMessage: [savedSettings.includes(this.notificationKinds.privateMessage)],
|
||||
privateMessage: [
|
||||
savedSettings.includes(this.notificationKinds.privateMessage),
|
||||
],
|
||||
zap: [savedSettings.includes(this.notificationKinds.zap)],
|
||||
follower: [savedSettings.includes(this.notificationKinds.follower)],
|
||||
});
|
||||
@@ -75,6 +77,6 @@ export class SettingsNotificationsComponent implements OnInit {
|
||||
|
||||
private loadNotificationSettings(): number[] {
|
||||
const storedSettings = localStorage.getItem('notificationSettings');
|
||||
return storedSettings ? JSON.parse(storedSettings) : [1, 3, 4, 9735]; // Default to all kinds if not set
|
||||
return storedSettings ? JSON.parse(storedSettings) : [1, 3, 4, 7, 9735]; // Default to all kinds if not set
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
|
||||
<!-- Section -->
|
||||
<div class="w-full">
|
||||
<div class="text-secondary">
|
||||
<div class="text-secondary">
|
||||
Following information is publicly displayed, be careful!
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,7 +13,11 @@
|
||||
<div class="sm:col-span-4">
|
||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||
<mat-label>Name</mat-label>
|
||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:user'" matPrefix></mat-icon>
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:user'"
|
||||
matPrefix
|
||||
></mat-icon>
|
||||
<input [formControlName]="'name'" matInput />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@@ -46,10 +50,16 @@
|
||||
<div class="sm:col-span-4">
|
||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||
<mat-label>About</mat-label>
|
||||
<textarea matInput [formControlName]="'about'" cdkTextareaAutosize [cdkAutosizeMinRows]="5"></textarea>
|
||||
<textarea
|
||||
matInput
|
||||
[formControlName]="'about'"
|
||||
cdkTextareaAutosize
|
||||
[cdkAutosizeMinRows]="5"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
<div class="text-hint mt-1 text-md">
|
||||
Brief description for your profile. Basic HTML and Emoji are allowed.
|
||||
Brief description for your profile. Basic HTML and Emoji are
|
||||
allowed.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,45 +79,51 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- LUD06 -->
|
||||
<div class="sm:col-span-4">
|
||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||
<mat-label>LUD06</mat-label>
|
||||
<input [formControlName]="'lud06'" matInput />
|
||||
<mat-hint>
|
||||
LUD06 is an LNURL (Lightning Network URL) for receiving Bitcoin payments over the Lightning Network.
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- LUD06 -->
|
||||
<div class="sm:col-span-4">
|
||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||
<mat-label>LUD06</mat-label>
|
||||
<input [formControlName]="'lud06'" matInput />
|
||||
<mat-hint>
|
||||
LUD06 is an LNURL (Lightning Network URL) for receiving
|
||||
Bitcoin payments over the Lightning Network.
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- LUD16 -->
|
||||
<div class="sm:col-span-4">
|
||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||
<mat-label>LUD16</mat-label>
|
||||
<input [formControlName]="'lud16'" matInput />
|
||||
<mat-hint>
|
||||
LUD16 is a Lightning address, similar to an email format, used to receive Bitcoin payments via the Lightning Network.
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- NIP05 -->
|
||||
<div class="sm:col-span-4">
|
||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||
<mat-label>NIP05</mat-label>
|
||||
<input [formControlName]="'nip05'" matInput />
|
||||
<mat-hint>
|
||||
NIP05 provides a user-friendly identifier for Nostr, similar to an email address, to help identify and verify your public identity.
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- LUD16 -->
|
||||
<div class="sm:col-span-4">
|
||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||
<mat-label>LUD16</mat-label>
|
||||
<input [formControlName]="'lud16'" matInput />
|
||||
<mat-hint>
|
||||
LUD16 is a Lightning address, similar to an email
|
||||
format, used to receive Bitcoin payments via the
|
||||
Lightning Network.
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- NIP05 -->
|
||||
<div class="sm:col-span-4">
|
||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||
<mat-label>NIP05</mat-label>
|
||||
<input [formControlName]="'nip05'" matInput />
|
||||
<mat-hint>
|
||||
NIP05 provides a user-friendly identifier for Nostr,
|
||||
similar to an email address, to help identify and verify
|
||||
your public identity.
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end mt-8">
|
||||
<div class="mt-8 flex items-center justify-end">
|
||||
<button mat-stroked-button type="button">Cancel</button>
|
||||
<button class="ml-4" mat-flat-button type="submit" color="primary">Save</button>
|
||||
<button class="ml-4" mat-flat-button type="submit" color="primary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ViewEncapsulation, ChangeDetectionStrategy, OnInit } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule, FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
OnInit,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
@@ -14,9 +25,9 @@ import { hexToBytes } from '@noble/hashes/utils';
|
||||
import { MetadataService } from 'app/services/metadata.service';
|
||||
import { RelayService } from 'app/services/relay.service';
|
||||
import { SignerService } from 'app/services/signer.service';
|
||||
import { UnsignedEvent, NostrEvent, finalizeEvent } from 'nostr-tools';
|
||||
import { PasswordDialogComponent } from 'app/shared/password-dialog/password-dialog.component';
|
||||
|
||||
import { NostrEvent, UnsignedEvent, finalizeEvent } from 'nostr-tools';
|
||||
|
||||
@Component({
|
||||
selector: 'settings-profile',
|
||||
templateUrl: './profile.component.html',
|
||||
@@ -33,7 +44,7 @@ import { PasswordDialogComponent } from 'app/shared/password-dialog/password-dia
|
||||
MatSelectModule,
|
||||
MatOptionModule,
|
||||
MatButtonModule,
|
||||
CommonModule
|
||||
CommonModule,
|
||||
],
|
||||
})
|
||||
export class SettingsProfileComponent implements OnInit {
|
||||
@@ -46,8 +57,8 @@ export class SettingsProfileComponent implements OnInit {
|
||||
private metadataService: MetadataService,
|
||||
private relayService: RelayService,
|
||||
private router: Router,
|
||||
private dialog: MatDialog,
|
||||
) { }
|
||||
private dialog: MatDialog
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.profileForm = this.fb.group({
|
||||
@@ -59,15 +70,23 @@ export class SettingsProfileComponent implements OnInit {
|
||||
picture: [''],
|
||||
banner: [''],
|
||||
lud06: [''],
|
||||
lud16: ['', Validators.pattern("^[a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,4}$")],
|
||||
nip05: ['', Validators.pattern("^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$")]
|
||||
lud16: [
|
||||
'',
|
||||
Validators.pattern('^[a-z0-9._-]+@[a-z0-9.-]+.[a-z]{2,4}$'),
|
||||
],
|
||||
nip05: [
|
||||
'',
|
||||
Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+.[a-z]{2,4}$'),
|
||||
],
|
||||
});
|
||||
|
||||
this.setValues();
|
||||
}
|
||||
|
||||
async setValues() {
|
||||
let kind0 = await this.metadataService.getUserMetadata(this.signerService.getPublicKey());
|
||||
let kind0 = await this.metadataService.getUserMetadata(
|
||||
this.signerService.getPublicKey()
|
||||
);
|
||||
if (kind0) {
|
||||
this.profileForm.setValue({
|
||||
name: kind0.name || '',
|
||||
@@ -100,7 +119,8 @@ export class SettingsProfileComponent implements OnInit {
|
||||
const storedPassword = this.signerService.getPassword();
|
||||
if (storedPassword) {
|
||||
try {
|
||||
const privateKey = await this.signerService.getSecretKey(storedPassword);
|
||||
const privateKey =
|
||||
await this.signerService.getSecretKey(storedPassword);
|
||||
this.signEvent(privateKey);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -108,46 +128,54 @@ export class SettingsProfileComponent implements OnInit {
|
||||
} else {
|
||||
const dialogRef = this.dialog.open(PasswordDialogComponent, {
|
||||
width: '300px',
|
||||
disableClose: true
|
||||
disableClose: true,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(async result => {
|
||||
dialogRef.afterClosed().subscribe(async (result) => {
|
||||
if (result && result.password) {
|
||||
try {
|
||||
const privateKey = await this.signerService.getSecretKey(result.password);
|
||||
const privateKey =
|
||||
await this.signerService.getSecretKey(
|
||||
result.password
|
||||
);
|
||||
this.signEvent(privateKey);
|
||||
if (result.duration != 0) {
|
||||
this.signerService.savePassword(result.password, result.duration);
|
||||
this.signerService.savePassword(
|
||||
result.password,
|
||||
result.duration
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
console.error('Password not provided');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
} else if (this.signerService.isUsingExtension()) {
|
||||
const unsignedEvent: UnsignedEvent = this.signerService.getUnsignedEvent(0, [], this.content);
|
||||
const signedEvent = await this.signerService.signEventWithExtension(unsignedEvent);
|
||||
const unsignedEvent: UnsignedEvent =
|
||||
this.signerService.getUnsignedEvent(0, [], this.content);
|
||||
const signedEvent =
|
||||
await this.signerService.signEventWithExtension(unsignedEvent);
|
||||
this.publishSignedEvent(signedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
async signEvent(privateKey: string) {
|
||||
const unsignedEvent: UnsignedEvent = this.signerService.getUnsignedEvent(0, [], this.content);
|
||||
const unsignedEvent: UnsignedEvent =
|
||||
this.signerService.getUnsignedEvent(0, [], this.content);
|
||||
const privateKeyBytes = hexToBytes(privateKey);
|
||||
const signedEvent: NostrEvent = finalizeEvent(unsignedEvent, privateKeyBytes);
|
||||
const signedEvent: NostrEvent = finalizeEvent(
|
||||
unsignedEvent,
|
||||
privateKeyBytes
|
||||
);
|
||||
this.publishSignedEvent(signedEvent);
|
||||
}
|
||||
|
||||
publishSignedEvent(signedEvent: NostrEvent) {
|
||||
this.relayService.publishEventToRelays(signedEvent);
|
||||
console.log("Profile Updated!");
|
||||
console.log('Profile Updated!');
|
||||
this.router.navigate([`/profile`]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,42 +3,72 @@
|
||||
<div class="w-full">
|
||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||
<mat-label>Add Relay</mat-label>
|
||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:link'" matPrefix></mat-icon>
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:link'"
|
||||
matPrefix
|
||||
></mat-icon>
|
||||
<input matInput [(ngModel)]="newRelayUrl" placeholder="Relay URL" />
|
||||
<button mat-icon-button matSuffix (click)="addRelay()">
|
||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:plus-circle'"></mat-icon>
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:plus-circle'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Relays -->
|
||||
<div class="mt-8 flex flex-col divide-y border-b border-t">
|
||||
<div *ngFor="let relay of relays; trackBy: trackByFn" class="flex flex-col py-6 sm:flex-row sm:items-center">
|
||||
<div
|
||||
*ngFor="let relay of relays; trackBy: trackByFn"
|
||||
class="flex flex-col py-6 sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="flex h-10 w-10 flex-0 items-center justify-center overflow-hidden rounded-full">
|
||||
<img class="h-full w-full object-cover" [src]="getSafeUrl(relayFavIcon(relay.url))"
|
||||
onerror="this.src='/images/avatars/avatar-placeholder.png'" alt="relay avatar" />
|
||||
|
||||
<div
|
||||
class="flex h-10 w-10 flex-0 items-center justify-center overflow-hidden rounded-full"
|
||||
>
|
||||
<img
|
||||
class="h-full w-full object-cover"
|
||||
[src]="getSafeUrl(relayFavIcon(relay.url))"
|
||||
onerror="this.src='/images/avatars/avatar-placeholder.png'"
|
||||
alt="relay avatar"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="font-medium">{{ relay.url }}</div>
|
||||
<div class="text-sm" [ngClass]="getRelayStatusClass(relay)">Status: {{ getRelayStatus(relay) }}
|
||||
<div class="text-sm" [ngClass]="getRelayStatusClass(relay)">
|
||||
Status: {{ getRelayStatus(relay) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center sm:ml-auto sm:mt-0">
|
||||
<mat-form-field class="angor-mat-dense w-50" [subscriptSizing]="'dynamic'">
|
||||
<mat-select [(ngModel)]="relay.accessType" (selectionChange)="updateRelayAccess(relay)">
|
||||
<mat-form-field
|
||||
class="angor-mat-dense w-50"
|
||||
[subscriptSizing]="'dynamic'"
|
||||
>
|
||||
<mat-select
|
||||
[(ngModel)]="relay.accessType"
|
||||
(selectionChange)="updateRelayAccess(relay)"
|
||||
>
|
||||
<mat-select-trigger class="text-md">
|
||||
<span class="ml-1 font-medium">{{ relay.accessType | titlecase }}</span>
|
||||
<span class="ml-1 font-medium">{{
|
||||
relay.accessType | titlecase
|
||||
}}</span>
|
||||
</mat-select-trigger>
|
||||
<mat-option *ngFor="let option of accessOptions" [value]="option.value">
|
||||
<mat-option
|
||||
*ngFor="let option of accessOptions"
|
||||
[value]="option.value"
|
||||
>
|
||||
<div class="font-medium">{{ option.label }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button mat-icon-button (click)="removeRelay(relay.url)">
|
||||
<mat-icon class="text-hint" [svgIcon]="'heroicons_outline:trash'"></mat-icon>
|
||||
<mat-icon
|
||||
class="text-hint"
|
||||
[svgIcon]="'heroicons_outline:trash'"
|
||||
></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { CommonModule, TitleCasePipe } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnInit,
|
||||
ViewEncapsulation,
|
||||
ChangeDetectorRef,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
@@ -52,7 +52,7 @@ export class SettingsRelayComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
// Subscribe to relays observable
|
||||
this.subscriptions.add(
|
||||
this.relayService.getRelays().subscribe(relays => {
|
||||
this.relayService.getRelays().subscribe((relays) => {
|
||||
this.zone.run(() => {
|
||||
this.relays = relays;
|
||||
this.cdr.markForCheck(); // Mark the component for check
|
||||
@@ -65,17 +65,20 @@ export class SettingsRelayComponent implements OnInit {
|
||||
{
|
||||
label: 'Read',
|
||||
value: 'read',
|
||||
description: 'Reads only, does not write, unless explicitly specified on publish action.',
|
||||
description:
|
||||
'Reads only, does not write, unless explicitly specified on publish action.',
|
||||
},
|
||||
{
|
||||
label: 'Write',
|
||||
value: 'write',
|
||||
description: 'Writes your events, profile, and other metadata updates. Connects on-demand.',
|
||||
description:
|
||||
'Writes your events, profile, and other metadata updates. Connects on-demand.',
|
||||
},
|
||||
{
|
||||
label: 'Read and Write',
|
||||
value: 'read-write',
|
||||
description: 'Reads and writes events, profiles, and other metadata. Always connected.',
|
||||
description:
|
||||
'Reads and writes events, profiles, and other metadata. Always connected.',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -113,13 +116,14 @@ export class SettingsRelayComponent implements OnInit {
|
||||
}
|
||||
|
||||
relayFavIcon(url: string): string {
|
||||
let safeUrl = url.replace('wss://', 'https://').replace('ws://', 'https://');
|
||||
let safeUrl = url
|
||||
.replace('wss://', 'https://')
|
||||
.replace('ws://', 'https://');
|
||||
|
||||
return safeUrl + '/favicon.ico';
|
||||
}
|
||||
}
|
||||
|
||||
getSafeUrl(url: string): SafeUrl {
|
||||
getSafeUrl(url: string): SafeUrl {
|
||||
return this.sanitizer.bypassSecurityTrustUrl(url);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user