mirror of
https://github.com/block-core/angor-hub-old.git
synced 2025-12-18 10:24:20 +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",
|
"quill-delta",
|
||||||
"buffer",
|
"buffer",
|
||||||
"localforage",
|
"localforage",
|
||||||
"moment"
|
"moment",
|
||||||
|
"bech32",
|
||||||
|
"bn.js",
|
||||||
|
"qrcode",
|
||||||
|
"dayjs",
|
||||||
|
"dayjs/plugin/relativeTime"
|
||||||
],
|
],
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "angor-hub",
|
"name": "angor-hub",
|
||||||
"version": "0.0.7",
|
"version": "0.0.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "angor-hub",
|
"name": "angor-hub",
|
||||||
"version": "0.0.7",
|
"version": "0.0.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-builders/custom-webpack": "^18.0.0",
|
"@angular-builders/custom-webpack": "^18.0.0",
|
||||||
"@angular/animations": "18.2.6",
|
"@angular/animations": "18.2.6",
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
"karma-jasmine-html-reporter": "2.1.0",
|
"karma-jasmine-html-reporter": "2.1.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"postcss": "8.4.47",
|
"postcss": "8.4.47",
|
||||||
"prettier": "3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-organize-imports": "4.1.0",
|
"prettier-plugin-organize-imports": "4.1.0",
|
||||||
"prettier-plugin-tailwindcss": "0.6.8",
|
"prettier-plugin-tailwindcss": "0.6.8",
|
||||||
"tailwindcss": "3.4.13",
|
"tailwindcss": "3.4.13",
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"deploy": "ng deploy",
|
"deploy": "ng deploy",
|
||||||
"version": "node -p \"require('./package.json').version\"",
|
"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": {
|
"dependencies": {
|
||||||
"@angular-builders/custom-webpack": "^18.0.0",
|
"@angular-builders/custom-webpack": "^18.0.0",
|
||||||
@@ -98,7 +99,7 @@
|
|||||||
"karma-jasmine-html-reporter": "2.1.0",
|
"karma-jasmine-html-reporter": "2.1.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"postcss": "8.4.47",
|
"postcss": "8.4.47",
|
||||||
"prettier": "3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-organize-imports": "4.1.0",
|
"prettier-plugin-organize-imports": "4.1.0",
|
||||||
"prettier-plugin-tailwindcss": "0.6.8",
|
"prettier-plugin-tailwindcss": "0.6.8",
|
||||||
"tailwindcss": "3.4.13",
|
"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 {
|
import {
|
||||||
ANGOR_MOCK_API_DEFAULT_DELAY,
|
ANGOR_MOCK_API_DEFAULT_DELAY,
|
||||||
mockApiInterceptor,
|
mockApiInterceptor,
|
||||||
@@ -25,6 +13,18 @@ import { AngorMediaWatcherService } from '@angor/services/media-watcher';
|
|||||||
import { AngorPlatformService } from '@angor/services/platform';
|
import { AngorPlatformService } from '@angor/services/platform';
|
||||||
import { AngorSplashScreenService } from '@angor/services/splash-screen';
|
import { AngorSplashScreenService } from '@angor/services/splash-screen';
|
||||||
import { AngorUtilsService } from '@angor/services/utils';
|
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 = {
|
export type AngorProviderConfig = {
|
||||||
mockApi?: {
|
mockApi?: {
|
||||||
@@ -40,10 +40,8 @@ export type AngorProviderConfig = {
|
|||||||
export const provideAngor = (
|
export const provideAngor = (
|
||||||
config: AngorProviderConfig
|
config: AngorProviderConfig
|
||||||
): Array<Provider | EnvironmentProviders> => {
|
): Array<Provider | EnvironmentProviders> => {
|
||||||
|
|
||||||
const providers: Array<Provider | EnvironmentProviders> = [
|
const providers: Array<Provider | EnvironmentProviders> = [
|
||||||
{
|
{
|
||||||
|
|
||||||
provide: MATERIAL_SANITY_CHECKS,
|
provide: MATERIAL_SANITY_CHECKS,
|
||||||
useValue: {
|
useValue: {
|
||||||
doctype: true,
|
doctype: true,
|
||||||
@@ -52,7 +50,6 @@ export const provideAngor = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
||||||
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
||||||
useValue: {
|
useValue: {
|
||||||
appearance: 'fill',
|
appearance: 'fill',
|
||||||
@@ -103,7 +100,6 @@ export const provideAngor = (
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
if (config?.mockApi?.services) {
|
if (config?.mockApi?.services) {
|
||||||
providers.push(
|
providers.push(
|
||||||
provideHttpClient(withInterceptors([mockApiInterceptor])),
|
provideHttpClient(withInterceptors([mockApiInterceptor])),
|
||||||
@@ -116,6 +112,5 @@ export const provideAngor = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return providers;
|
return providers;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
* Defines animation curves for Angor.
|
* Defines animation curves for Angor.
|
||||||
*/
|
*/
|
||||||
export class AngorAnimationCurves {
|
export class AngorAnimationCurves {
|
||||||
static standard = 'cubic-bezier(0.4, 0.0, 0.2, 1)'; // Standard animation 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 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 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 sharp = 'cubic-bezier(0.4, 0.0, 0.6, 1)'; // Sharp curve
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines animation durations for Angor.
|
* Defines animation durations for Angor.
|
||||||
*/
|
*/
|
||||||
export class AngorAnimationDurations {
|
export class AngorAnimationDurations {
|
||||||
static complex = '375ms'; // Duration for complex animations
|
static complex = '375ms'; // Duration for complex animations
|
||||||
static entering = '225ms'; // Duration for entering animations
|
static entering = '225ms'; // Duration for entering animations
|
||||||
static exiting = '195ms'; // Duration for exiting animations
|
static exiting = '195ms'; // Duration for exiting animations
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
AngorAnimationCurves,
|
||||||
|
AngorAnimationDurations,
|
||||||
|
} from '@angor/animations/defaults';
|
||||||
import {
|
import {
|
||||||
animate,
|
animate,
|
||||||
state,
|
state,
|
||||||
@@ -5,10 +9,6 @@ import {
|
|||||||
transition,
|
transition,
|
||||||
trigger,
|
trigger,
|
||||||
} from '@angular/animations';
|
} from '@angular/animations';
|
||||||
import {
|
|
||||||
AngorAnimationCurves,
|
|
||||||
AngorAnimationDurations,
|
|
||||||
} from '@angor/animations/defaults';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Animation trigger for expand/collapse transitions
|
* Animation trigger for expand/collapse transitions
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
AngorAnimationCurves,
|
||||||
|
AngorAnimationDurations,
|
||||||
|
} from '@angor/animations/defaults';
|
||||||
import {
|
import {
|
||||||
animate,
|
animate,
|
||||||
state,
|
state,
|
||||||
@@ -5,10 +9,6 @@ import {
|
|||||||
transition,
|
transition,
|
||||||
trigger,
|
trigger,
|
||||||
} from '@angular/animations';
|
} from '@angular/animations';
|
||||||
import {
|
|
||||||
AngorAnimationCurves,
|
|
||||||
AngorAnimationDurations,
|
|
||||||
} from '@angor/animations/defaults';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fade in animation trigger
|
* Fade in animation trigger
|
||||||
|
|||||||
@@ -21,15 +21,42 @@ const shake = trigger('shake', [
|
|||||||
'{{timings}}',
|
'{{timings}}',
|
||||||
keyframes([
|
keyframes([
|
||||||
style({ transform: 'translate3d(0, 0, 0)', offset: 0 }),
|
style({ transform: 'translate3d(0, 0, 0)', offset: 0 }),
|
||||||
style({ transform: 'translate3d(-10px, 0, 0)', offset: 0.1 }),
|
style({
|
||||||
style({ transform: 'translate3d(10px, 0, 0)', offset: 0.2 }),
|
transform: 'translate3d(-10px, 0, 0)',
|
||||||
style({ transform: 'translate3d(-10px, 0, 0)', offset: 0.3 }),
|
offset: 0.1,
|
||||||
style({ transform: 'translate3d(10px, 0, 0)', offset: 0.4 }),
|
}),
|
||||||
style({ transform: 'translate3d(-10px, 0, 0)', offset: 0.5 }),
|
style({
|
||||||
style({ transform: 'translate3d(10px, 0, 0)', offset: 0.6 }),
|
transform: 'translate3d(10px, 0, 0)',
|
||||||
style({ transform: 'translate3d(-10px, 0, 0)', offset: 0.7 }),
|
offset: 0.2,
|
||||||
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.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 }),
|
style({ transform: 'translate3d(0, 0, 0)', offset: 1 }),
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
AngorAnimationCurves,
|
||||||
|
AngorAnimationDurations,
|
||||||
|
} from '@angor/animations/defaults';
|
||||||
import {
|
import {
|
||||||
animate,
|
animate,
|
||||||
state,
|
state,
|
||||||
@@ -5,10 +9,6 @@ import {
|
|||||||
transition,
|
transition,
|
||||||
trigger,
|
trigger,
|
||||||
} from '@angular/animations';
|
} from '@angular/animations';
|
||||||
import {
|
|
||||||
AngorAnimationCurves,
|
|
||||||
AngorAnimationDurations,
|
|
||||||
} from '@angor/animations/defaults';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Slide in from top animation trigger
|
* Slide in from top animation trigger
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
AngorAnimationCurves,
|
||||||
|
AngorAnimationDurations,
|
||||||
|
} from '@angor/animations/defaults';
|
||||||
import {
|
import {
|
||||||
animate,
|
animate,
|
||||||
state,
|
state,
|
||||||
@@ -5,10 +9,6 @@ import {
|
|||||||
transition,
|
transition,
|
||||||
trigger,
|
trigger,
|
||||||
} from '@angular/animations';
|
} from '@angular/animations';
|
||||||
import {
|
|
||||||
AngorAnimationCurves,
|
|
||||||
AngorAnimationDurations,
|
|
||||||
} from '@angor/animations/defaults';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a reusable animation trigger with configurable parameters.
|
* 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 { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -16,13 +23,6 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
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';
|
import { Subject, filter, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -86,16 +86,22 @@ export class AngorAlertComponent implements OnChanges, OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if ('dismissed' in changes) {
|
if ('dismissed' in changes) {
|
||||||
this.dismissed = coerceBooleanProperty(changes.dismissed.currentValue);
|
this.dismissed = coerceBooleanProperty(
|
||||||
|
changes.dismissed.currentValue
|
||||||
|
);
|
||||||
this._toggleDismiss(this.dismissed);
|
this._toggleDismiss(this.dismissed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('dismissible' in changes) {
|
if ('dismissible' in changes) {
|
||||||
this.dismissible = coerceBooleanProperty(changes.dismissible.currentValue);
|
this.dismissible = coerceBooleanProperty(
|
||||||
|
changes.dismissible.currentValue
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('showIcon' in changes) {
|
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 { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
@@ -7,8 +9,6 @@ import {
|
|||||||
SimpleChanges,
|
SimpleChanges,
|
||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { angorAnimations } from '@angor/animations';
|
|
||||||
import { AngorCardFace } from '@angor/components/card/card.types';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'angor-card',
|
selector: 'angor-card',
|
||||||
@@ -47,11 +47,15 @@ export class AngorCardComponent implements OnChanges {
|
|||||||
*/
|
*/
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if ('expanded' in changes) {
|
if ('expanded' in changes) {
|
||||||
this.expanded = coerceBooleanProperty(changes.expanded.currentValue);
|
this.expanded = coerceBooleanProperty(
|
||||||
|
changes.expanded.currentValue
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('flippable' in changes) {
|
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 {
|
import {
|
||||||
animate,
|
animate,
|
||||||
AnimationBuilder,
|
AnimationBuilder,
|
||||||
@@ -11,6 +17,7 @@ import {
|
|||||||
EventEmitter,
|
EventEmitter,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
HostListener,
|
HostListener,
|
||||||
|
inject,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
@@ -19,14 +26,7 @@ import {
|
|||||||
Renderer2,
|
Renderer2,
|
||||||
SimpleChanges,
|
SimpleChanges,
|
||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
inject,
|
|
||||||
} from '@angular/core';
|
} 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({
|
@Component({
|
||||||
selector: 'angor-drawer',
|
selector: 'angor-drawer',
|
||||||
@@ -56,7 +56,8 @@ export class AngorDrawerComponent implements OnChanges, OnInit, OnDestroy {
|
|||||||
@Output() readonly fixedChanged = new EventEmitter<boolean>();
|
@Output() readonly fixedChanged = new EventEmitter<boolean>();
|
||||||
@Output() readonly modeChanged = new EventEmitter<AngorDrawerMode>();
|
@Output() readonly modeChanged = new EventEmitter<AngorDrawerMode>();
|
||||||
@Output() readonly openedChanged = new EventEmitter<boolean>();
|
@Output() readonly openedChanged = new EventEmitter<boolean>();
|
||||||
@Output() readonly positionChanged = new EventEmitter<AngorDrawerPosition>();
|
@Output() readonly positionChanged =
|
||||||
|
new EventEmitter<AngorDrawerPosition>();
|
||||||
|
|
||||||
private _animationsEnabled: boolean = false;
|
private _animationsEnabled: boolean = false;
|
||||||
private _hovered: boolean = false;
|
private _hovered: boolean = false;
|
||||||
@@ -107,7 +108,11 @@ export class AngorDrawerComponent implements OnChanges, OnInit, OnDestroy {
|
|||||||
this._hideOverlay();
|
this._hideOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previousMode === 'side' && currentMode === 'over' && this.opened) {
|
if (
|
||||||
|
previousMode === 'side' &&
|
||||||
|
currentMode === 'over' &&
|
||||||
|
this.opened
|
||||||
|
) {
|
||||||
this._showOverlay();
|
this._showOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +130,9 @@ export class AngorDrawerComponent implements OnChanges, OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ('transparentOverlay' in changes) {
|
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 { AngorDrawerComponent } from '@angor/components/drawer/drawer.component';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AngorDrawerService {
|
export class AngorDrawerService {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AngorHighlightService } from '@angor/components/highlight/highlight.service';
|
||||||
import { NgClass } from '@angular/common';
|
import { NgClass } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
@@ -16,7 +17,6 @@ import {
|
|||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { DomSanitizer } from '@angular/platform-browser';
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
import { AngorHighlightService } from '@angor/components/highlight/highlight.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'textarea[angor-highlight]',
|
selector: 'textarea[angor-highlight]',
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import hljs from 'highlight.js';
|
|||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AngorHighlightService {
|
export class AngorHighlightService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Highlights the provided code using the specified language.
|
* Highlights the provided code using the specified language.
|
||||||
*/
|
*/
|
||||||
@@ -28,15 +27,17 @@ export class AngorHighlightService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine the smallest indentation
|
// Determine the smallest indentation
|
||||||
lines.filter(line => line.length).forEach((line, index) => {
|
lines
|
||||||
if (index === 0) {
|
.filter((line) => line.length)
|
||||||
indentation = line.search(/\S|$/);
|
.forEach((line, index) => {
|
||||||
} else {
|
if (index === 0) {
|
||||||
indentation = Math.min(line.search(/\S|$/), indentation);
|
indentation = line.search(/\S|$/);
|
||||||
}
|
} else {
|
||||||
});
|
indentation = Math.min(line.search(/\S|$/), indentation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Remove extra indentation and return formatted code
|
// 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 { 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';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { angorAnimations } from '@angor/animations';
|
||||||
import { NgTemplateOutlet } from '@angular/common';
|
import { NgTemplateOutlet } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
TemplateRef,
|
TemplateRef,
|
||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { angorAnimations } from '@angor/animations';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'angor-masonry',
|
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 { NgClass, NgTemplateOutlet } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -16,10 +20,6 @@ import {
|
|||||||
RouterLink,
|
RouterLink,
|
||||||
RouterLinkActive,
|
RouterLinkActive,
|
||||||
} from '@angular/router';
|
} 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';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -69,7 +69,7 @@ export class AngorHorizontalNavigationBasicItemComponent
|
|||||||
// "isActiveMatchOptions" or the equivalent form of
|
// "isActiveMatchOptions" or the equivalent form of
|
||||||
// item's "exactMatch" option
|
// item's "exactMatch" option
|
||||||
this.isActiveMatchOptions =
|
this.isActiveMatchOptions =
|
||||||
this.item.isActiveMatchOptions ?? this.item.exactMatch
|
(this.item.isActiveMatchOptions ?? this.item.exactMatch)
|
||||||
? this._angorUtilsService.exactMatchOptions
|
? this._angorUtilsService.exactMatchOptions
|
||||||
: this._angorUtilsService.subsetMatchOptions;
|
: this._angorUtilsService.subsetMatchOptions;
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,10 @@
|
|||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
@if (item.type === '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
|
<angor-horizontal-navigation-divider-item
|
||||||
[item]="item"
|
[item]="item"
|
||||||
[name]="name"
|
[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 { BooleanInput } from '@angular/cdk/coercion';
|
||||||
import { NgClass, NgTemplateOutlet } from '@angular/common';
|
import { NgClass, NgTemplateOutlet } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
@@ -14,11 +19,6 @@ import {
|
|||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatMenu, MatMenuModule } from '@angular/material/menu';
|
import { MatMenu, MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
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';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@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 { NgClass } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -8,9 +11,6 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
} from '@angular/core';
|
} 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';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@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 { NgClass } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -8,9 +11,6 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
} from '@angular/core';
|
} 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';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@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 {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
@@ -10,10 +14,6 @@ import {
|
|||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
inject,
|
inject,
|
||||||
} from '@angular/core';
|
} 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 { ReplaySubject, Subject } from 'rxjs';
|
||||||
import { AngorHorizontalNavigationBasicItemComponent } from './components/basic/basic.component';
|
import { AngorHorizontalNavigationBasicItemComponent } from './components/basic/basic.component';
|
||||||
import { AngorHorizontalNavigationBranchItemComponent } from './components/branch/branch.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 { AngorNavigationItem } from '@angor/components/navigation/navigation.types';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AngorNavigationService {
|
export class AngorNavigationService {
|
||||||
@@ -93,7 +93,10 @@ export class AngorNavigationService {
|
|||||||
* @param id
|
* @param id
|
||||||
* @param navigation
|
* @param navigation
|
||||||
*/
|
*/
|
||||||
getItem(id: string, navigation: AngorNavigationItem[]): AngorNavigationItem | null {
|
getItem(
|
||||||
|
id: string,
|
||||||
|
navigation: AngorNavigationItem[]
|
||||||
|
): AngorNavigationItem | null {
|
||||||
for (const item of navigation) {
|
for (const item of navigation) {
|
||||||
if (item.id === id) return item;
|
if (item.id === id) return item;
|
||||||
if (item.children) {
|
if (item.children) {
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ export interface AngorNavigationItem {
|
|||||||
meta?: any;
|
meta?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AngorVerticalNavigationAppearance = 'default' | 'compact' | 'dense' | 'thin';
|
export type AngorVerticalNavigationAppearance =
|
||||||
|
| 'default'
|
||||||
|
| 'compact'
|
||||||
|
| 'dense'
|
||||||
|
| 'thin';
|
||||||
export type AngorVerticalNavigationMode = 'over' | 'side';
|
export type AngorVerticalNavigationMode = 'over' | 'side';
|
||||||
export type AngorVerticalNavigationPosition = 'left' | 'right';
|
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 { BooleanInput } from '@angular/cdk/coercion';
|
||||||
import { NgClass } from '@angular/common';
|
import { NgClass } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
@@ -14,14 +22,6 @@ import {
|
|||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
import { NavigationEnd, Router } from '@angular/router';
|
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';
|
import { Subject, filter, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@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 { NgClass, NgTemplateOutlet } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -15,10 +19,6 @@ import {
|
|||||||
RouterLink,
|
RouterLink,
|
||||||
RouterLinkActive,
|
RouterLinkActive,
|
||||||
} from '@angular/router';
|
} 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';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -67,7 +67,7 @@ export class AngorVerticalNavigationBasicItemComponent
|
|||||||
// "isActiveMatchOptions" or the equivalent form of
|
// "isActiveMatchOptions" or the equivalent form of
|
||||||
// item's "exactMatch" option
|
// item's "exactMatch" option
|
||||||
this.isActiveMatchOptions =
|
this.isActiveMatchOptions =
|
||||||
this.item.isActiveMatchOptions ?? this.item.exactMatch
|
(this.item.isActiveMatchOptions ?? this.item.exactMatch)
|
||||||
? this._angorUtilsService.exactMatchOptions
|
? this._angorUtilsService.exactMatchOptions
|
||||||
: this._angorUtilsService.subsetMatchOptions;
|
: 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 { BooleanInput } from '@angular/cdk/coercion';
|
||||||
import { NgClass } from '@angular/common';
|
import { NgClass } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
@@ -14,14 +22,6 @@ import {
|
|||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
import { NavigationEnd, Router } from '@angular/router';
|
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';
|
import { Subject, filter, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@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 { NgClass } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -8,9 +11,6 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
} from '@angular/core';
|
} 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';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@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 { BooleanInput } from '@angular/cdk/coercion';
|
||||||
import { NgClass } from '@angular/common';
|
import { NgClass } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
@@ -11,13 +18,6 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
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';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@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 { NgClass } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -8,9 +11,6 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
} from '@angular/core';
|
} 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';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -80,8 +80,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- 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 {
|
import {
|
||||||
animate,
|
animate,
|
||||||
AnimationBuilder,
|
AnimationBuilder,
|
||||||
@@ -30,22 +46,6 @@ import {
|
|||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { NavigationEnd, Router } from '@angular/router';
|
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 {
|
import {
|
||||||
delay,
|
delay,
|
||||||
filter,
|
filter,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class AngorScrollResetDirective implements OnInit, OnDestroy {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this._router.events
|
this._router.events
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(event => event instanceof NavigationEnd),
|
filter((event) => event instanceof NavigationEnd),
|
||||||
takeUntil(this._unsubscribeAll)
|
takeUntil(this._unsubscribeAll)
|
||||||
)
|
)
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
ScrollbarGeometry,
|
||||||
|
ScrollbarPosition,
|
||||||
|
} from '@angor/directives/scrollbar/scrollbar.types';
|
||||||
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
|
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||||
import { Platform } from '@angular/cdk/platform';
|
import { Platform } from '@angular/cdk/platform';
|
||||||
import {
|
import {
|
||||||
@@ -10,10 +14,6 @@ import {
|
|||||||
SimpleChanges,
|
SimpleChanges,
|
||||||
inject,
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
|
||||||
ScrollbarGeometry,
|
|
||||||
ScrollbarPosition,
|
|
||||||
} from '@angor/directives/scrollbar/scrollbar.types';
|
|
||||||
import { merge } from 'lodash-es';
|
import { merge } from 'lodash-es';
|
||||||
import PerfectScrollbar from 'perfect-scrollbar';
|
import PerfectScrollbar from 'perfect-scrollbar';
|
||||||
import { Subject, debounceTime, fromEvent, takeUntil } from 'rxjs';
|
import { Subject, debounceTime, fromEvent, takeUntil } from 'rxjs';
|
||||||
@@ -47,12 +47,20 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if ('angorScrollbar' in changes) {
|
if ('angorScrollbar' in changes) {
|
||||||
this.angorScrollbar = coerceBooleanProperty(changes.angorScrollbar.currentValue);
|
this.angorScrollbar = coerceBooleanProperty(
|
||||||
this.angorScrollbar ? this._initScrollbar() : this._destroyScrollbar();
|
changes.angorScrollbar.currentValue
|
||||||
|
);
|
||||||
|
this.angorScrollbar
|
||||||
|
? this._initScrollbar()
|
||||||
|
: this._destroyScrollbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('angorScrollbarOptions' in changes) {
|
if ('angorScrollbarOptions' in changes) {
|
||||||
this._options = merge({}, this._options, changes.angorScrollbarOptions.currentValue);
|
this._options = merge(
|
||||||
|
{},
|
||||||
|
this._options,
|
||||||
|
changes.angorScrollbarOptions.currentValue
|
||||||
|
);
|
||||||
this._reinitializeScrollbar();
|
this._reinitializeScrollbar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +100,10 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
|||||||
|
|
||||||
position(absolute: boolean = false): ScrollbarPosition {
|
position(absolute: boolean = false): ScrollbarPosition {
|
||||||
if (!absolute && this._ps) {
|
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 {
|
} else {
|
||||||
return new ScrollbarPosition(
|
return new ScrollbarPosition(
|
||||||
this._elementRef.nativeElement.scrollLeft,
|
this._elementRef.nativeElement.scrollLeft,
|
||||||
@@ -115,7 +126,6 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
scrollToX(x: number, speed?: number): void {
|
scrollToX(x: number, speed?: number): void {
|
||||||
this.animateScrolling('scrollLeft', x, speed);
|
this.animateScrolling('scrollLeft', x, speed);
|
||||||
}
|
}
|
||||||
@@ -129,7 +139,9 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom(offset: number = 0, speed?: number): void {
|
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);
|
this.animateScrolling('scrollTop', top - offset, speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +150,9 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollToRight(offset: number = 0, speed?: number): void {
|
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);
|
this.animateScrolling('scrollLeft', left - offset, speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,14 +166,29 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
|||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
const elementPos = element.getBoundingClientRect();
|
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')) {
|
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')) {
|
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 {
|
private _initScrollbar(): void {
|
||||||
if (this._ps || this._platform.ANDROID || this._platform.IOS || !this._platform.isBrowser) return;
|
if (
|
||||||
this._ps = new PerfectScrollbar(this._elementRef.nativeElement, { ...this._options });
|
this._ps ||
|
||||||
|
this._platform.ANDROID ||
|
||||||
|
this._platform.IOS ||
|
||||||
|
!this._platform.isBrowser
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
this._ps = new PerfectScrollbar(this._elementRef.nativeElement, {
|
||||||
|
...this._options,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _destroyScrollbar(): void {
|
private _destroyScrollbar(): void {
|
||||||
@@ -198,7 +235,8 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
|||||||
ignoreVisible: boolean,
|
ignoreVisible: boolean,
|
||||||
speed?: number
|
speed?: number
|
||||||
): void {
|
): void {
|
||||||
if (ignoreVisible && elementPos <= scrollerPos - Math.abs(offset)) return;
|
if (ignoreVisible && elementPos <= scrollerPos - Math.abs(offset))
|
||||||
|
return;
|
||||||
|
|
||||||
const currentPos = this._elementRef.nativeElement[target];
|
const currentPos = this._elementRef.nativeElement[target];
|
||||||
const position = elementPos - scrollerPos + currentPos;
|
const position = elementPos - scrollerPos + currentPos;
|
||||||
@@ -213,7 +251,9 @@ export class AngorScrollbarDirective implements OnChanges, OnInit, OnDestroy {
|
|||||||
|
|
||||||
const step = (newTimestamp: number) => {
|
const step = (newTimestamp: number) => {
|
||||||
scrollCount += Math.PI / (speed / (newTimestamp - oldTimestamp));
|
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 (this._elementRef.nativeElement[target] === oldValue) {
|
||||||
if (scrollCount >= Math.PI) {
|
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 {
|
import {
|
||||||
HttpErrorResponse,
|
HttpErrorResponse,
|
||||||
HttpEvent,
|
HttpEvent,
|
||||||
@@ -6,8 +8,6 @@ import {
|
|||||||
HttpResponse,
|
HttpResponse,
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import { inject } from '@angular/core';
|
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 { Observable, delay, of, switchMap, throwError } from 'rxjs';
|
||||||
import { AngorMockApiMethods } from './mock-api.types';
|
import { AngorMockApiMethods } from './mock-api.types';
|
||||||
|
|
||||||
@@ -43,11 +43,14 @@ export const mockApiInterceptor = (
|
|||||||
switchMap((response) => {
|
switchMap((response) => {
|
||||||
// If no response is returned, generate a 404 error
|
// If no response is returned, generate a 404 error
|
||||||
if (!response) {
|
if (!response) {
|
||||||
return throwError(() => new HttpErrorResponse({
|
return throwError(
|
||||||
error: 'NOT FOUND',
|
() =>
|
||||||
status: 404,
|
new HttpErrorResponse({
|
||||||
statusText: 'NOT FOUND',
|
error: 'NOT FOUND',
|
||||||
}));
|
status: 404,
|
||||||
|
statusText: 'NOT FOUND',
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the response data (status and body)
|
// 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 status code is between 200 and 300, return a successful response
|
||||||
if (data.status >= 200 && data.status < 300) {
|
if (data.status >= 200 && data.status < 300) {
|
||||||
return of(new HttpResponse({
|
return of(
|
||||||
body: data.body,
|
new HttpResponse({
|
||||||
status: data.status,
|
body: data.body,
|
||||||
statusText: 'OK',
|
status: data.status,
|
||||||
}));
|
statusText: 'OK',
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other status codes, throw an error response
|
// For other status codes, throw an error response
|
||||||
return throwError(() => new HttpErrorResponse({
|
return throwError(
|
||||||
error: data.body?.error,
|
() =>
|
||||||
status: data.status,
|
new HttpErrorResponse({
|
||||||
statusText: 'ERROR',
|
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 { AngorMockApiReplyCallback } from '@angor/lib/mock-api/mock-api.types';
|
||||||
|
import { HttpRequest } from '@angular/common/http';
|
||||||
import { Observable, of, take, throwError } from 'rxjs';
|
import { Observable, of, take, throwError } from 'rxjs';
|
||||||
|
|
||||||
export class AngorMockApiHandler {
|
export class AngorMockApiHandler {
|
||||||
@@ -17,7 +17,10 @@ export class AngorMockApiHandler {
|
|||||||
* @param url - The URL for the mock API handler
|
* @param url - The URL for the mock API handler
|
||||||
* @param delay - Optional delay for the response
|
* @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.
|
* Getter for the response observable.
|
||||||
@@ -27,12 +30,16 @@ export class AngorMockApiHandler {
|
|||||||
get response(): Observable<any> {
|
get response(): Observable<any> {
|
||||||
// Check if the execution limit has been reached
|
// Check if the execution limit has been reached
|
||||||
if (this._replyCount > 0 && this._replyCount <= this._replied) {
|
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
|
// Ensure the response callback exists
|
||||||
if (!this._reply) {
|
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
|
// 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 { AngorMockApiHandler } from '@angor/lib/mock-api/mock-api.request-handler';
|
||||||
import { AngorMockApiMethods } from '@angor/lib/mock-api/mock-api.types';
|
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' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AngorMockApiService {
|
export class AngorMockApiService {
|
||||||
private readonly _handlers: Record<AngorMockApiMethods, Map<string, AngorMockApiHandler>> = {
|
private readonly _handlers: Record<
|
||||||
|
AngorMockApiMethods,
|
||||||
|
Map<string, AngorMockApiHandler>
|
||||||
|
> = {
|
||||||
get: new Map<string, AngorMockApiHandler>(),
|
get: new Map<string, AngorMockApiHandler>(),
|
||||||
post: new Map<string, AngorMockApiHandler>(),
|
post: new Map<string, AngorMockApiHandler>(),
|
||||||
patch: new Map<string, AngorMockApiHandler>(),
|
patch: new Map<string, AngorMockApiHandler>(),
|
||||||
@@ -26,24 +29,36 @@ export class AngorMockApiService {
|
|||||||
findHandler(
|
findHandler(
|
||||||
method: AngorMockApiMethods,
|
method: AngorMockApiMethods,
|
||||||
url: string
|
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 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) {
|
for (const [handlerUrl, handler] of handlers) {
|
||||||
const handlerUrlParts = handlerUrl.split('/');
|
const handlerUrlParts = handlerUrl.split('/');
|
||||||
|
|
||||||
if (urlParts.length === handlerUrlParts.length) {
|
if (urlParts.length === handlerUrlParts.length) {
|
||||||
const matches = handlerUrlParts.every((part, index) =>
|
const matches = handlerUrlParts.every(
|
||||||
part.startsWith(':') || part === urlParts[index]
|
(part, index) =>
|
||||||
|
part.startsWith(':') || part === urlParts[index]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (matches) {
|
if (matches) {
|
||||||
matchingHandler.handler = handler;
|
matchingHandler.handler = handler;
|
||||||
matchingHandler.urlParams = fromPairs(
|
matchingHandler.urlParams = fromPairs(
|
||||||
handlerUrlParts
|
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)
|
.filter(Boolean)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -150,7 +165,11 @@ export class AngorMockApiService {
|
|||||||
* @param delay - (Optional) Delay for the response in milliseconds
|
* @param delay - (Optional) Delay for the response in milliseconds
|
||||||
* @returns An instance of AngorMockApiHandler
|
* @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);
|
const handler = new AngorMockApiHandler(url, delay);
|
||||||
this._handlers[method].set(url, handler);
|
this._handlers[method].set(url, handler);
|
||||||
return handler;
|
return handler;
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ export class AngorFindByKeyPipe implements PipeTransform {
|
|||||||
* @param source The array of objects to search within.
|
* @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.
|
* @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 value is an array of strings, map each to its corresponding object in the source.
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value.map((item) =>
|
return value.map((item) =>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { inject, Injectable } from '@angular/core';
|
|
||||||
import { ANGOR_CONFIG } from '@angor/services/config/config.constants';
|
import { ANGOR_CONFIG } from '@angor/services/config/config.constants';
|
||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { merge } from 'lodash-es';
|
import { merge } from 'lodash-es';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AngorConfigService {
|
export class AngorConfigService {
|
||||||
private readonly _defaultConfig = inject(ANGOR_CONFIG);
|
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.
|
* 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.
|
* This ensures consistency when defining or updating app settings.
|
||||||
*/
|
*/
|
||||||
export interface AngorConfig {
|
export interface AngorConfig {
|
||||||
layout: string; // Layout type (e.g., 'vertical', 'horizontal')
|
layout: string; // Layout type (e.g., 'vertical', 'horizontal')
|
||||||
scheme: Scheme; // Color scheme: 'auto', 'dark', or 'light'
|
scheme: Scheme; // Color scheme: 'auto', 'dark', or 'light'
|
||||||
screens: Screens; // Screen breakpoints, e.g., { 'xs': '600px', ... }
|
screens: Screens; // Screen breakpoints, e.g., { 'xs': '600px', ... }
|
||||||
theme: Theme; // Active theme identifier, e.g., 'theme-default'
|
theme: Theme; // Active theme identifier, e.g., 'theme-default'
|
||||||
themes: Themes; // List of available themes, each with an id and name
|
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 { AngorConfirmationConfig } from '@angor/services/confirmation/confirmation.types';
|
||||||
import { AngorConfirmationDialogComponent } from '@angor/services/confirmation/dialog/dialog.component';
|
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';
|
import { merge } from 'lodash-es';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { AngorConfirmationConfig } from '@angor/services/confirmation/confirmation.types';
|
||||||
import { NgClass } from '@angular/common';
|
import { NgClass } from '@angular/common';
|
||||||
import { Component, ViewEncapsulation, inject } from '@angular/core';
|
import { Component, ViewEncapsulation, inject } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { AngorConfirmationConfig } from '@angor/services/confirmation/confirmation.types';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'angor-confirmation-dialog',
|
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 { HttpEvent, HttpHandlerFn, HttpRequest } from '@angular/common/http';
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { AngorLoadingService } from '@angor/services/loading/loading.service';
|
|
||||||
import { Observable, finalize, take } from 'rxjs';
|
import { Observable, finalize, take } from 'rxjs';
|
||||||
|
|
||||||
export const angorLoadingInterceptor = (
|
export const angorLoadingInterceptor = (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { AngorConfigService } from '@angor/services/config';
|
||||||
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
|
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { AngorConfigService } from '@angor/services/config';
|
|
||||||
import { fromPairs } from 'lodash-es';
|
import { fromPairs } from 'lodash-es';
|
||||||
import { Observable, ReplaySubject, map, switchMap } from 'rxjs';
|
import { Observable, ReplaySubject, map, switchMap } from 'rxjs';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
/* ----------------------------------------------------------------------------------------------------- */
|
|
||||||
/* @ Example viewer
|
|
||||||
/* ----------------------------------------------------------------------------------------------------- */
|
|
||||||
.example-viewer {
|
.example-viewer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ $dark-base: (
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
/* Include the core Angular Material styles */
|
/* Include the core Angular Material styles */
|
||||||
@include mat.core();
|
@include mat.core();
|
||||||
|
|
||||||
|
|||||||
@@ -13,17 +13,6 @@ const jsonToSassMap = require(
|
|||||||
path.resolve(__dirname, '../utils/json-to-sass-map')
|
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) => {
|
const normalizeTheme = (theme) => {
|
||||||
return _.fromPairs(
|
return _.fromPairs(
|
||||||
_.map(
|
_.map(
|
||||||
@@ -43,17 +32,10 @@ const normalizeTheme = (theme) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
// @ ANGOR TailwindCSS Main Plugin
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
const theming = plugin.withOptions(
|
const theming = plugin.withOptions(
|
||||||
(options) =>
|
(options) =>
|
||||||
({ addComponents, e, theme }) => {
|
({ 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(
|
const userThemes = _.fromPairs(
|
||||||
_.map(options.themes, (theme, themeName) => [
|
_.map(options.themes, (theme, themeName) => [
|
||||||
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(
|
let themes = _.fromPairs(
|
||||||
_.map(userThemes, (theme, themeName) => [
|
_.map(userThemes, (theme, themeName) => [
|
||||||
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(
|
themes = _.fromPairs(
|
||||||
_.map(themes, (theme, themeName) => [
|
_.map(themes, (theme, themeName) => [
|
||||||
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(
|
themes = _.fromPairs(
|
||||||
_.map(themes, (theme, themeName) => [
|
_.map(themes, (theme, themeName) => [
|
||||||
themeName,
|
themeName,
|
||||||
@@ -119,18 +89,15 @@ const theming = plugin.withOptions(
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Generate the SASS map using the themes object */
|
|
||||||
const sassMap = jsonToSassMap(
|
const sassMap = jsonToSassMap(
|
||||||
JSON.stringify({ 'user-themes': themes })
|
JSON.stringify({ 'user-themes': themes })
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Get the file path */
|
|
||||||
const filename = path.resolve(
|
const filename = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../../styles/user-themes.scss'
|
'../../styles/user-themes.scss'
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Read the file and get its data */
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = fs.readFileSync(filename, { encoding: 'utf8' });
|
data = fs.readFileSync(filename, { encoding: 'utf8' });
|
||||||
@@ -138,7 +105,6 @@ const theming = plugin.withOptions(
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Write the file if the map has been changed */
|
|
||||||
if (data !== sassMap) {
|
if (data !== sassMap) {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(filename, sassMap, { encoding: 'utf8' });
|
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(
|
addComponents(
|
||||||
_.fromPairs(
|
_.fromPairs(
|
||||||
_.map(options.themes, (theme, themeName) => [
|
_.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(
|
const schemeCustomProps = _.map(
|
||||||
['light', 'dark'],
|
['light', 'dark'],
|
||||||
(colorScheme) => {
|
(colorScheme) => {
|
||||||
@@ -234,31 +193,9 @@ const theming = plugin.withOptions(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
[isDark ? darkSchemeSelectors : lightSchemeSelectors]: {
|
[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' } : {}),
|
...(!isDark ? { '--is-dark': 'false' } : {}),
|
||||||
|
|
||||||
/* Generate custom properties from customProps */
|
|
||||||
..._.fromPairs(
|
..._.fromPairs(
|
||||||
_.flatten(
|
_.flatten(
|
||||||
_.map(background, (value, key) => [
|
_.map(background, (value, key) => [
|
||||||
@@ -287,7 +224,6 @@ const theming = plugin.withOptions(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const schemeUtilities = (() => {
|
const schemeUtilities = (() => {
|
||||||
/* Generate general styles & utilities */
|
|
||||||
return {};
|
return {};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -298,11 +234,6 @@ const theming = plugin.withOptions(
|
|||||||
return {
|
return {
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
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(
|
colors: _.fromPairs(
|
||||||
_.flatten(
|
_.flatten(
|
||||||
_.map(
|
_.map(
|
||||||
@@ -398,7 +329,6 @@ const theming = plugin.withOptions(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
const plugin = require('tailwindcss/plugin');
|
const plugin = require('tailwindcss/plugin');
|
||||||
|
|
||||||
module.exports = plugin(({ addComponents }) => {
|
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({
|
addComponents({
|
||||||
'.mat-icon': {
|
'.mat-icon': {
|
||||||
'--tw-text-opacity': '1',
|
'--tw-text-opacity': '1',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto; // Flex-grow, flex-shrink, and basis set to auto
|
flex: 1 1 auto; // Flex-grow, flex-shrink, and basis set to auto
|
||||||
width: 100%; // Full width of the container
|
width: 100%; // Full width of the container
|
||||||
height: 100%; // Full height 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 { 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 { LuxonDateAdapter } from '@angular/material-luxon-adapter';
|
||||||
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
|
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
|
||||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||||
@@ -9,20 +15,18 @@ import {
|
|||||||
withInMemoryScrolling,
|
withInMemoryScrolling,
|
||||||
withPreloading,
|
withPreloading,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { provideAngor } from '@angor';
|
import { provideServiceWorker } from '@angular/service-worker';
|
||||||
import { TranslocoService, provideTransloco } from '@ngneat/transloco';
|
import { TranslocoService, provideTransloco } from '@ngneat/transloco';
|
||||||
|
import { WebLNProvider } from '@webbtc/webln-types';
|
||||||
import { appRoutes } from 'app/app.routes';
|
import { appRoutes } from 'app/app.routes';
|
||||||
import { provideIcons } from 'app/core/icons/icons.provider';
|
import { provideIcons } from 'app/core/icons/icons.provider';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { TranslocoHttpLoader } from './core/transloco/transloco.http-loader';
|
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 { navigationServices } from './layout/navigation/navigation.services';
|
||||||
import { WebLNProvider } from '@webbtc/webln-types';
|
import { HashService } from './services/hash.service';
|
||||||
import { NostrWindow } from './types/nostr';
|
import { NostrWindow } from './types/nostr';
|
||||||
|
|
||||||
export function initializeApp(hashService: HashService) {
|
export function initializeApp(hashService: HashService) {
|
||||||
console.log('initializeApp. Getting hashService.load.');
|
|
||||||
return (): Promise<void> => hashService.load();
|
return (): Promise<void> => hashService.load();
|
||||||
}
|
}
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
@@ -31,7 +35,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
provideHttpClient(),
|
provideHttpClient(),
|
||||||
provideServiceWorker('ngsw-worker.js', {
|
provideServiceWorker('ngsw-worker.js', {
|
||||||
enabled: !isDevMode(),
|
enabled: !isDevMode(),
|
||||||
registrationStrategy: 'registerWhenStable:30000'
|
registrationStrategy: 'registerWhenStable:30000',
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
@@ -54,12 +58,12 @@ export const appConfig: ApplicationConfig = {
|
|||||||
provide: MAT_DATE_FORMATS,
|
provide: MAT_DATE_FORMATS,
|
||||||
useValue: {
|
useValue: {
|
||||||
parse: {
|
parse: {
|
||||||
dateInput: 'D', // Date format for parsing
|
dateInput: 'D', // Date format for parsing
|
||||||
},
|
},
|
||||||
display: {
|
display: {
|
||||||
dateInput: 'DDD', // Date format for input display
|
dateInput: 'DDD', // Date format for input display
|
||||||
monthYearLabel: 'LLL yyyy', // Format for month-year labels
|
monthYearLabel: 'LLL yyyy', // Format for month-year labels
|
||||||
dateA11yLabel: 'DD', // Accessible format for dates
|
dateA11yLabel: 'DD', // Accessible format for dates
|
||||||
monthYearA11yLabel: 'LLLL yyyy', // Accessible format for month-year
|
monthYearA11yLabel: 'LLLL yyyy', // Accessible format for month-year
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -72,7 +76,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
{
|
{
|
||||||
id: 'en',
|
id: 'en',
|
||||||
label: 'English',
|
label: 'English',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
defaultLang: 'en',
|
defaultLang: 'en',
|
||||||
fallbackLang: 'en',
|
fallbackLang: 'en',
|
||||||
@@ -121,13 +125,11 @@ export const appConfig: ApplicationConfig = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
webln?: WebLNProvider;
|
webln?: WebLNProvider;
|
||||||
nostr?: NostrWindow;
|
nostr?: NostrWindow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import { forkJoin } from 'rxjs';
|
|||||||
*/
|
*/
|
||||||
export const initialDataResolver = () => {
|
export const initialDataResolver = () => {
|
||||||
const navigationService = inject(NavigationService);
|
const navigationService = inject(NavigationService);
|
||||||
const quickChatService = inject(QuickChatService);
|
const quickChatService = inject(QuickChatService);
|
||||||
|
|
||||||
// Combine API calls into a single observable
|
// Combine API calls into a single observable
|
||||||
return forkJoin([
|
return forkJoin([
|
||||||
navigationService.get(), // Fetch navigation data
|
navigationService.get(), // Fetch navigation data
|
||||||
// quickChatService.getChats(), // Fetch chat data
|
// quickChatService.getChats(), // Fetch chat data
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import { Route } from '@angular/router';
|
import { Route } from '@angular/router';
|
||||||
import { initialDataResolver } from 'app/app.resolvers';
|
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';
|
import { authGuard } from './core/auth/auth.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application routes configuration
|
* Application routes configuration
|
||||||
*/
|
*/
|
||||||
export const appRoutes: Route[] = [
|
export const appRoutes: Route[] = [
|
||||||
|
|
||||||
// Redirect root path to '/explore'
|
// Redirect root path to '/explore'
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
redirectTo: 'home'
|
redirectTo: 'home',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'project/:pubkey',
|
path: 'project/:pubkey',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
redirectTo: 'explore'
|
redirectTo: 'explore',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Redirect login user to '/explore'
|
// Redirect login user to '/explore'
|
||||||
{
|
{
|
||||||
path: 'login-redirect',
|
path: 'login-redirect',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
redirectTo: 'explore'
|
redirectTo: 'explore',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Routes for guests
|
// Routes for guests
|
||||||
@@ -36,13 +35,15 @@ export const appRoutes: Route[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
loadChildren: () => import('app/components/auth/login/login.routes')
|
loadChildren: () =>
|
||||||
|
import('app/components/auth/login/login.routes'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'register',
|
path: 'register',
|
||||||
loadChildren: () => import('app/components/auth/register/register.routes')
|
loadChildren: () =>
|
||||||
}
|
import('app/components/auth/register/register.routes'),
|
||||||
]
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Routes for authenticated users
|
// Routes for authenticated users
|
||||||
@@ -55,13 +56,12 @@ export const appRoutes: Route[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'logout',
|
path: 'logout',
|
||||||
loadChildren: () => import('app/components/auth/logout/logout.routes')
|
loadChildren: () =>
|
||||||
}
|
import('app/components/auth/logout/logout.routes'),
|
||||||
]
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Authenticated routes for Angor
|
// Authenticated routes for Angor
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
@@ -72,41 +72,49 @@ export const appRoutes: Route[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'home',
|
path: 'home',
|
||||||
loadChildren: () => import('app/components/home/home.routes')
|
loadChildren: () => import('app/components/home/home.routes'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'explore',
|
path: 'explore',
|
||||||
loadChildren: () => import('app/components/explore/explore.routes')
|
loadChildren: () =>
|
||||||
|
import('app/components/explore/explore.routes'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'profile',
|
path: 'profile',
|
||||||
loadChildren: () => import('app/components/profile/profile.routes')
|
loadChildren: () =>
|
||||||
|
import('app/components/profile/profile.routes'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'profile/:pubkey',
|
path: 'profile/:pubkey',
|
||||||
loadChildren: () => import('app/components/profile/profile.routes')
|
loadChildren: () =>
|
||||||
|
import('app/components/profile/profile.routes'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
loadChildren: () => import('app/components/settings/settings.routes')
|
loadChildren: () =>
|
||||||
|
import('app/components/settings/settings.routes'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings/:id',
|
path: 'settings/:id',
|
||||||
loadChildren: () => import('app/components/settings/settings.routes')
|
loadChildren: () =>
|
||||||
|
import('app/components/settings/settings.routes'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'chat',
|
path: 'chat',
|
||||||
loadChildren: () => import('app/components/chat/chat.routes')
|
loadChildren: () => import('app/components/chat/chat.routes'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '404-not-found',
|
path: '404-not-found',
|
||||||
pathMatch: 'full',
|
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: '**',
|
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
|
<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">
|
<div class="mx-auto w-full max-w-80 sm:mx-0 sm:w-80">
|
||||||
<!-- Title -->
|
<!-- 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
|
Login
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-0.5 flex items-baseline font-medium">
|
<div class="mt-0.5 flex items-baseline font-medium">
|
||||||
<div>Don't have an account?</div>
|
<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>
|
</div>
|
||||||
|
|
||||||
<angor-alert *ngIf="showSecAlert" class="mt-8" [appearance]="'outline'" [showIcon]="false" [type]="secAlert.type"
|
<angor-alert
|
||||||
[@shake]="secAlert.type === 'error'">
|
*ngIf="showSecAlert"
|
||||||
|
class="mt-8"
|
||||||
|
[appearance]="'outline'"
|
||||||
|
[showIcon]="false"
|
||||||
|
[type]="secAlert.type"
|
||||||
|
[@shake]="secAlert.type === 'error'"
|
||||||
|
>
|
||||||
{{ secAlert.message }}
|
{{ secAlert.message }}
|
||||||
</angor-alert>
|
</angor-alert>
|
||||||
|
|
||||||
@@ -26,8 +41,16 @@
|
|||||||
|
|
||||||
<!-- extension login buttons -->
|
<!-- extension login buttons -->
|
||||||
<div class="mt-8 flex items-center space-x-4">
|
<div class="mt-8 flex items-center space-x-4">
|
||||||
<button class="flex-auto space-x-2" type="button" mat-stroked-button (click)="loginWithNostrExtension()">
|
<button
|
||||||
<mat-icon class="icon-size-5" [svgIcon]="'feather:zap'"></mat-icon>
|
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>
|
<span>Login with Nostr Extension</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +63,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Login form with Secret Key -->
|
<!-- Login form with Secret Key -->
|
||||||
<form class="mt-8" [formGroup]="SecretKeyLoginForm" (ngSubmit)="loginWithSecretKey()">
|
<form
|
||||||
|
class="mt-8"
|
||||||
|
[formGroup]="SecretKeyLoginForm"
|
||||||
|
(ngSubmit)="loginWithSecretKey()"
|
||||||
|
>
|
||||||
<!-- secret key field -->
|
<!-- secret key field -->
|
||||||
<div class="mt-8 flex items-center">
|
<div class="mt-8 flex items-center">
|
||||||
<div class="mt-px flex-auto border-t"></div>
|
<div class="mt-px flex-auto border-t"></div>
|
||||||
@@ -49,8 +76,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<mat-form-field class="w-full">
|
<mat-form-field class="w-full">
|
||||||
<mat-label>Secret Key</mat-label>
|
<mat-label>Secret Key</mat-label>
|
||||||
<input matInput formControlName="secretKey" autocomplete="secretKey" />
|
<input
|
||||||
@if (SecretKeyLoginForm.get('secretKey').hasError('required')) {
|
matInput
|
||||||
|
formControlName="secretKey"
|
||||||
|
autocomplete="secretKey"
|
||||||
|
/>
|
||||||
|
@if (
|
||||||
|
SecretKeyLoginForm.get('secretKey').hasError('required')
|
||||||
|
) {
|
||||||
<mat-error> Secret key is required </mat-error>
|
<mat-error> Secret key is required </mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@@ -58,47 +91,94 @@
|
|||||||
<!-- Password field -->
|
<!-- Password field -->
|
||||||
<mat-form-field class="w-full">
|
<mat-form-field class="w-full">
|
||||||
<mat-label>Password</mat-label>
|
<mat-label>Password</mat-label>
|
||||||
<input matInput type="password" [formControlName]="'password'" autocomplete="current-password-seckey" #secretPasswordField />
|
<input
|
||||||
<button mat-icon-button type="button" (click)="
|
matInput
|
||||||
secretPasswordField.type === 'password'
|
type="password"
|
||||||
? (secretPasswordField.type = 'text')
|
[formControlName]="'password'"
|
||||||
: (secretPasswordField.type = 'password')
|
autocomplete="current-password-seckey"
|
||||||
" matSuffix>
|
#secretPasswordField
|
||||||
<mat-icon *ngIf="secretPasswordField.type === 'password'" class="icon-size-5"
|
/>
|
||||||
[svgIcon]="'heroicons_solid:eye'"></mat-icon>
|
<button
|
||||||
<mat-icon *ngIf="secretPasswordField.type === 'text'" class="icon-size-5"
|
mat-icon-button
|
||||||
[svgIcon]="'heroicons_solid:eye-slash'"></mat-icon>
|
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>
|
</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-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<!-- Submit button -->
|
<!-- Submit button -->
|
||||||
<button class="angor-mat-button-large mt-6 w-full" mat-flat-button color="primary"
|
<button
|
||||||
[disabled]="SecretKeyLoginForm.invalid">
|
class="angor-mat-button-large mt-6 w-full"
|
||||||
|
mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
[disabled]="SecretKeyLoginForm.invalid"
|
||||||
|
>
|
||||||
<span *ngIf="!loading">Login</span>
|
<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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
<div class="mt-8 flex items-center">
|
<div class="mt-8 flex items-center">
|
||||||
<div class="mt-px flex-auto border-t"></div>
|
<div class="mt-px flex-auto border-t"></div>
|
||||||
<div class="text-secondary mx-2">Or enter menemonic</div>
|
<div class="text-secondary mx-2">Or enter menemonic</div>
|
||||||
<div class="mt-px flex-auto border-t"></div>
|
<div class="mt-px flex-auto border-t"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<angor-alert *ngIf="showMenemonicAlert" class="mt-8" [appearance]="'outline'" [showIcon]="false" [type]="menemonicAlert.type"
|
<angor-alert
|
||||||
[@shake]="menemonicAlert.type === 'error'">
|
*ngIf="showMenemonicAlert"
|
||||||
|
class="mt-8"
|
||||||
|
[appearance]="'outline'"
|
||||||
|
[showIcon]="false"
|
||||||
|
[type]="menemonicAlert.type"
|
||||||
|
[@shake]="menemonicAlert.type === 'error'"
|
||||||
|
>
|
||||||
{{ menemonicAlert.message }}
|
{{ menemonicAlert.message }}
|
||||||
</angor-alert>
|
</angor-alert>
|
||||||
<!-- Login form with Menemonic -->
|
<!-- Login form with Menemonic -->
|
||||||
<form class="mt-8" [formGroup]="MenemonicLoginForm" (ngSubmit)="loginWithMenemonic()">
|
<form
|
||||||
|
class="mt-8"
|
||||||
|
[formGroup]="MenemonicLoginForm"
|
||||||
|
(ngSubmit)="loginWithMenemonic()"
|
||||||
|
>
|
||||||
<!-- Menemonic field -->
|
<!-- Menemonic field -->
|
||||||
<mat-form-field class="w-full">
|
<mat-form-field class="w-full">
|
||||||
<mat-label>Menemonic</mat-label>
|
<mat-label>Menemonic</mat-label>
|
||||||
<input matInput formControlName="menemonic" autocomplete="menemonic" />
|
<input
|
||||||
@if (MenemonicLoginForm.get('menemonic').hasError('required')) {
|
matInput
|
||||||
|
formControlName="menemonic"
|
||||||
|
autocomplete="menemonic"
|
||||||
|
/>
|
||||||
|
@if (
|
||||||
|
MenemonicLoginForm.get('menemonic').hasError('required')
|
||||||
|
) {
|
||||||
<mat-error> Menemonic is required </mat-error>
|
<mat-error> Menemonic is required </mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@@ -106,66 +186,156 @@
|
|||||||
<!-- Passphrase field -->
|
<!-- Passphrase field -->
|
||||||
<mat-form-field class="w-full">
|
<mat-form-field class="w-full">
|
||||||
<mat-label>Passphrase (Optional)</mat-label>
|
<mat-label>Passphrase (Optional)</mat-label>
|
||||||
<input matInput type="password" [formControlName]="'passphrase'" autocomplete="current-passphrase-menemonic"
|
<input
|
||||||
#passphraseField />
|
matInput
|
||||||
<button mat-icon-button type="button" (click)="
|
type="password"
|
||||||
passphraseField.type === 'password'
|
[formControlName]="'passphrase'"
|
||||||
? (passphraseField.type = 'text')
|
autocomplete="current-passphrase-menemonic"
|
||||||
: (passphraseField.type = 'password')
|
#passphraseField
|
||||||
" matSuffix>
|
/>
|
||||||
<mat-icon *ngIf="passphraseField.type === 'password'" class="icon-size-5" [svgIcon]="'heroicons_solid:eye'"></mat-icon>
|
<button
|
||||||
<mat-icon *ngIf="passphraseField.type === 'text'" class="icon-size-5" [svgIcon]="'heroicons_solid:eye-slash'"></mat-icon>
|
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>
|
</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-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<!-- Password field -->
|
<!-- Password field -->
|
||||||
<mat-form-field class="w-full">
|
<mat-form-field class="w-full">
|
||||||
<mat-label>Password</mat-label>
|
<mat-label>Password</mat-label>
|
||||||
<input matInput type="password" [formControlName]="'password'" autocomplete="current-password-menemonic"
|
<input
|
||||||
#menemonicPasswordField />
|
matInput
|
||||||
<button mat-icon-button type="button" (click)="
|
type="password"
|
||||||
menemonicPasswordField.type === 'password'
|
[formControlName]="'password'"
|
||||||
? (menemonicPasswordField.type = 'text')
|
autocomplete="current-password-menemonic"
|
||||||
: (menemonicPasswordField.type = 'password')
|
#menemonicPasswordField
|
||||||
" matSuffix>
|
/>
|
||||||
<mat-icon *ngIf="menemonicPasswordField.type === 'password'" class="icon-size-5" [svgIcon]="'heroicons_solid:eye'"></mat-icon>
|
<button
|
||||||
<mat-icon *ngIf="menemonicPasswordField.type === 'text'" class="icon-size-5" [svgIcon]="'heroicons_solid:eye-slash'"></mat-icon>
|
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>
|
</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-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<!-- Submit button -->
|
<!-- Submit button -->
|
||||||
<button class="angor-mat-button-large mt-6 w-full" mat-flat-button color="primary"
|
<button
|
||||||
[disabled]="MenemonicLoginForm.invalid">
|
class="angor-mat-button-large mt-6 w-full"
|
||||||
|
mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
[disabled]="MenemonicLoginForm.invalid"
|
||||||
|
>
|
||||||
<span *ngIf="!loading">Login</span>
|
<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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
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%"
|
<svg
|
||||||
preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
|
class="pointer-events-none absolute inset-0"
|
||||||
<g class="text-gray-700 opacity-25" fill="none" stroke="currentColor" stroke-width="100">
|
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="196" cy="23"></circle>
|
||||||
<circle r="234" cx="790" cy="491"></circle>
|
<circle r="234" cx="790" cy="491"></circle>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<svg class="absolute -top-16 -right-16 text-gray-700" viewBox="0 0 220 192" width="220" height="192"
|
<svg
|
||||||
fill="none">
|
class="absolute -right-16 -top-16 text-gray-700"
|
||||||
|
viewBox="0 0 220 192"
|
||||||
|
width="220"
|
||||||
|
height="192"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="837c3e70-6c3a-44e6-8854-cc48c737b659" x="0" y="0" width="20" height="20"
|
<pattern
|
||||||
patternUnits="userSpaceOnUse">
|
id="837c3e70-6c3a-44e6-8854-cc48c737b659"
|
||||||
<rect x="0" y="0" width="4" height="4" fill="currentColor"></rect>
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="4"
|
||||||
|
height="4"
|
||||||
|
fill="currentColor"
|
||||||
|
></rect>
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</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>
|
</svg>
|
||||||
<!-- Background and Content -->
|
<!-- Background and Content -->
|
||||||
<div class="relative z-10 w-full max-w-2xl">
|
<div class="relative z-10 w-full max-w-2xl">
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { AngorAlertComponent } from '@angor/components/alert';
|
import { AngorAlertComponent } from '@angor/components/alert';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, OnInit } from '@angular/core';
|
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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
@@ -26,7 +32,7 @@ import { SignerService } from 'app/services/signer.service';
|
|||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
CommonModule
|
CommonModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit {
|
export class LoginComponent implements OnInit {
|
||||||
@@ -41,15 +47,15 @@ export class LoginComponent implements OnInit {
|
|||||||
loading = false;
|
loading = false;
|
||||||
isInstalledExtension = false;
|
isInstalledExtension = false;
|
||||||
privateKey: Uint8Array = new Uint8Array();
|
privateKey: Uint8Array = new Uint8Array();
|
||||||
publicKey: string = "";
|
publicKey: string = '';
|
||||||
npub: string = "";
|
npub: string = '';
|
||||||
nsec: string = "";
|
nsec: string = '';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _formBuilder: FormBuilder,
|
private _formBuilder: FormBuilder,
|
||||||
private _router: Router,
|
private _router: Router,
|
||||||
private _signerService: SignerService
|
private _signerService: SignerService
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.initializeForms();
|
this.initializeForms();
|
||||||
@@ -59,20 +65,25 @@ export class LoginComponent implements OnInit {
|
|||||||
private initializeForms(): void {
|
private initializeForms(): void {
|
||||||
this.SecretKeyLoginForm = this._formBuilder.group({
|
this.SecretKeyLoginForm = this._formBuilder.group({
|
||||||
secretKey: ['', [Validators.required, Validators.minLength(3)]],
|
secretKey: ['', [Validators.required, Validators.minLength(3)]],
|
||||||
password: ['', Validators.required]
|
password: ['', Validators.required],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.MenemonicLoginForm = this._formBuilder.group({
|
this.MenemonicLoginForm = this._formBuilder.group({
|
||||||
menemonic: ['', [Validators.required, Validators.minLength(3)]],
|
menemonic: ['', [Validators.required, Validators.minLength(3)]],
|
||||||
passphrase: [''], // Passphrase is optional
|
passphrase: [''], // Passphrase is optional
|
||||||
password: ['', Validators.required]
|
password: ['', Validators.required],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkNostrExtensionAvailability(): void {
|
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;
|
this.isInstalledExtension = true;
|
||||||
} else {
|
} else {
|
||||||
this.isInstalledExtension = false;
|
this.isInstalledExtension = false;
|
||||||
@@ -91,7 +102,10 @@ export class LoginComponent implements OnInit {
|
|||||||
this.showSecAlert = false;
|
this.showSecAlert = false;
|
||||||
|
|
||||||
try {
|
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) {
|
if (success) {
|
||||||
// Successful login
|
// Successful login
|
||||||
@@ -102,26 +116,33 @@ export class LoginComponent implements OnInit {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle login failure
|
// Handle login failure
|
||||||
this.loading = false;
|
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;
|
this.showSecAlert = true;
|
||||||
console.error("Login error: ", error);
|
console.error('Login error: ', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
loginWithMenemonic(): void {
|
loginWithMenemonic(): void {
|
||||||
if (this.MenemonicLoginForm.invalid) {
|
if (this.MenemonicLoginForm.invalid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const menemonic = this.MenemonicLoginForm.get('menemonic')?.value;
|
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;
|
const password = this.MenemonicLoginForm.get('password')?.value;
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.showMenemonicAlert = false;
|
this.showMenemonicAlert = false;
|
||||||
|
|
||||||
const success = this._signerService.handleLoginWithMenemonic(menemonic, passphrase,password);
|
const success = this._signerService.handleLoginWithMenemonic(
|
||||||
|
menemonic,
|
||||||
|
passphrase,
|
||||||
|
password
|
||||||
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this._router.navigateByUrl('/home');
|
this._router.navigateByUrl('/home');
|
||||||
@@ -140,5 +161,4 @@ export class LoginComponent implements OnInit {
|
|||||||
console.error('Failed to log in using Nostr extension');
|
console.error('Failed to log in using Nostr extension');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,8 @@
|
|||||||
<a
|
<a
|
||||||
class="ml-1 text-primary-500 hover:underline"
|
class="ml-1 text-primary-500 hover:underline"
|
||||||
[routerLink]="['/login']"
|
[routerLink]="['/login']"
|
||||||
>
|
>
|
||||||
<span class="text-2xl font-extrabold">Go to login</span>
|
<span class="text-2xl font-extrabold">Go to login</span>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ export class LogoutComponent implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||||
|
|
||||||
constructor(private _router: Router, private _signerService: SignerService) {}
|
constructor(
|
||||||
|
private _router: Router,
|
||||||
|
private _signerService: SignerService
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
timer(1000, 1000)
|
timer(1000, 1000)
|
||||||
@@ -35,7 +38,6 @@ export class LogoutComponent implements OnInit, OnDestroy {
|
|||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this._unsubscribeAll.next(null);
|
this._unsubscribeAll.next(null);
|
||||||
this._unsubscribeAll.complete();
|
this._unsubscribeAll.complete();
|
||||||
@@ -44,6 +46,6 @@ export class LogoutComponent implements OnInit, OnDestroy {
|
|||||||
logout(): void {
|
logout(): void {
|
||||||
this._signerService.clearPassword();
|
this._signerService.clearPassword();
|
||||||
this._signerService.logout();
|
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
|
<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">
|
<div class="mx-auto w-full max-w-80 sm:mx-0 sm:w-80">
|
||||||
<!-- Title -->
|
<!-- 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
|
Register
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-0.5 flex items-baseline font-medium">
|
<div class="mt-0.5 flex items-baseline font-medium">
|
||||||
<div>Already have an account?</div>
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alert -->
|
<!-- Alert -->
|
||||||
@if (showAlert) {
|
@if (showAlert) {
|
||||||
<angor-alert class="mt-8" [appearance]="'outline'" [showIcon]="false" [type]="alert.type"
|
<angor-alert
|
||||||
[@shake]="alert.type === 'error'">
|
class="mt-8"
|
||||||
{{ alert.message }}
|
[appearance]="'outline'"
|
||||||
</angor-alert>
|
[showIcon]="false"
|
||||||
|
[type]="alert.type"
|
||||||
|
[@shake]="alert.type === 'error'"
|
||||||
|
>
|
||||||
|
{{ alert.message }}
|
||||||
|
</angor-alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Register form -->
|
<!-- Register form -->
|
||||||
<form class="mt-8" [formGroup]="registerForm" #registerNgForm="ngForm">
|
<form
|
||||||
|
class="mt-8"
|
||||||
|
[formGroup]="registerForm"
|
||||||
|
#registerNgForm="ngForm"
|
||||||
|
>
|
||||||
<!-- Name field (secretKey) -->
|
<!-- Name field (secretKey) -->
|
||||||
<mat-form-field class="w-full">
|
<mat-form-field class="w-full">
|
||||||
<mat-label>Full name</mat-label>
|
<mat-label>Full name</mat-label>
|
||||||
<input id="name" matInput [formControlName]="'name'" autocomplete="name"/>
|
<input
|
||||||
<mat-error *ngIf="registerForm.get('name').hasError('required')"> Full name is required </mat-error>
|
id="name"
|
||||||
|
matInput
|
||||||
|
[formControlName]="'name'"
|
||||||
|
autocomplete="name"
|
||||||
|
/>
|
||||||
|
<mat-error
|
||||||
|
*ngIf="registerForm.get('name').hasError('required')"
|
||||||
|
>
|
||||||
|
Full name is required
|
||||||
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<!-- Username field -->
|
<!-- Username field -->
|
||||||
<mat-form-field class="w-full">
|
<mat-form-field class="w-full">
|
||||||
<mat-label>Username</mat-label>
|
<mat-label>Username</mat-label>
|
||||||
<input id="username" matInput [formControlName]="'username'" autocomplete="username"/>
|
<input
|
||||||
<mat-error *ngIf="registerForm.get('username').hasError('required')"> Username is required </mat-error>
|
id="username"
|
||||||
|
matInput
|
||||||
|
[formControlName]="'username'"
|
||||||
|
autocomplete="username"
|
||||||
|
/>
|
||||||
|
<mat-error
|
||||||
|
*ngIf="
|
||||||
|
registerForm.get('username').hasError('required')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Username is required
|
||||||
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<!-- About field -->
|
<!-- About field -->
|
||||||
<mat-form-field class="w-full">
|
<mat-form-field class="w-full">
|
||||||
<mat-label>About</mat-label>
|
<mat-label>About</mat-label>
|
||||||
<textarea id="about" matInput [formControlName]="'about'"></textarea>
|
<textarea
|
||||||
|
id="about"
|
||||||
|
matInput
|
||||||
|
[formControlName]="'about'"
|
||||||
|
></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<!-- Avatar URL field -->
|
<!-- Avatar URL field -->
|
||||||
<mat-form-field class="w-full">
|
<mat-form-field class="w-full">
|
||||||
<mat-label>Avatar URL</mat-label>
|
<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>
|
</mat-form-field>
|
||||||
<!-- Password field -->
|
<!-- Password field -->
|
||||||
<mat-form-field class="w-full">
|
<mat-form-field class="w-full">
|
||||||
<mat-label>Password</mat-label>
|
<mat-label>Password</mat-label>
|
||||||
<input id="password" matInput type="password" [formControlName]="'password'" autocomplete="password" #passwordField />
|
<input
|
||||||
<button mat-icon-button type="button" (click)="
|
id="password"
|
||||||
|
matInput
|
||||||
|
type="password"
|
||||||
|
[formControlName]="'password'"
|
||||||
|
autocomplete="password"
|
||||||
|
#passwordField
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
type="button"
|
||||||
|
(click)="
|
||||||
passwordField.type === 'password'
|
passwordField.type === 'password'
|
||||||
? (passwordField.type = 'text')
|
? (passwordField.type = 'text')
|
||||||
: (passwordField.type = 'password')
|
: (passwordField.type = 'password')
|
||||||
" matSuffix>
|
"
|
||||||
<mat-icon *ngIf="passwordField.type === 'password'" class="icon-size-5" [svgIcon]="'heroicons_solid:eye'"></mat-icon>
|
matSuffix
|
||||||
<mat-icon *ngIf="passwordField.type === 'text'" class="icon-size-5" [svgIcon]="'heroicons_solid:eye-slash'"></mat-icon>
|
>
|
||||||
|
<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>
|
</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>
|
</mat-form-field>
|
||||||
|
|
||||||
<!-- ToS and PP -->
|
<!-- ToS and PP -->
|
||||||
<div class="mt-1.5 inline-flex w-full items-end">
|
<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>
|
<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>
|
<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>
|
</mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit button -->
|
<!-- Submit button -->
|
||||||
<button class="angor-mat-button-large mt-6 w-full" mat-flat-button [color]="'primary'"
|
<button
|
||||||
[disabled]="registerForm.invalid" (click)="register()">
|
class="angor-mat-button-large mt-6 w-full"
|
||||||
|
mat-flat-button
|
||||||
|
[color]="'primary'"
|
||||||
|
[disabled]="registerForm.invalid"
|
||||||
|
(click)="register()"
|
||||||
|
>
|
||||||
<span>Create your account</span>
|
<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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
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%"
|
<svg
|
||||||
preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
|
class="pointer-events-none absolute inset-0"
|
||||||
<g class="text-gray-700 opacity-25" fill="none" stroke="currentColor" stroke-width="100">
|
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="196" cy="23"></circle>
|
||||||
<circle r="234" cx="790" cy="491"></circle>
|
<circle r="234" cx="790" cy="491"></circle>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<svg class="absolute -top-16 -right-16 text-gray-700" viewBox="0 0 220 192" width="220" height="192"
|
<svg
|
||||||
fill="none">
|
class="absolute -right-16 -top-16 text-gray-700"
|
||||||
|
viewBox="0 0 220 192"
|
||||||
|
width="220"
|
||||||
|
height="192"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="837c3e70-6c3a-44e6-8854-cc48c737b659" x="0" y="0" width="20" height="20"
|
<pattern
|
||||||
patternUnits="userSpaceOnUse">
|
id="837c3e70-6c3a-44e6-8854-cc48c737b659"
|
||||||
<rect x="0" y="0" width="4" height="4" fill="currentColor"></rect>
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="4"
|
||||||
|
height="4"
|
||||||
|
fill="currentColor"
|
||||||
|
></rect>
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</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>
|
</svg>
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="relative z-10 w-full max-w-2xl">
|
<div class="relative z-10 w-full max-w-2xl">
|
||||||
@@ -110,8 +234,8 @@
|
|||||||
<div>Angor Hub</div>
|
<div>Angor Hub</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 text-lg leading-6 tracking-tight text-gray-400">
|
<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
|
Angor Hub is a Nostr client that is customized around the Angor
|
||||||
platform.
|
protocol, a decentralized crowdfunding platform.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -14,11 +17,7 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { Router, RouterLink } from '@angular/router';
|
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 { SignerService } from 'app/services/signer.service';
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'auth-register',
|
selector: 'auth-register',
|
||||||
@@ -37,7 +36,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
CommonModule
|
CommonModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class RegisterComponent implements OnInit {
|
export class RegisterComponent implements OnInit {
|
||||||
@@ -89,7 +88,10 @@ export class RegisterComponent implements OnInit {
|
|||||||
if (!keys) {
|
if (!keys) {
|
||||||
// If key generation failed, enable the form and show an error
|
// If key generation failed, enable the form and show an error
|
||||||
this.registerForm.enable();
|
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;
|
this.showAlert = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -112,11 +114,13 @@ export class RegisterComponent implements OnInit {
|
|||||||
console.log('User Metadata:', userMetadata);
|
console.log('User Metadata:', userMetadata);
|
||||||
|
|
||||||
// Display success alert
|
// Display success alert
|
||||||
this.alert = { type: 'success', message: 'Account created successfully!' };
|
this.alert = {
|
||||||
|
type: 'success',
|
||||||
|
message: 'Account created successfully!',
|
||||||
|
};
|
||||||
this.showAlert = true;
|
this.showAlert = true;
|
||||||
|
|
||||||
// Redirect to home
|
// Redirect to home
|
||||||
this._router.navigateByUrl('/home');
|
this._router.navigateByUrl('/home');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ const conversationResolver = (
|
|||||||
const chatService = inject(ChatService);
|
const chatService = inject(ChatService);
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
|
|
||||||
let chatId = route.paramMap.get('id') || localStorage.getItem('currentChatId');
|
let chatId =
|
||||||
|
route.paramMap.get('id') || localStorage.getItem('currentChatId');
|
||||||
|
|
||||||
if (!chatId) {
|
if (!chatId) {
|
||||||
const parentUrl = state.url.split('/').slice(0, -1).join('/');
|
const parentUrl = state.url.split('/').slice(0, -1).join('/');
|
||||||
@@ -45,7 +46,6 @@ const conversationResolver = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
import { Injectable, OnDestroy } from '@angular/core';
|
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 { Chat, Contact, Profile } from 'app/components/chat/chat.types';
|
||||||
import { IndexedDBService } from 'app/services/indexed-db.service';
|
import { IndexedDBService } from 'app/services/indexed-db.service';
|
||||||
import { MetadataService } from 'app/services/metadata.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 { 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 { 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' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ChatService implements OnDestroy {
|
export class ChatService implements OnDestroy {
|
||||||
@@ -18,22 +32,26 @@ export class ChatService implements OnDestroy {
|
|||||||
private isDecrypting = false;
|
private isDecrypting = false;
|
||||||
private recipientPublicKey: string;
|
private recipientPublicKey: string;
|
||||||
private message: string;
|
private message: string;
|
||||||
private decryptedPrivateKey: string = "";
|
private decryptedPrivateKey: string = '';
|
||||||
private _chat: BehaviorSubject<Chat | null> = new BehaviorSubject(null);
|
private _chat: BehaviorSubject<Chat | null> = new BehaviorSubject(null);
|
||||||
private _chats: BehaviorSubject<Chat[] | null> = new BehaviorSubject(null);
|
private _chats: BehaviorSubject<Chat[] | null> = new BehaviorSubject(null);
|
||||||
private _contact: BehaviorSubject<Contact | null> = new BehaviorSubject(null);
|
private _contact: BehaviorSubject<Contact | null> = new BehaviorSubject(
|
||||||
private _contacts: BehaviorSubject<Contact[] | null> = new BehaviorSubject(null);
|
null
|
||||||
private _profile: BehaviorSubject<Profile | 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>();
|
private _unsubscribeAll: Subject<void> = new Subject<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _metadataService: MetadataService,
|
private _metadataService: MetadataService,
|
||||||
private _signerService: SignerService,
|
private _signerService: SignerService,
|
||||||
private _indexedDBService: IndexedDBService,
|
private _indexedDBService: IndexedDBService,
|
||||||
private _relayService: RelayService,
|
private _relayService: RelayService
|
||||||
|
) {}
|
||||||
|
|
||||||
) { }
|
|
||||||
get profile$(): Observable<Profile | null> {
|
get profile$(): Observable<Profile | null> {
|
||||||
return this._profile.asObservable();
|
return this._profile.asObservable();
|
||||||
}
|
}
|
||||||
@@ -54,46 +72,50 @@ export class ChatService implements OnDestroy {
|
|||||||
return this._contacts.asObservable();
|
return this._contacts.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
checkCurrentChatOnPageRefresh(chatIdFromURL: string): void {
|
checkCurrentChatOnPageRefresh(chatIdFromURL: string): void {
|
||||||
if (chatIdFromURL) {
|
if (chatIdFromURL) {
|
||||||
const currentChat = this._chat.value;
|
const currentChat = this._chat.value;
|
||||||
this.getChatById(chatIdFromURL).subscribe(chat => {
|
this.getChatById(chatIdFromURL).subscribe((chat) => {
|
||||||
if (chat) {
|
if (chat) {
|
||||||
this._chat.next(chat);
|
this._chat.next(chat);
|
||||||
this.loadChatHistory(chatIdFromURL);
|
this.loadChatHistory(chatIdFromURL);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async getContact(pubkey: string): Promise<void> {
|
async getContact(pubkey: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata = await this._metadataService.fetchMetadataWithCache(pubkey);
|
const metadata =
|
||||||
|
await this._metadataService.fetchMetadataWithCache(pubkey);
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
const contact: Contact = {
|
const contact: Contact = {
|
||||||
pubKey: pubkey,
|
pubKey: pubkey,
|
||||||
displayName: metadata.name ? metadata.name : 'Unknown',
|
displayName: metadata.name ? metadata.name : 'Unknown',
|
||||||
picture: metadata.picture,
|
picture: metadata.picture,
|
||||||
about: metadata.about
|
about: metadata.about,
|
||||||
};
|
};
|
||||||
this._contact.next(contact);
|
this._contact.next(contact);
|
||||||
|
|
||||||
this._indexedDBService.getMetadataStream()
|
this._indexedDBService
|
||||||
|
.getMetadataStream()
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe((updatedMetadata) => {
|
.subscribe((updatedMetadata) => {
|
||||||
if (updatedMetadata && updatedMetadata.pubkey === pubkey) {
|
if (
|
||||||
|
updatedMetadata &&
|
||||||
|
updatedMetadata.pubkey === pubkey
|
||||||
|
) {
|
||||||
const updatedContact: Contact = {
|
const updatedContact: Contact = {
|
||||||
pubKey: pubkey,
|
pubKey: pubkey,
|
||||||
displayName: updatedMetadata.metadata.name ? updatedMetadata.metadata.name : 'Unknown',
|
displayName: updatedMetadata.metadata.name
|
||||||
|
? updatedMetadata.metadata.name
|
||||||
|
: 'Unknown',
|
||||||
picture: updatedMetadata.metadata.picture,
|
picture: updatedMetadata.metadata.picture,
|
||||||
about: updatedMetadata.metadata.about
|
about: updatedMetadata.metadata.about,
|
||||||
};
|
};
|
||||||
this._contact.next(updatedContact);
|
this._contact.next(updatedContact);
|
||||||
}
|
}
|
||||||
@@ -104,18 +126,23 @@ export class ChatService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getContacts(): Observable<Contact[]> {
|
getContacts(): Observable<Contact[]> {
|
||||||
return new Observable<Contact[]>((observer) => {
|
return new Observable<Contact[]>((observer) => {
|
||||||
this._indexedDBService.getAllUsers()
|
this._indexedDBService
|
||||||
|
.getAllUsers()
|
||||||
.then((cachedContacts: Contact[]) => {
|
.then((cachedContacts: Contact[]) => {
|
||||||
if (cachedContacts && cachedContacts.length > 0) {
|
if (cachedContacts && cachedContacts.length > 0) {
|
||||||
const validatedContacts = cachedContacts.map(contact => {
|
const validatedContacts = cachedContacts.map(
|
||||||
if (!contact.pubKey) {
|
(contact) => {
|
||||||
console.error('Contact is missing pubKey:', contact);
|
if (!contact.pubKey) {
|
||||||
|
console.error(
|
||||||
|
'Contact is missing pubKey:',
|
||||||
|
contact
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return contact;
|
||||||
}
|
}
|
||||||
return contact;
|
);
|
||||||
});
|
|
||||||
|
|
||||||
this._contacts.next(validatedContacts);
|
this._contacts.next(validatedContacts);
|
||||||
observer.next(validatedContacts);
|
observer.next(validatedContacts);
|
||||||
@@ -125,33 +152,49 @@ export class ChatService implements OnDestroy {
|
|||||||
observer.complete();
|
observer.complete();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error loading cached contacts from IndexedDB:', error);
|
console.error(
|
||||||
|
'Error loading cached contacts from IndexedDB:',
|
||||||
|
error
|
||||||
|
);
|
||||||
observer.next([]);
|
observer.next([]);
|
||||||
observer.complete();
|
observer.complete();
|
||||||
});
|
});
|
||||||
|
|
||||||
return { unsubscribe() { } };
|
return { unsubscribe() {} };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateChatListMetadata(): Promise<void> {
|
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) {
|
if (pubKeys.length > 0) {
|
||||||
const metadataList = await this._metadataService.fetchMetadataForMultipleKeys(pubKeys);
|
const metadataList =
|
||||||
|
await this._metadataService.fetchMetadataForMultipleKeys(
|
||||||
|
pubKeys
|
||||||
|
);
|
||||||
|
|
||||||
metadataList.forEach(metadata => {
|
metadataList.forEach((metadata) => {
|
||||||
const contact = this._contacts.value?.find(contact => contact.pubKey === metadata.pubkey);
|
const contact = this._contacts.value?.find(
|
||||||
|
(contact) => contact.pubKey === metadata.pubkey
|
||||||
|
);
|
||||||
if (contact) {
|
if (contact) {
|
||||||
contact.displayName = metadata.metadata.name || 'Unknown';
|
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;
|
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) {
|
if (chat) {
|
||||||
chat.contact.displayName = metadata.metadata.name || 'Unknown';
|
chat.contact.displayName =
|
||||||
chat.contact.picture = metadata.metadata.picture || chat.contact.picture;
|
metadata.metadata.name || 'Unknown';
|
||||||
chat.contact.about = metadata.metadata.about || chat.contact.about;
|
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 {
|
private subscribeToRealTimeMetadataUpdates(pubKey: string): void {
|
||||||
this._metadataService.getMetadataStream()
|
this._metadataService
|
||||||
.pipe(filter(updatedMetadata => updatedMetadata && updatedMetadata.pubkey === pubKey))
|
.getMetadataStream()
|
||||||
.subscribe(updatedMetadata => {
|
.pipe(
|
||||||
const chat = this.chatList.find(chat => chat.contact?.pubKey === pubKey);
|
filter(
|
||||||
|
(updatedMetadata) =>
|
||||||
|
updatedMetadata && updatedMetadata.pubkey === pubKey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe((updatedMetadata) => {
|
||||||
|
const chat = this.chatList.find(
|
||||||
|
(chat) => chat.contact?.pubKey === pubKey
|
||||||
|
);
|
||||||
if (chat) {
|
if (chat) {
|
||||||
chat.contact.displayName = updatedMetadata.metadata.name || 'Unknown';
|
chat.contact.displayName =
|
||||||
chat.contact.picture = updatedMetadata.metadata.picture || chat.contact.picture;
|
updatedMetadata.metadata.name || 'Unknown';
|
||||||
chat.contact.about = updatedMetadata.metadata.about || chat.contact.about;
|
chat.contact.picture =
|
||||||
|
updatedMetadata.metadata.picture ||
|
||||||
|
chat.contact.picture;
|
||||||
|
chat.contact.about =
|
||||||
|
updatedMetadata.metadata.about || chat.contact.about;
|
||||||
this._chats.next(this.chatList);
|
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) {
|
if (contact) {
|
||||||
contact.displayName = updatedMetadata.metadata.name || 'Unknown';
|
contact.displayName =
|
||||||
contact.picture = updatedMetadata.metadata.picture || contact.picture;
|
updatedMetadata.metadata.name || 'Unknown';
|
||||||
contact.about = updatedMetadata.metadata.about || contact.about;
|
contact.picture =
|
||||||
|
updatedMetadata.metadata.picture || contact.picture;
|
||||||
|
contact.about =
|
||||||
|
updatedMetadata.metadata.about || contact.about;
|
||||||
this._contacts.next(this._contacts.value || []);
|
this._contacts.next(this._contacts.value || []);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async getProfile(): Promise<void> {
|
async getProfile(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const publicKey = this._signerService.getPublicKey();
|
const publicKey = this._signerService.getPublicKey();
|
||||||
const metadata = await this._metadataService.fetchMetadataWithCache(publicKey);
|
const metadata =
|
||||||
|
await this._metadataService.fetchMetadataWithCache(publicKey);
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
this._profile.next(metadata);
|
this._profile.next(metadata);
|
||||||
|
|
||||||
|
this._indexedDBService
|
||||||
this._indexedDBService.getMetadataStream()
|
.getMetadataStream()
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe((updatedMetadata) => {
|
.subscribe((updatedMetadata) => {
|
||||||
if (updatedMetadata && updatedMetadata.pubkey === publicKey) {
|
if (
|
||||||
|
updatedMetadata &&
|
||||||
|
updatedMetadata.pubkey === publicKey
|
||||||
|
) {
|
||||||
this._profile.next(updatedMetadata.metadata);
|
this._profile.next(updatedMetadata.metadata);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -206,14 +269,14 @@ export class ChatService implements OnDestroy {
|
|||||||
|
|
||||||
async getChats(): Promise<Observable<Chat[]>> {
|
async getChats(): Promise<Observable<Chat[]>> {
|
||||||
return this.getChatListStream().pipe(
|
return this.getChatListStream().pipe(
|
||||||
tap(chats => {
|
tap((chats) => {
|
||||||
if (chats && chats.length === 0) {
|
if (chats && chats.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pubKeys = chats
|
const pubKeys = chats
|
||||||
.filter(chat => chat.contact?.pubKey)
|
.filter((chat) => chat.contact?.pubKey)
|
||||||
.map(chat => chat.contact!.pubKey);
|
.map((chat) => chat.contact!.pubKey);
|
||||||
|
|
||||||
// Subscribe to all metadata updates in parallel
|
// Subscribe to all metadata updates in parallel
|
||||||
this.subscribeToRealTimeMetadataUpdatesBatch(pubKeys);
|
this.subscribeToRealTimeMetadataUpdatesBatch(pubKeys);
|
||||||
@@ -226,12 +289,19 @@ export class ChatService implements OnDestroy {
|
|||||||
const useExtension = await this._signerService.isUsingExtension();
|
const useExtension = await this._signerService.isUsingExtension();
|
||||||
const useSecretKey = await this._signerService.isUsingSecretKey();
|
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
|
// Perform metadata and chat loading in parallel for speed
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.updateChatListMetadata(),
|
this.updateChatListMetadata(),
|
||||||
this.subscribeToChatList(pubkey, useExtension, useSecretKey, this.decryptedPrivateKey)
|
this.subscribeToChatList(
|
||||||
|
pubkey,
|
||||||
|
useExtension,
|
||||||
|
useSecretKey,
|
||||||
|
this.decryptedPrivateKey
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return this.getChatListStream();
|
return this.getChatListStream();
|
||||||
@@ -239,46 +309,80 @@ export class ChatService implements OnDestroy {
|
|||||||
|
|
||||||
private subscribeToRealTimeMetadataUpdatesBatch(pubKeys: string[]): void {
|
private subscribeToRealTimeMetadataUpdatesBatch(pubKeys: string[]): void {
|
||||||
// Batch subscribe to all pubKeys metadata updates for efficiency
|
// Batch subscribe to all pubKeys metadata updates for efficiency
|
||||||
pubKeys.forEach(pubKey => {
|
pubKeys.forEach((pubKey) => {
|
||||||
this.subscribeToRealTimeMetadataUpdates(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 () => {
|
this._relayService.ensureConnectedRelays().then(async () => {
|
||||||
const filters: Filter[] = [
|
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, {
|
this._relayService
|
||||||
onevent: async (event: NostrEvent) => {
|
.getPool()
|
||||||
const otherPartyPubKey = event.pubkey === pubkey
|
.subscribeMany(
|
||||||
? event.tags.find(tag => tag[0] === 'p')?.[1] || ''
|
this._relayService.getConnectedRelays(),
|
||||||
: event.pubkey;
|
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;
|
const lastTimestamp =
|
||||||
if (event.created_at > lastTimestamp) {
|
this.latestMessageTimestamps[
|
||||||
this.messageQueue.push(event);
|
otherPartyPubKey
|
||||||
this.processNextMessage(pubkey, useExtension, useSecretKey, decryptedSenderPrivateKey);
|
] || 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();
|
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;
|
if (this.isDecrypting || this.messageQueue.length === 0) return;
|
||||||
|
|
||||||
this.isDecrypting = true;
|
this.isDecrypting = true;
|
||||||
@@ -290,7 +394,7 @@ export class ChatService implements OnDestroy {
|
|||||||
|
|
||||||
const isSentByUser = event.pubkey === pubkey;
|
const isSentByUser = event.pubkey === pubkey;
|
||||||
const otherPartyPubKey = isSentByUser
|
const otherPartyPubKey = isSentByUser
|
||||||
? event.tags.find(tag => tag[0] === 'p')?.[1] || ''
|
? event.tags.find((tag) => tag[0] === 'p')?.[1] || ''
|
||||||
: event.pubkey;
|
: event.pubkey;
|
||||||
|
|
||||||
if (!otherPartyPubKey) continue;
|
if (!otherPartyPubKey) continue;
|
||||||
@@ -304,7 +408,12 @@ export class ChatService implements OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (decryptedMessage) {
|
if (decryptedMessage) {
|
||||||
this.addOrUpdateChatList(otherPartyPubKey, decryptedMessage, event.created_at, isSentByUser);
|
this.addOrUpdateChatList(
|
||||||
|
otherPartyPubKey,
|
||||||
|
decryptedMessage,
|
||||||
|
event.created_at,
|
||||||
|
isSentByUser
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -314,8 +423,15 @@ export class ChatService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addOrUpdateChatList(pubKey: string, message: string, createdAt: number, isMine: boolean): void {
|
private addOrUpdateChatList(
|
||||||
const existingChat = this.chatList.find(chat => chat.contact?.pubKey === pubKey);
|
pubKey: string,
|
||||||
|
message: string,
|
||||||
|
createdAt: number,
|
||||||
|
isMine: boolean
|
||||||
|
): void {
|
||||||
|
const existingChat = this.chatList.find(
|
||||||
|
(chat) => chat.contact?.pubKey === pubKey
|
||||||
|
);
|
||||||
|
|
||||||
const newMessage = {
|
const newMessage = {
|
||||||
id: `${pubKey}-${createdAt}`,
|
id: `${pubKey}-${createdAt}`,
|
||||||
@@ -327,11 +443,19 @@ export class ChatService implements OnDestroy {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (existingChat) {
|
if (existingChat) {
|
||||||
const messageExists = existingChat.messages?.some(m => m.id === newMessage.id);
|
const messageExists = existingChat.messages?.some(
|
||||||
|
(m) => m.id === newMessage.id
|
||||||
|
);
|
||||||
|
|
||||||
if (!messageExists) {
|
if (!messageExists) {
|
||||||
existingChat.messages = [...(existingChat.messages || []), newMessage]
|
existingChat.messages = [
|
||||||
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
...(existingChat.messages || []),
|
||||||
|
newMessage,
|
||||||
|
].sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(a.createdAt).getTime() -
|
||||||
|
new Date(b.createdAt).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
if (Number(existingChat.lastMessageAt) < createdAt) {
|
if (Number(existingChat.lastMessageAt) < createdAt) {
|
||||||
existingChat.lastMessage = message;
|
existingChat.lastMessage = message;
|
||||||
@@ -339,25 +463,34 @@ export class ChatService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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 = {
|
const newChat: Chat = {
|
||||||
id: pubKey,
|
id: pubKey,
|
||||||
contact: {
|
contact: {
|
||||||
pubKey: contactInfo.pubKey,
|
pubKey: contactInfo.pubKey,
|
||||||
name: contactInfo.name || "Unknown",
|
name: contactInfo.name || 'Unknown',
|
||||||
picture: contactInfo.picture || "/images/avatars/avatar-placeholder.png",
|
picture:
|
||||||
about: contactInfo.about || "",
|
contactInfo.picture ||
|
||||||
displayName: contactInfo.displayName || contactInfo.name || "Unknown"
|
'/images/avatars/avatar-placeholder.png',
|
||||||
|
about: contactInfo.about || '',
|
||||||
|
displayName:
|
||||||
|
contactInfo.displayName ||
|
||||||
|
contactInfo.name ||
|
||||||
|
'Unknown',
|
||||||
},
|
},
|
||||||
lastMessage: message,
|
lastMessage: message,
|
||||||
lastMessageAt: createdAt.toString(),
|
lastMessageAt: createdAt.toString(),
|
||||||
messages: [newMessage]
|
messages: [newMessage],
|
||||||
};
|
};
|
||||||
this.chatList.push(newChat);
|
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);
|
this._chats.next(this.chatList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,9 +506,16 @@ export class ChatService implements OnDestroy {
|
|||||||
recipientPublicKey: string
|
recipientPublicKey: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (useExtension && !useSecretKey) {
|
if (useExtension && !useSecretKey) {
|
||||||
return await this._signerService.decryptMessageWithExtension(recipientPublicKey, event.content);
|
return await this._signerService.decryptMessageWithExtension(
|
||||||
|
recipientPublicKey,
|
||||||
|
event.content
|
||||||
|
);
|
||||||
} else if (useSecretKey && !useExtension) {
|
} 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 myPubKey = this._signerService.getPublicKey();
|
||||||
|
|
||||||
const historyFilter: Filter[] = [
|
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, {
|
this._relayService
|
||||||
onevent: async (event: NostrEvent) => {
|
.getPool()
|
||||||
const isSentByMe = event.pubkey === myPubKey;
|
.subscribeMany(
|
||||||
const senderOrRecipientPubKey = isSentByMe ? pubKey : event.pubkey;
|
this._relayService.getConnectedRelays(),
|
||||||
const useExtension = await this._signerService.isUsingExtension();
|
historyFilter,
|
||||||
const useSecretKey = await this._signerService.isUsingSecretKey();
|
{
|
||||||
const decryptedMessage = await this.decryptReceivedMessage(
|
onevent: async (event: NostrEvent) => {
|
||||||
event,
|
const isSentByMe = event.pubkey === myPubKey;
|
||||||
useExtension,
|
const senderOrRecipientPubKey = isSentByMe
|
||||||
useSecretKey,
|
? pubKey
|
||||||
this.decryptedPrivateKey,
|
: event.pubkey;
|
||||||
senderOrRecipientPubKey
|
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) {
|
if (decryptedMessage) {
|
||||||
const messageTimestamp = Math.floor(event.created_at);
|
const messageTimestamp = Math.floor(
|
||||||
|
event.created_at
|
||||||
|
);
|
||||||
|
|
||||||
this.addOrUpdateChatList(pubKey, decryptedMessage, messageTimestamp, isSentByMe);
|
this.addOrUpdateChatList(
|
||||||
this._chat.next(this.chatList.find(chat => chat.id === pubKey));
|
pubKey,
|
||||||
|
decryptedMessage,
|
||||||
|
messageTimestamp,
|
||||||
|
isSentByMe
|
||||||
|
);
|
||||||
|
this._chat.next(
|
||||||
|
this.chatList.find((chat) => chat.id === pubKey)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
oneose: () => {},
|
||||||
}
|
}
|
||||||
},
|
);
|
||||||
oneose: () => {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchChatHistory(pubKey: string): Promise<any[]> {
|
private async fetchChatHistory(pubKey: string): Promise<any[]> {
|
||||||
const myPubKey = this._signerService.getPublicKey();
|
const myPubKey = this._signerService.getPublicKey();
|
||||||
|
|
||||||
const historyFilter: Filter[] = [
|
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[] = [];
|
const messages: any[] = [];
|
||||||
|
|
||||||
this._relayService.getPool().subscribeMany(this._relayService.getConnectedRelays(), historyFilter, {
|
this._relayService
|
||||||
onevent: async (event: NostrEvent) => {
|
.getPool()
|
||||||
const isSentByMe = event.pubkey === myPubKey;
|
.subscribeMany(
|
||||||
const senderOrRecipientPubKey = isSentByMe ? pubKey : event.pubkey;
|
this._relayService.getConnectedRelays(),
|
||||||
const useExtension = await this._signerService.isUsingExtension();
|
historyFilter,
|
||||||
const useSecretKey = await this._signerService.isUsingSecretKey();
|
{
|
||||||
const decryptedMessage = await this.decryptReceivedMessage(
|
onevent: async (event: NostrEvent) => {
|
||||||
event,
|
const isSentByMe = event.pubkey === myPubKey;
|
||||||
useExtension,
|
const senderOrRecipientPubKey = isSentByMe
|
||||||
useSecretKey,
|
? pubKey
|
||||||
this.decryptedPrivateKey,
|
: event.pubkey;
|
||||||
senderOrRecipientPubKey
|
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) {
|
if (decryptedMessage) {
|
||||||
const messageTimestamp = Math.floor(event.created_at);
|
const messageTimestamp = Math.floor(
|
||||||
|
event.created_at
|
||||||
|
);
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
chatId: pubKey,
|
chatId: pubKey,
|
||||||
contactId: senderOrRecipientPubKey,
|
contactId: senderOrRecipientPubKey,
|
||||||
isMine: isSentByMe,
|
isMine: isSentByMe,
|
||||||
value: decryptedMessage,
|
value: decryptedMessage,
|
||||||
createdAt: new Date(messageTimestamp * 1000).toISOString(),
|
createdAt: new Date(
|
||||||
};
|
messageTimestamp * 1000
|
||||||
|
).toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
messages.push(message);
|
messages.push(message);
|
||||||
|
|
||||||
this.addOrUpdateChatList(pubKey, decryptedMessage, messageTimestamp, isSentByMe);
|
this.addOrUpdateChatList(
|
||||||
this._chat.next(this.chatList.find(chat => chat.id === pubKey));
|
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;
|
return messages;
|
||||||
}
|
}
|
||||||
@@ -484,10 +684,14 @@ export class ChatService implements OnDestroy {
|
|||||||
|
|
||||||
event.id = getEventHash(event);
|
event.id = getEventHash(event);
|
||||||
|
|
||||||
return from(this._relayService.publishEventToRelays(event)).pipe(
|
return from(
|
||||||
|
this._relayService.publishEventToRelays(event)
|
||||||
|
).pipe(
|
||||||
map(() => {
|
map(() => {
|
||||||
if (chats) {
|
if (chats) {
|
||||||
const index = chats.findIndex((item) => item.id === id);
|
const index = chats.findIndex(
|
||||||
|
(item) => item.id === id
|
||||||
|
);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
chats[index] = chat;
|
chats[index] = chat;
|
||||||
this._chats.next(chats);
|
this._chats.next(chats);
|
||||||
@@ -496,7 +700,10 @@ export class ChatService implements OnDestroy {
|
|||||||
return chat;
|
return chat;
|
||||||
}),
|
}),
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
console.error('Failed to update chat via Nostr:', error);
|
console.error(
|
||||||
|
'Failed to update chat via Nostr:',
|
||||||
|
error
|
||||||
|
);
|
||||||
return throwError(error);
|
return throwError(error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -516,7 +723,7 @@ export class ChatService implements OnDestroy {
|
|||||||
return this.createNewChat(id, contact);
|
return this.createNewChat(id, contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedChat = chats.find(chat => chat.id === id);
|
const cachedChat = chats.find((chat) => chat.id === id);
|
||||||
if (cachedChat) {
|
if (cachedChat) {
|
||||||
this._chat.next(cachedChat);
|
this._chat.next(cachedChat);
|
||||||
this.loadChatHistory(this.recipientPublicKey);
|
this.loadChatHistory(this.recipientPublicKey);
|
||||||
@@ -534,8 +741,6 @@ export class ChatService implements OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
createNewChat(id: string, contact: Contact = null): Observable<Chat> {
|
createNewChat(id: string, contact: Contact = null): Observable<Chat> {
|
||||||
// const existingChat = this._chats.value?.find(chat => chat.id === id);
|
// const existingChat = this._chats.value?.find(chat => chat.id === id);
|
||||||
|
|
||||||
@@ -545,11 +750,21 @@ export class ChatService implements OnDestroy {
|
|||||||
const newChat: Chat = {
|
const newChat: Chat = {
|
||||||
id: id || '',
|
id: id || '',
|
||||||
contact: contact
|
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...',
|
lastMessage: 'new chat...',
|
||||||
lastMessageAt: Math.floor(Date.now() / 1000).toString() || '0',
|
lastMessageAt: Math.floor(Date.now() / 1000).toString() || '0',
|
||||||
messages: []
|
messages: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// const updatedChats = this._chats.value ? [...this._chats.value, newChat] : [newChat];
|
// const updatedChats = this._chats.value ? [...this._chats.value, newChat] : [newChat];
|
||||||
@@ -559,10 +774,13 @@ export class ChatService implements OnDestroy {
|
|||||||
map((metadata: any) => {
|
map((metadata: any) => {
|
||||||
newChat.contact = {
|
newChat.contact = {
|
||||||
pubKey: id,
|
pubKey: id,
|
||||||
name: metadata?.name || "Unknown",
|
name: metadata?.name || 'Unknown',
|
||||||
picture: metadata?.picture || "/images/avatars/avatar-placeholder.png",
|
picture:
|
||||||
about: metadata?.about || "",
|
metadata?.picture ||
|
||||||
displayName: metadata?.displayName || metadata?.name || "Unknown"
|
'/images/avatars/avatar-placeholder.png',
|
||||||
|
about: metadata?.about || '',
|
||||||
|
displayName:
|
||||||
|
metadata?.displayName || metadata?.name || 'Unknown',
|
||||||
};
|
};
|
||||||
|
|
||||||
return newChat;
|
return newChat;
|
||||||
@@ -576,8 +794,10 @@ export class ChatService implements OnDestroy {
|
|||||||
chatId: id,
|
chatId: id,
|
||||||
contactId: id,
|
contactId: id,
|
||||||
isMine: true,
|
isMine: true,
|
||||||
value: "new chat...",
|
value: 'new chat...',
|
||||||
createdAt: Math.floor(Date.now() / 1000).toString(),
|
createdAt: Math.floor(
|
||||||
|
Date.now() / 1000
|
||||||
|
).toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
messages.push(testMessage);
|
messages.push(testMessage);
|
||||||
@@ -591,7 +811,9 @@ export class ChatService implements OnDestroy {
|
|||||||
newChat.lastMessageAt = lastMessage.createdAt;
|
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._chats.next(updatedChatsWithMessages);
|
||||||
this._chat.next(newChat);
|
this._chat.next(newChat);
|
||||||
|
|
||||||
@@ -602,14 +824,10 @@ export class ChatService implements OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
resetChat(): void {
|
resetChat(): void {
|
||||||
this._chat.next(null);
|
this._chat.next(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async sendPrivateMessage(message: string): Promise<void> {
|
public async sendPrivateMessage(message: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
@@ -620,20 +838,31 @@ export class ChatService implements OnDestroy {
|
|||||||
await this.handleMessageSendingWithExtension();
|
await this.handleMessageSendingWithExtension();
|
||||||
} else if (!useExtension && useSecretKey) {
|
} else if (!useExtension && useSecretKey) {
|
||||||
if (!this.isValidMessageSetup()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const encryptedMessage = await this._signerService.encryptMessage(
|
const encryptedMessage =
|
||||||
this.decryptedPrivateKey,
|
await this._signerService.encryptMessage(
|
||||||
this.recipientPublicKey,
|
this.decryptedPrivateKey,
|
||||||
this.message
|
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) {
|
if (published) {
|
||||||
this.message = '';
|
this.message = '';
|
||||||
@@ -641,7 +870,6 @@ export class ChatService implements OnDestroy {
|
|||||||
console.error('Failed to send the message.');
|
console.error('Failed to send the message.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending private message:', error);
|
console.error('Error sending private message:', error);
|
||||||
}
|
}
|
||||||
@@ -649,20 +877,23 @@ export class ChatService implements OnDestroy {
|
|||||||
|
|
||||||
private async handleMessageSendingWithExtension(): Promise<void> {
|
private async handleMessageSendingWithExtension(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const encryptedMessage = await this._signerService.encryptMessageWithExtension(
|
const encryptedMessage =
|
||||||
this.message,
|
await this._signerService.encryptMessageWithExtension(
|
||||||
this.recipientPublicKey
|
this.message,
|
||||||
);
|
this.recipientPublicKey
|
||||||
|
);
|
||||||
|
|
||||||
const signedEvent = await this._signerService.signEventWithExtension({
|
const signedEvent =
|
||||||
kind: 4,
|
await this._signerService.signEventWithExtension({
|
||||||
pubkey: this._signerService.getPublicKey(),
|
kind: 4,
|
||||||
tags: [['p', this.recipientPublicKey]],
|
pubkey: this._signerService.getPublicKey(),
|
||||||
content: encryptedMessage,
|
tags: [['p', this.recipientPublicKey]],
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
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) {
|
if (published) {
|
||||||
this.message = '';
|
this.message = '';
|
||||||
@@ -682,6 +913,4 @@ export class ChatService implements OnDestroy {
|
|||||||
this._unsubscribeAll.next();
|
this._unsubscribeAll.next();
|
||||||
this._unsubscribeAll.complete();
|
this._unsubscribeAll.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
<div class="bg-card relative flex w-full flex-auto dark:bg-transparent">
|
<div class="bg-card relative flex w-full flex-auto dark:bg-transparent">
|
||||||
<mat-drawer-container class="h-full flex-auto" [hasBackdrop]="false">
|
<mat-drawer-container class="h-full flex-auto" [hasBackdrop]="false">
|
||||||
<!-- Drawer -->
|
<!-- Drawer -->
|
||||||
<mat-drawer class="w-full dark:bg-gray-900 sm:w-100 lg:border-r lg:shadow-none" [autoFocus]="false"
|
<mat-drawer
|
||||||
[(opened)]="drawerOpened" #drawer>
|
class="w-full dark:bg-gray-900 sm:w-100 lg:border-r lg:shadow-none"
|
||||||
|
[autoFocus]="false"
|
||||||
|
[(opened)]="drawerOpened"
|
||||||
|
#drawer
|
||||||
|
>
|
||||||
<!-- New chat -->
|
<!-- New chat -->
|
||||||
@if (drawerComponent === 'new-chat') {
|
@if (drawerComponent === 'new-chat') {
|
||||||
<chat-new-chat [drawer]="drawer"></chat-new-chat>
|
<chat-new-chat [drawer]="drawer"></chat-new-chat>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Profile -->
|
<!-- Profile -->
|
||||||
@if (drawerComponent === 'profile') {
|
@if (drawerComponent === 'profile') {
|
||||||
<chat-profile [drawer]="drawer"></chat-profile>
|
<chat-profile [drawer]="drawer"></chat-profile>
|
||||||
}
|
}
|
||||||
</mat-drawer>
|
</mat-drawer>
|
||||||
|
|
||||||
@@ -18,180 +22,276 @@
|
|||||||
<mat-drawer-content class="flex overflow-hidden">
|
<mat-drawer-content class="flex overflow-hidden">
|
||||||
<!-- Chats list -->
|
<!-- Chats list -->
|
||||||
@if (chats && chats.length > 0) {
|
@if (chats && chats.length > 0) {
|
||||||
<div
|
<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">
|
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">
|
<!-- Header -->
|
||||||
<div class="flex items-center">
|
<div
|
||||||
<div class="mr-1 flex cursor-pointer items-center" (click)="openProfile()">
|
class="flex flex-0 flex-col border-b bg-gray-50 px-8 py-4 dark:bg-transparent"
|
||||||
<div class="h-10 w-10">
|
>
|
||||||
@if (profile?.picture) {
|
<div class="flex items-center">
|
||||||
<img class="h-full w-full rounded-full object-cover" [src]="profile?.picture"
|
<div
|
||||||
onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
|
class="mr-1 flex cursor-pointer items-center"
|
||||||
alt="Profile picture" />
|
(click)="openProfile()"
|
||||||
}
|
>
|
||||||
@if (!profile?.picture) {
|
<div class="h-10 w-10">
|
||||||
<div
|
@if (profile?.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">
|
<img
|
||||||
{{ profile?.name?.charAt(0) }}
|
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>
|
</div>
|
||||||
<div class="ml-4 truncate font-medium">
|
<button
|
||||||
{{ profile?.name }}
|
class="ml-auto"
|
||||||
</div>
|
mat-icon-button
|
||||||
</div>
|
(click)="openNewChat()"
|
||||||
<button class="ml-auto" mat-icon-button (click)="openNewChat()">
|
>
|
||||||
<mat-icon [svgIcon]="'heroicons_outline:plus-circle'"></mat-icon>
|
<mat-icon
|
||||||
</button>
|
[svgIcon]="'heroicons_outline:plus-circle'"
|
||||||
<button class="-mr-4 ml-1" mat-icon-button [matMenuTriggerFor]="chatsHeaderMenu">
|
></mat-icon>
|
||||||
<mat-icon [svgIcon]="
|
</button>
|
||||||
|
<button
|
||||||
|
class="-mr-4 ml-1"
|
||||||
|
mat-icon-button
|
||||||
|
[matMenuTriggerFor]="chatsHeaderMenu"
|
||||||
|
>
|
||||||
|
<mat-icon
|
||||||
|
[svgIcon]="
|
||||||
'heroicons_outline:ellipsis-vertical'
|
'heroicons_outline:ellipsis-vertical'
|
||||||
"></mat-icon>
|
"
|
||||||
<mat-menu #chatsHeaderMenu>
|
></mat-icon>
|
||||||
<button mat-menu-item>
|
<mat-menu #chatsHeaderMenu>
|
||||||
<mat-icon [svgIcon]="
|
<button mat-menu-item>
|
||||||
|
<mat-icon
|
||||||
|
[svgIcon]="
|
||||||
'heroicons_outline:user-group'
|
'heroicons_outline:user-group'
|
||||||
"></mat-icon>
|
"
|
||||||
New group
|
></mat-icon>
|
||||||
</button>
|
New group
|
||||||
<button mat-menu-item>
|
</button>
|
||||||
<mat-icon [svgIcon]="
|
<button mat-menu-item>
|
||||||
|
<mat-icon
|
||||||
|
[svgIcon]="
|
||||||
'heroicons_outline:chat-bubble-left-right'
|
'heroicons_outline:chat-bubble-left-right'
|
||||||
"></mat-icon>
|
"
|
||||||
Create a room
|
></mat-icon>
|
||||||
</button>
|
Create a room
|
||||||
<button mat-menu-item (click)="openProfile()">
|
</button>
|
||||||
<mat-icon [svgIcon]="
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
(click)="openProfile()"
|
||||||
|
>
|
||||||
|
<mat-icon
|
||||||
|
[svgIcon]="
|
||||||
'heroicons_outline:user-circle'
|
'heroicons_outline:user-circle'
|
||||||
"></mat-icon>
|
"
|
||||||
Profile
|
></mat-icon>
|
||||||
</button>
|
Profile
|
||||||
<button mat-menu-item>
|
</button>
|
||||||
<mat-icon [svgIcon]="
|
<button mat-menu-item>
|
||||||
|
<mat-icon
|
||||||
|
[svgIcon]="
|
||||||
'heroicons_outline:archive-box'
|
'heroicons_outline:archive-box'
|
||||||
"></mat-icon>
|
"
|
||||||
Archived
|
></mat-icon>
|
||||||
</button>
|
Archived
|
||||||
<button mat-menu-item>
|
</button>
|
||||||
<mat-icon [svgIcon]="'heroicons_outline:star'"></mat-icon>
|
<button mat-menu-item>
|
||||||
Starred
|
<mat-icon
|
||||||
</button>
|
[svgIcon]="'heroicons_outline:star'"
|
||||||
<button mat-menu-item>
|
></mat-icon>
|
||||||
<mat-icon [svgIcon]="
|
Starred
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item>
|
||||||
|
<mat-icon
|
||||||
|
[svgIcon]="
|
||||||
'heroicons_outline:cog-8-tooth'
|
'heroicons_outline:cog-8-tooth'
|
||||||
"></mat-icon>
|
"
|
||||||
Settings
|
></mat-icon>
|
||||||
</button>
|
Settings
|
||||||
</mat-menu>
|
</button>
|
||||||
</button>
|
</mat-menu>
|
||||||
</div>
|
</button>
|
||||||
<!-- Search -->
|
</div>
|
||||||
<div class="mt-4">
|
<!-- Search -->
|
||||||
<mat-form-field class="angor-mat-rounded angor-mat-dense w-full" [subscriptSizing]="'dynamic'">
|
<div class="mt-4">
|
||||||
<mat-icon matPrefix class="icon-size-5" [svgIcon]="
|
<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'
|
'heroicons_solid:magnifying-glass'
|
||||||
"></mat-icon>
|
"
|
||||||
<input matInput [autocomplete]="'off'" [placeholder]="'Search or start new chat'"
|
></mat-icon>
|
||||||
(input)="filterChats(searchField.value)" #searchField />
|
<input
|
||||||
</mat-form-field>
|
matInput
|
||||||
|
[autocomplete]="'off'"
|
||||||
|
[placeholder]="'Search or start new chat'"
|
||||||
|
(input)="filterChats(searchField.value)"
|
||||||
|
#searchField
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Chats -->
|
<!-- Chats -->
|
||||||
<div class="flex-auto overflow-y-auto">
|
<div class="flex-auto overflow-y-auto">
|
||||||
@if (filteredChats.length > 0) {
|
@if (filteredChats.length > 0) {
|
||||||
@for (
|
@for (
|
||||||
chat of filteredChats;
|
chat of filteredChats;
|
||||||
track trackByFn($index, chat)
|
track trackByFn($index, chat)
|
||||||
) {
|
) {
|
||||||
<a class="z-20 flex cursor-pointer items-center border-b px-8 py-5" [ngClass]="{
|
<a
|
||||||
|
class="z-20 flex cursor-pointer items-center border-b px-8 py-5"
|
||||||
|
[ngClass]="{
|
||||||
'dark:hover:bg-hover hover:bg-gray-100':
|
'dark:hover:bg-hover hover:bg-gray-100':
|
||||||
!selectedChat ||
|
!selectedChat ||
|
||||||
selectedChat.id !== chat.id,
|
selectedChat.id !== chat.id,
|
||||||
'bg-primary-50 dark:bg-hover':
|
'bg-primary-50 dark:bg-hover':
|
||||||
selectedChat &&
|
selectedChat &&
|
||||||
selectedChat.id === chat.id,
|
selectedChat.id === chat.id,
|
||||||
}" [routerLink]="[chat.id]">
|
}"
|
||||||
<div class="relative flex h-10 w-10 flex-0 items-center justify-center">
|
[routerLink]="[chat.id]"
|
||||||
@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"
|
<div
|
||||||
[class.ring-primary-50]="
|
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 &&
|
||||||
selectedChat.id === chat.id
|
selectedChat.id === chat.id
|
||||||
"></div>
|
"
|
||||||
}
|
></div>
|
||||||
<!-- If contact has a picture -->
|
}
|
||||||
<img *ngIf="chat.contact?.picture" class="h-full w-full rounded-full object-cover"
|
<!-- If contact has a picture -->
|
||||||
[src]="chat.contact?.picture" (error)="
|
<img
|
||||||
|
*ngIf="chat.contact?.picture"
|
||||||
|
class="h-full w-full rounded-full object-cover"
|
||||||
|
[src]="chat.contact?.picture"
|
||||||
|
(error)="
|
||||||
this.src =
|
this.src =
|
||||||
'/images/avatars/avatar-placeholder.png'
|
'/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 -->
|
<!-- If contact doesn't have a picture, display the first letter of the name -->
|
||||||
<div *ngIf="!chat.contact?.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">
|
*ngIf="!chat.contact?.picture"
|
||||||
{{ chat.contact?.name?.charAt(0) }}
|
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"
|
||||||
</div>
|
>
|
||||||
</div>
|
{{ chat.contact?.name?.charAt(0) }}
|
||||||
<div class="ml-4 min-w-0">
|
</div>
|
||||||
<div class="truncate font-medium leading-5">
|
</div>
|
||||||
{{ chat.contact?.name }}
|
<div class="ml-4 min-w-0">
|
||||||
</div>
|
<div
|
||||||
<div class="text-secondary truncate leading-5" [class.text-primary]="
|
class="truncate font-medium leading-5"
|
||||||
|
>
|
||||||
|
{{ chat.contact?.name }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-secondary truncate leading-5"
|
||||||
|
[class.text-primary]="
|
||||||
chat.unreadCount > 0
|
chat.unreadCount > 0
|
||||||
" [class.dark:text-primary-500]="
|
"
|
||||||
|
[class.dark:text-primary-500]="
|
||||||
chat.unreadCount > 0
|
chat.unreadCount > 0
|
||||||
">
|
"
|
||||||
{{ chat.lastMessage | checkmessage}}
|
>
|
||||||
</div>
|
{{
|
||||||
</div>
|
chat.lastMessage | checkmessage
|
||||||
<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">
|
</div>
|
||||||
{{ chat.lastMessageAt | ago }}
|
</div>
|
||||||
</div>
|
<div
|
||||||
@if (chat.muted) {
|
class="ml-auto flex flex-col items-end self-start pl-2"
|
||||||
<mat-icon class="text-hint icon-size-5" [svgIcon]="
|
>
|
||||||
|
<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'
|
'heroicons_solid:speaker-x-mark'
|
||||||
"></mat-icon>
|
"
|
||||||
|
></mat-icon>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
} @else {
|
||||||
</a>
|
<div
|
||||||
}
|
class="flex h-full flex-auto flex-col items-center justify-center"
|
||||||
} @else {
|
>
|
||||||
<div class="flex h-full flex-auto flex-col items-center justify-center">
|
<mat-icon
|
||||||
<mat-icon class="icon-size-24" [svgIcon]="
|
class="icon-size-24"
|
||||||
|
[svgIcon]="
|
||||||
'heroicons_outline:chat-bubble-oval-left-ellipsis'
|
'heroicons_outline:chat-bubble-oval-left-ellipsis'
|
||||||
"></mat-icon>
|
"
|
||||||
<div class="text-secondary mt-4 text-2xl font-semibold tracking-tight">
|
></mat-icon>
|
||||||
No chats
|
<div
|
||||||
</div>
|
class="text-secondary mt-4 text-2xl font-semibold tracking-tight"
|
||||||
|
>
|
||||||
|
No chats
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
} @else {
|
} @else {
|
||||||
<div class="flex h-full flex-auto flex-col items-center justify-center">
|
<div
|
||||||
<mat-icon class="icon-size-24" [svgIcon]="
|
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'
|
'heroicons_outline:chat-bubble-oval-left-ellipsis'
|
||||||
"></mat-icon>
|
"
|
||||||
<div class="text-secondary mt-4 text-2xl font-semibold tracking-tight">
|
></mat-icon>
|
||||||
No chats
|
<div
|
||||||
|
class="text-secondary mt-4 text-2xl font-semibold tracking-tight"
|
||||||
|
>
|
||||||
|
No chats
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- No chats template -->
|
<!-- No chats template -->
|
||||||
|
|
||||||
<!-- Conversation -->
|
<!-- Conversation -->
|
||||||
@if (chats && chats.length > 0) {
|
@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':
|
'absolute inset-0 z-20 flex lg:static lg:inset-auto':
|
||||||
selectedChat && selectedChat.id,
|
selectedChat && selectedChat.id,
|
||||||
'hidden lg:flex': !selectedChat || !selectedChat.id,
|
'hidden lg:flex': !selectedChat || !selectedChat.id,
|
||||||
}">
|
}"
|
||||||
<router-outlet></router-outlet>
|
>
|
||||||
</div>
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</mat-drawer-content>
|
</mat-drawer-content>
|
||||||
</mat-drawer-container>
|
</mat-drawer-container>
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import { MatInputModule } from '@angular/material/input';
|
|||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
import { ActivatedRoute, RouterLink, RouterOutlet } from '@angular/router';
|
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 { ChatService } from '../chat.service';
|
||||||
import { Chat, Profile } from '../chat.types';
|
import { Chat, Profile } from '../chat.types';
|
||||||
import { NewChatComponent } from '../new-chat/new-chat.component';
|
import { NewChatComponent } from '../new-chat/new-chat.component';
|
||||||
import { ProfileComponent } from '../profile/profile.component';
|
import { ProfileComponent } from '../profile/profile.component';
|
||||||
import { AgoPipe } from 'app/shared/pipes/ago.pipe';
|
|
||||||
import { CheckmessagePipe } from 'app/shared/pipes/checkmessage.pipe';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'chat-chats',
|
selector: 'chat-chats',
|
||||||
@@ -42,7 +42,7 @@ import { CheckmessagePipe } from 'app/shared/pipes/checkmessage.pipe';
|
|||||||
RouterOutlet,
|
RouterOutlet,
|
||||||
AgoPipe,
|
AgoPipe,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
CheckmessagePipe
|
CheckmessagePipe,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ChatsComponent implements OnInit, OnDestroy {
|
export class ChatsComponent implements OnInit, OnDestroy {
|
||||||
@@ -67,9 +67,8 @@ export class ChatsComponent implements OnInit, OnDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
private _chatService: ChatService,
|
private _chatService: ChatService,
|
||||||
private _changeDetectorRef: ChangeDetectorRef,
|
private _changeDetectorRef: ChangeDetectorRef,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute
|
||||||
|
) {}
|
||||||
) { }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Angular lifecycle hook (ngOnInit) for component initialization.
|
* Angular lifecycle hook (ngOnInit) for component initialization.
|
||||||
@@ -100,7 +99,7 @@ export class ChatsComponent implements OnInit, OnDestroy {
|
|||||||
this._markForCheck();
|
this._markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this._chatService.InitSubscribeToChatList();
|
this._chatService.InitSubscribeToChatList();
|
||||||
|
|
||||||
const savedChatId = localStorage.getItem('currentChatId');
|
const savedChatId = localStorage.getItem('currentChatId');
|
||||||
|
|
||||||
@@ -128,7 +127,7 @@ export class ChatsComponent implements OnInit, OnDestroy {
|
|||||||
this.filteredChats = this.chats;
|
this.filteredChats = this.chats;
|
||||||
} else {
|
} else {
|
||||||
const lowerCaseQuery = query.toLowerCase();
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
this.filteredChats = this.chats.filter(chat =>
|
this.filteredChats = this.chats.filter((chat) =>
|
||||||
chat.contact?.name.toLowerCase().includes(lowerCaseQuery)
|
chat.contact?.name.toLowerCase().includes(lowerCaseQuery)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -169,6 +168,4 @@ export class ChatsComponent implements OnInit, OnDestroy {
|
|||||||
private _markForCheck(): void {
|
private _markForCheck(): void {
|
||||||
this._changeDetectorRef.markForCheck();
|
this._changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,14 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 text-lg font-medium">{{ chat.contact?.name }}</div>
|
<div class="mt-4 text-lg font-medium">
|
||||||
<div class="text-secondary mt-0.5 text-md">
|
<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 }}
|
{{ chat.contact?.about }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatDrawer } from '@angular/material/sidenav';
|
import { MatDrawer } from '@angular/material/sidenav';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
import { Chat } from '../chat.types';
|
import { Chat } from '../chat.types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -15,7 +16,7 @@ import { Chat } from '../chat.types';
|
|||||||
encapsulation: ViewEncapsulation.None,
|
encapsulation: ViewEncapsulation.None,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [MatButtonModule, MatIconModule],
|
imports: [MatButtonModule, MatIconModule, RouterModule],
|
||||||
})
|
})
|
||||||
export class ContactInfoComponent {
|
export class ContactInfoComponent {
|
||||||
@Input() chat: Chat;
|
@Input() chat: Chat;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
.c-img{
|
.c-img {
|
||||||
max-width: 100%; border-radius: 10px;
|
max-width: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
.c-video
|
.c-video {
|
||||||
{
|
max-width: 100%;
|
||||||
max-width: 100%; border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,33 +176,44 @@
|
|||||||
>
|
>
|
||||||
<!-- Bubble -->
|
<!-- Bubble -->
|
||||||
<div
|
<div
|
||||||
class="relative max-w-3/4 rounded-lg px-2 py-2"
|
class="relative max-w-3/4 rounded-lg px-2 py-2"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'bg-gray-400 text-blue-50': message.isMine,
|
'bg-gray-400 text-blue-50':
|
||||||
'bg-gray-500 text-gray-50': !message.isMine
|
message.isMine,
|
||||||
}"
|
'bg-gray-500 text-gray-50':
|
||||||
>
|
!message.isMine,
|
||||||
<!-- Speech bubble tail -->
|
}"
|
||||||
@if (
|
>
|
||||||
last ||
|
<!-- Speech bubble tail -->
|
||||||
chat.messages[i + 1].isMine !== message.isMine
|
@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
|
<div
|
||||||
class="absolute bottom-0 w-3"
|
class="min-w-4 whitespace-normal break-words leading-5"
|
||||||
[ngClass]="{
|
[innerHTML]="
|
||||||
'-right-1 -mr-px mb-px text-gray-400': message.isMine,
|
parseContent(message.value)
|
||||||
'-left-1 -ml-px mb-px -scale-x-1 text-gray-500': !message.isMine
|
"
|
||||||
}"
|
></div>
|
||||||
>
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Time -->
|
<!-- Time -->
|
||||||
@if (
|
@if (
|
||||||
@@ -237,7 +248,7 @@
|
|||||||
class="flex items-end border-t bg-gray-50 p-4 dark:bg-transparent"
|
class="flex items-end border-t bg-gray-50 p-4 dark:bg-transparent"
|
||||||
>
|
>
|
||||||
<div class="my-px flex h-11 items-center">
|
<div class="my-px flex h-11 items-center">
|
||||||
<button mat-icon-button (click)="openGifDialog()">
|
<button mat-icon-button (click)="openGifDialog()">
|
||||||
<mat-icon
|
<mat-icon
|
||||||
[svgIcon]="'heroicons_outline:gif'"
|
[svgIcon]="'heroicons_outline:gif'"
|
||||||
></mat-icon>
|
></mat-icon>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
import { AngorMediaWatcherService } from '@angor/services/media-watcher';
|
||||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||||
import { CommonModule, DatePipe, NgClass, NgTemplateOutlet } from '@angular/common';
|
import {
|
||||||
|
CommonModule,
|
||||||
|
DatePipe,
|
||||||
|
NgClass,
|
||||||
|
NgTemplateOutlet,
|
||||||
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
@@ -19,17 +25,16 @@ import { MatInputModule } from '@angular/material/input';
|
|||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
import { RouterLink } from '@angular/router';
|
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 { AngorConfigService } from '@angor/services/config';
|
||||||
import { GifDialogComponent } from 'app/shared/gif-dialog/gif-dialog.component';
|
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
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({
|
@Component({
|
||||||
selector: 'chat-conversation',
|
selector: 'chat-conversation',
|
||||||
@@ -52,7 +57,7 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
|||||||
TextFieldModule,
|
TextFieldModule,
|
||||||
DatePipe,
|
DatePipe,
|
||||||
PickerComponent,
|
PickerComponent,
|
||||||
CommonModule
|
CommonModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ConversationComponent implements OnInit, OnDestroy {
|
export class ConversationComponent implements OnInit, OnDestroy {
|
||||||
@@ -68,7 +73,6 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
|||||||
isListening: boolean = false;
|
isListening: boolean = false;
|
||||||
userEdited: boolean = false;
|
userEdited: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _changeDetectorRef: ChangeDetectorRef,
|
private _changeDetectorRef: ChangeDetectorRef,
|
||||||
private _chatService: ChatService,
|
private _chatService: ChatService,
|
||||||
@@ -78,11 +82,15 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
|||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
private sanitizer: DomSanitizer
|
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) {
|
if (!SpeechRecognition) {
|
||||||
console.error('Speech recognition is not supported in this browser.');
|
console.error(
|
||||||
return;
|
'Speech recognition is not supported in this browser.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
this.recognition = new SpeechRecognition();
|
this.recognition = new SpeechRecognition();
|
||||||
this.recognition.lang = 'en-US';
|
this.recognition.lang = 'en-US';
|
||||||
@@ -91,41 +99,35 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
|||||||
this.setupRecognitionEvents();
|
this.setupRecognitionEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
openGifDialog(): void {
|
openGifDialog(): void {
|
||||||
const dialogRef = this.dialog.open(GifDialogComponent, {
|
const dialogRef = this.dialog.open(GifDialogComponent, {
|
||||||
width: '600px',
|
width: '600px',
|
||||||
maxHeight: '80vh',
|
maxHeight: '80vh',
|
||||||
data: { apiKey: 'LIVDSRZULELA' }
|
data: { apiKey: 'LIVDSRZULELA' },
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(result => {
|
dialogRef.afterClosed().subscribe((result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
|
const messageContent = result;
|
||||||
|
|
||||||
const messageContent = result
|
if (messageContent) {
|
||||||
|
|
||||||
if (messageContent) {
|
|
||||||
|
|
||||||
this.messageInput.nativeElement.value = '';
|
|
||||||
this._chatService.sendPrivateMessage(messageContent)
|
|
||||||
.then(() => {
|
|
||||||
this.messageInput.nativeElement.value = '';
|
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 = '';
|
this.finalTranscript = '';
|
||||||
})
|
this.userEdited = false;
|
||||||
.catch((error) => {
|
}
|
||||||
console.error('Failed to send message:', error);
|
}
|
||||||
});
|
});
|
||||||
this.finalTranscript = '';
|
|
||||||
this.userEdited = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this._angorConfigService.config$.subscribe((config) => {
|
this._angorConfigService.config$.subscribe((config) => {
|
||||||
if (config.scheme === 'auto') {
|
if (config.scheme === 'auto') {
|
||||||
@@ -140,22 +142,18 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
|||||||
.subscribe((chat: Chat) => {
|
.subscribe((chat: Chat) => {
|
||||||
this.chat = chat;
|
this.chat = chat;
|
||||||
|
|
||||||
|
|
||||||
this._changeDetectorRef.markForCheck();
|
this._changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this._angorMediaWatcherService.onMediaChange$
|
this._angorMediaWatcherService.onMediaChange$
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe(({ matchingAliases }) => {
|
.subscribe(({ matchingAliases }) => {
|
||||||
|
|
||||||
if (matchingAliases.includes('lg')) {
|
if (matchingAliases.includes('lg')) {
|
||||||
this.drawerMode = 'side';
|
this.drawerMode = 'side';
|
||||||
} else {
|
} else {
|
||||||
this.drawerMode = 'over';
|
this.drawerMode = 'over';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this._changeDetectorRef.markForCheck();
|
this._changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -163,50 +161,50 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
|||||||
parseContent(content: string): SafeHtml {
|
parseContent(content: string): SafeHtml {
|
||||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||||
const cleanedContent = content.replace(/["]+/g, '');
|
const cleanedContent = content.replace(/["]+/g, '');
|
||||||
const parsedContent = cleanedContent.replace(urlRegex, (url) => {
|
const parsedContent = cleanedContent
|
||||||
if (url.match(/\.(jpeg|jpg|gif|png|bmp|svg|webp|tiff)$/) != null) {
|
.replace(urlRegex, (url) => {
|
||||||
return `<img src="${url}" alt="Image" width="100%" class="c-img">`;
|
if (
|
||||||
} else if (url.match(/\.(mp4|webm)$/) != null) {
|
url.match(/\.(jpeg|jpg|gif|png|bmp|svg|webp|tiff)$/) != 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=)/)) {
|
return `<img src="${url}" alt="Image" width="100%" class="c-img">`;
|
||||||
let videoId;
|
} else if (url.match(/\.(mp4|webm)$/) != null) {
|
||||||
if (url.includes('youtu.be/')) {
|
return `<video controls width="100%" class="c-video"><source src="${url}" type="video/mp4">Your browser does not support the video tag.</video>`;
|
||||||
videoId = url.split('youtu.be/')[1];
|
} else if (url.match(/(youtu\.be\/|youtube\.com\/watch\?v=)/)) {
|
||||||
} else if (url.includes('watch?v=')) {
|
let videoId;
|
||||||
const urlParams = new URLSearchParams(url.split('?')[1]);
|
if (url.includes('youtu.be/')) {
|
||||||
videoId = urlParams.get('v');
|
videoId = url.split('youtu.be/')[1];
|
||||||
}
|
} else if (url.includes('watch?v=')) {
|
||||||
return `<iframe width="100%" class="c-video" src="https://www.youtube.com/embed/${videoId}" frameborder="0" allowfullscreen></iframe>`;
|
const urlParams = new URLSearchParams(
|
||||||
} else {
|
url.split('?')[1]
|
||||||
return `<a href="${url}" target="_blank">${url}</a>`;
|
);
|
||||||
}
|
videoId = urlParams.get('v');
|
||||||
}).replace(/\n/g, '<br>');
|
}
|
||||||
|
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);
|
return this.sanitizer.bypassSecurityTrustHtml(parsedContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('input')
|
@HostListener('input')
|
||||||
@HostListener('ngModelChange')
|
@HostListener('ngModelChange')
|
||||||
private _resizeMessageInput(): void {
|
private _resizeMessageInput(): void {
|
||||||
|
|
||||||
this._ngZone.runOutsideAngular(() => {
|
this._ngZone.runOutsideAngular(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
||||||
this.messageInput.nativeElement.style.height = 'auto';
|
this.messageInput.nativeElement.style.height = 'auto';
|
||||||
|
|
||||||
|
|
||||||
this._changeDetectorRef.detectChanges();
|
this._changeDetectorRef.detectChanges();
|
||||||
|
|
||||||
|
|
||||||
this.messageInput.nativeElement.style.height = `${this.messageInput.nativeElement.scrollHeight}px`;
|
this.messageInput.nativeElement.style.height = `${this.messageInput.nativeElement.scrollHeight}px`;
|
||||||
|
|
||||||
|
|
||||||
this._changeDetectorRef.detectChanges();
|
this._changeDetectorRef.detectChanges();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setupRecognitionEvents(): void {
|
setupRecognitionEvents(): void {
|
||||||
this.recognition.onresult = (event: any) => {
|
this.recognition.onresult = (event: any) => {
|
||||||
let interimTranscript = '';
|
let interimTranscript = '';
|
||||||
@@ -220,9 +218,9 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!this.userEdited) {
|
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 {
|
toggleSpeechRecognition(): void {
|
||||||
this.finalTranscript = '';
|
this.finalTranscript = '';
|
||||||
if (this.isListening) {
|
if (this.isListening) {
|
||||||
@@ -248,43 +245,32 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleUserInput(event: Event): void {
|
handleUserInput(event: Event): void {
|
||||||
this.userEdited = true;
|
this.userEdited = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
|
||||||
this._unsubscribeAll.next(null);
|
this._unsubscribeAll.next(null);
|
||||||
this._unsubscribeAll.complete();
|
this._unsubscribeAll.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
openContactInfo(): void {
|
openContactInfo(): void {
|
||||||
|
|
||||||
this.drawerOpened = true;
|
this.drawerOpened = true;
|
||||||
|
|
||||||
|
|
||||||
this._changeDetectorRef.markForCheck();
|
this._changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
resetChat(): void {
|
resetChat(): void {
|
||||||
this._chatService.resetChat();
|
this._chatService.resetChat();
|
||||||
|
|
||||||
|
|
||||||
this.drawerOpened = false;
|
this.drawerOpened = false;
|
||||||
|
|
||||||
|
|
||||||
this._changeDetectorRef.markForCheck();
|
this._changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleMuteNotifications(): void {
|
toggleMuteNotifications(): void {
|
||||||
|
|
||||||
this.chat.muted = !this.chat.muted;
|
this.chat.muted = !this.chat.muted;
|
||||||
|
|
||||||
|
|
||||||
this._chatService.updateChat(this.chat.id, this.chat).subscribe();
|
this._chatService.updateChat(this.chat.id, this.chat).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,9 +278,10 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
|||||||
return item.id || index;
|
return item.id || index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
detectSystemTheme() {
|
detectSystemTheme() {
|
||||||
const darkSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
const darkSchemeMedia = window.matchMedia(
|
||||||
|
'(prefers-color-scheme: dark)'
|
||||||
|
);
|
||||||
this.darkMode = darkSchemeMedia.matches;
|
this.darkMode = darkSchemeMedia.matches;
|
||||||
|
|
||||||
darkSchemeMedia.addEventListener('change', (event) => {
|
darkSchemeMedia.addEventListener('change', (event) => {
|
||||||
@@ -312,10 +299,10 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
|||||||
sendMessage(): void {
|
sendMessage(): void {
|
||||||
const messageContent = this.messageInput.nativeElement.value.trim();
|
const messageContent = this.messageInput.nativeElement.value.trim();
|
||||||
|
|
||||||
if (messageContent) {
|
if (messageContent) {
|
||||||
|
|
||||||
this.messageInput.nativeElement.value = '';
|
this.messageInput.nativeElement.value = '';
|
||||||
this._chatService.sendPrivateMessage(messageContent)
|
this._chatService
|
||||||
|
.sendPrivateMessage(messageContent)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.messageInput.nativeElement.value = '';
|
this.messageInput.nativeElement.value = '';
|
||||||
this.finalTranscript = '';
|
this.finalTranscript = '';
|
||||||
@@ -336,5 +323,4 @@ export class ConversationComponent implements OnInit, OnDestroy {
|
|||||||
toggleEmojiPicker() {
|
toggleEmojiPicker() {
|
||||||
this.showEmojiPicker = !this.showEmojiPicker;
|
this.showEmojiPicker = !this.showEmojiPicker;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
) {
|
) {
|
||||||
<div
|
<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"
|
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) }}
|
{{ contact.name.charAt(0) }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
<div
|
<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"
|
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)"
|
(click)="openChat(contact)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex h-10 w-10 flex-0 items-center justify-center overflow-hidden rounded-full"
|
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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatDrawer } from '@angular/material/sidenav';
|
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 { Router } from '@angular/router';
|
||||||
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
import { ChatService } from '../chat.service';
|
||||||
|
import { Contact } from '../chat.types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'chat-new-chat',
|
selector: 'chat-new-chat',
|
||||||
@@ -30,8 +30,10 @@ export class NewChatComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
constructor(private _chatService: ChatService, private router: Router) {}
|
constructor(
|
||||||
|
private _chatService: ChatService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// Contacts
|
// Contacts
|
||||||
@@ -42,14 +44,12 @@ export class NewChatComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
// Unsubscribe from all subscriptions
|
// Unsubscribe from all subscriptions
|
||||||
this._unsubscribeAll.next(null);
|
this._unsubscribeAll.next(null);
|
||||||
this._unsubscribeAll.complete();
|
this._unsubscribeAll.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
trackByFn(index: number, item: any): any {
|
trackByFn(index: number, item: any): any {
|
||||||
return item.id || index;
|
return item.id || index;
|
||||||
}
|
}
|
||||||
@@ -65,8 +65,7 @@ export class NewChatComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
this.drawer.close();
|
this.drawer.close();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatDrawer } from '@angular/material/sidenav';
|
import { MatDrawer } from '@angular/material/sidenav';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
import { Profile } from '../chat.types';
|
|
||||||
import { ChatService } from '../chat.service';
|
import { ChatService } from '../chat.service';
|
||||||
|
import { Profile } from '../chat.types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'chat-profile',
|
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">
|
<div class="absolute inset-0 flex min-w-0 flex-col overflow-y-auto">
|
||||||
<!-- Header -->
|
<!-- 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 -->
|
<!-- Background -->
|
||||||
<svg class="pointer-events-none absolute inset-0" viewBox="0 0 960 540" width="100%" height="100%"
|
<svg
|
||||||
preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
|
class="pointer-events-none absolute inset-0"
|
||||||
<g class="text-gray-700 opacity-25" fill="none" stroke="currentColor" stroke-width="100">
|
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="196" cy="23"></circle>
|
||||||
<circle r="234" cx="790" cy="491"></circle>
|
<circle r="234" cx="790" cy="491"></circle>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="relative z-10 flex flex-col items-center">
|
<div class="relative z-10 flex flex-col items-center">
|
||||||
<h2 class="text-xl font-semibold">Explore Projects</h2>
|
<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?
|
What’s your next investment?
|
||||||
</div>
|
</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
|
Check out our projects and find your next investment
|
||||||
opportunity.
|
opportunity.
|
||||||
</div>
|
</div>
|
||||||
@@ -23,110 +40,188 @@
|
|||||||
|
|
||||||
<!-- Main -->
|
<!-- Main -->
|
||||||
<div class="p-6 sm:p-10">
|
<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 -->
|
<!-- 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 -->
|
<!-- Search bar with clear button -->
|
||||||
<div class="flex items-center space-x-2 w-full sm:w-auto">
|
<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-form-field
|
||||||
<mat-icon matPrefix class="icon-size-5" [svgIcon]="'heroicons_solid:magnifying-glass'"></mat-icon>
|
class="mt-4 w-full sm:w-80"
|
||||||
<input (keyup.enter)="filterByQuery(query.value)" placeholder="Search ..." matInput #query />
|
[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>
|
</mat-form-field>
|
||||||
<!-- Clear search button -->
|
<!-- Clear search button -->
|
||||||
<button mat-icon-button color="warn" *ngIf="showCloseSearchButton" (click)="resetSearch(query)" class="mt-4">
|
<button
|
||||||
<mat-icon [svgIcon]="'heroicons_solid:x-mark'"></mat-icon>
|
mat-icon-button
|
||||||
|
color="warn"
|
||||||
|
*ngIf="showCloseSearchButton"
|
||||||
|
(click)="resetSearch(query)"
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
|
<mat-icon
|
||||||
|
[svgIcon]="'heroicons_solid:x-mark'"
|
||||||
|
></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button mat-icon-button color="success" *ngIf="!showCloseSearchButton" (click)="filterByQuery(query.value)" class="mt-4">
|
<button
|
||||||
<mat-icon [svgIcon]="'heroicons_solid:magnifying-glass'"></mat-icon>
|
mat-icon-button
|
||||||
|
color="success"
|
||||||
|
*ngIf="!showCloseSearchButton"
|
||||||
|
(click)="filterByQuery(query.value)"
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
|
<mat-icon
|
||||||
|
[svgIcon]="'heroicons_solid:magnifying-glass'"
|
||||||
|
></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Toggle completed -->
|
<!-- 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
|
Hide completed
|
||||||
</mat-slide-toggle>
|
</mat-slide-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-auto flex w-full flex-auto flex-col sm:max-w-5xl">
|
<div class="mx-auto flex w-full flex-auto flex-col sm:max-w-5xl">
|
||||||
<!-- Project Cards -->
|
<!-- 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 -->
|
<!-- Loop through projects and render cards -->
|
||||||
<ng-container *ngFor="let project of filteredProjects">
|
<ng-container *ngFor="let project of filteredProjects">
|
||||||
<angor-card class="filter-info flex w-full flex-col">
|
<angor-card class="filter-info flex w-full flex-col">
|
||||||
<div class="flex h-32">
|
<div class="flex h-32">
|
||||||
<img class="object-cover" [src]="
|
<img
|
||||||
|
class="object-cover"
|
||||||
|
[src]="
|
||||||
getSafeUrl(project?.banner, true) ||
|
getSafeUrl(project?.banner, true) ||
|
||||||
'images/pages/profile/cover.jpg'
|
'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>
|
||||||
<div class="flex px-8">
|
<div class="flex px-8">
|
||||||
<div class="bg-card -mt-12 rounded-full p-1">
|
<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) ||
|
getSafeUrl(project?.picture, false) ||
|
||||||
'images/avatars/avatar-placeholder.png'
|
'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>
|
</div>
|
||||||
<div class="flex flex-col px-8 pb-6 pt-4">
|
<div class="flex flex-col px-8 pb-6 pt-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="mr-4 min-w-0 flex-1">
|
<div class="mr-4 min-w-0 flex-1">
|
||||||
@if (project.displayName || project.name) {
|
@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)
|
goToProjectDetails(project)
|
||||||
">
|
"
|
||||||
{{
|
>
|
||||||
project.displayName ||
|
{{
|
||||||
project.nostrPubKey
|
project.displayName ||
|
||||||
}}
|
project.nostrPubKey
|
||||||
</div>
|
}}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
@if (
|
@if (
|
||||||
!project.name && !project.displayName
|
!project.name && !project.displayName
|
||||||
) {
|
) {
|
||||||
<div class="truncate text-2xl font-semibold leading-tight">
|
<div
|
||||||
{{
|
class="truncate text-2xl font-semibold leading-tight"
|
||||||
project.displayName ||
|
>
|
||||||
project.nostrPubKey
|
{{
|
||||||
}}
|
project.displayName ||
|
||||||
</div>
|
project.nostrPubKey
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
<div class="text-secondary mt-1 truncate leading-tight">
|
<div
|
||||||
|
class="text-secondary mt-1 truncate leading-tight"
|
||||||
|
>
|
||||||
{{
|
{{
|
||||||
project.about ||
|
project.about ||
|
||||||
'No description available'
|
'No description available'
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (project.displayName || project.name) {
|
@if (project.displayName || project.name) {
|
||||||
<div class="flex h-10 w-10 items-center justify-center rounded-full border">
|
<div
|
||||||
<button mat-icon-button (click)="
|
class="flex h-10 w-10 items-center justify-center rounded-full border"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
(click)="
|
||||||
openChat(project.nostrPubKey)
|
openChat(project.nostrPubKey)
|
||||||
">
|
"
|
||||||
<mat-icon class="icon-size-5" [svgIcon]="
|
>
|
||||||
|
<mat-icon
|
||||||
|
class="icon-size-5"
|
||||||
|
[svgIcon]="
|
||||||
'heroicons_outline:chat-bubble-left-right'
|
'heroicons_outline:chat-bubble-left-right'
|
||||||
"></mat-icon>
|
"
|
||||||
</button>
|
></mat-icon>
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-6 w-full border-t" />
|
<hr class="my-6 w-full border-t" />
|
||||||
<div class="flex items-center justify-between">
|
<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 }}
|
{{ project.totalInvestmentsCount || 0 }}
|
||||||
investors
|
investors
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ng-container *ngFor="
|
<ng-container
|
||||||
let investor of [].constructor(project.totalInvestmentsCount || 0);
|
*ngFor="
|
||||||
let i = index
|
let investor of [].constructor(
|
||||||
">
|
project.totalInvestmentsCount ||
|
||||||
|
0
|
||||||
|
);
|
||||||
|
let i = index
|
||||||
|
"
|
||||||
|
>
|
||||||
<ng-container *ngIf="i < 10">
|
<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]="{
|
[ngClass]="{
|
||||||
'-ml-3': project.totalInvestmentsCount > 1 && i > 0
|
'-ml-3':
|
||||||
}" [src]="'images/avatars/avatar-placeholder.png'"
|
project.totalInvestmentsCount >
|
||||||
alt="Investor avatar {{ i + 1 }}" />
|
1 && i > 0,
|
||||||
|
}"
|
||||||
|
[src]="
|
||||||
|
'images/avatars/avatar-placeholder.png'
|
||||||
|
"
|
||||||
|
alt="Investor avatar {{
|
||||||
|
i + 1
|
||||||
|
}}"
|
||||||
|
/>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,18 +230,32 @@
|
|||||||
</angor-card>
|
</angor-card>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="filteredProjects.length ==0">
|
<ng-container *ngIf="filteredProjects.length == 0">
|
||||||
<div class="flex flex-auto flex-col items-center justify-center bg-gray-100 dark:bg-transparent">
|
<div
|
||||||
<mat-icon class="icon-size-24"
|
class="flex flex-auto flex-col items-center justify-center bg-gray-100 dark:bg-transparent"
|
||||||
[svgIcon]="'heroicons_outline:archive-box-x-mark'"></mat-icon>
|
>
|
||||||
<div class="text-secondary mt-4 text-2xl font-semibold tracking-tight">
|
<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
|
No project
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<!-- Load More Button -->
|
<!-- Load More Button -->
|
||||||
<div *ngIf="filteredProjects.length >0" class="mt-10 flex justify-center">
|
<div
|
||||||
<button mat-raised-button color="primary" (click)="loadProjects()" [disabled]="loading">
|
*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' }}
|
{{ loading ? 'Loading...' : 'Load More Projects' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
import { Component, OnInit, OnDestroy, ViewEncapsulation, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
|
import { AngorCardComponent } from '@angor/components/card';
|
||||||
import { Router } from '@angular/router';
|
import { AngorFindByKeyPipe } from '@angor/pipes/find-by-key';
|
||||||
import { ProjectsService } from '../../services/projects.service';
|
import { CdkScrollable } from '@angular/cdk/scrolling';
|
||||||
import { StateService } from '../../services/state.service';
|
import {
|
||||||
|
CommonModule,
|
||||||
|
I18nPluralPipe,
|
||||||
|
NgClass,
|
||||||
|
PercentPipe,
|
||||||
|
} from '@angular/common';
|
||||||
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
ViewEncapsulation,
|
||||||
|
} from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatOptionModule } from '@angular/material/core';
|
import { MatOptionModule } from '@angular/material/core';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
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 { MatSelectModule } from '@angular/material/select';
|
||||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
import { RouterLink } from '@angular/router';
|
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||||
import { AngorCardComponent } from '@angor/components/card';
|
import { Router, RouterLink } from '@angular/router';
|
||||||
import { AngorFindByKeyPipe } from '@angor/pipes/find-by-key';
|
import { Project } from 'app/interface/project.interface';
|
||||||
import { CdkScrollable } from '@angular/cdk/scrolling';
|
import { IndexedDBService } from 'app/services/indexed-db.service';
|
||||||
import { NgClass, PercentPipe, I18nPluralPipe, CommonModule } from '@angular/common';
|
|
||||||
import { MetadataService } from 'app/services/metadata.service';
|
import { MetadataService } from 'app/services/metadata.service';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
import { IndexedDBService } from 'app/services/indexed-db.service';
|
import { ProjectsService } from '../../services/projects.service';
|
||||||
import { Project } from 'app/interface/project.interface';
|
import { StateService } from '../../services/state.service';
|
||||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
|
||||||
import { Contact } from '../chat/chat.types';
|
|
||||||
import { ChatService } from '../chat/chat.service';
|
import { ChatService } from '../chat/chat.service';
|
||||||
|
import { Contact } from '../chat/chat.types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'explore',
|
selector: 'explore',
|
||||||
@@ -30,10 +40,23 @@ import { ChatService } from '../chat/chat.service';
|
|||||||
templateUrl: './explore.component.html',
|
templateUrl: './explore.component.html',
|
||||||
encapsulation: ViewEncapsulation.None,
|
encapsulation: ViewEncapsulation.None,
|
||||||
imports: [
|
imports: [
|
||||||
MatButtonModule, RouterLink, MatIconModule, AngorCardComponent,
|
MatButtonModule,
|
||||||
CdkScrollable, MatFormFieldModule, MatSelectModule, MatOptionModule,
|
RouterLink,
|
||||||
MatInputModule, MatSlideToggleModule, NgClass, MatTooltipModule,
|
MatIconModule,
|
||||||
MatProgressBarModule, AngorFindByKeyPipe, PercentPipe, I18nPluralPipe, CommonModule
|
AngorCardComponent,
|
||||||
|
CdkScrollable,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatOptionModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatSlideToggleModule,
|
||||||
|
NgClass,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatProgressBarModule,
|
||||||
|
AngorFindByKeyPipe,
|
||||||
|
PercentPipe,
|
||||||
|
I18nPluralPipe,
|
||||||
|
CommonModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ExploreComponent implements OnInit, OnDestroy {
|
export class ExploreComponent implements OnInit, OnDestroy {
|
||||||
@@ -45,7 +68,6 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
|||||||
filteredProjects: Project[] = [];
|
filteredProjects: Project[] = [];
|
||||||
showCloseSearchButton: boolean = false;
|
showCloseSearchButton: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private projectService: ProjectsService,
|
private projectService: ProjectsService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -55,7 +77,7 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private sanitizer: DomSanitizer,
|
private sanitizer: DomSanitizer,
|
||||||
private _chatService: ChatService
|
private _chatService: ChatService
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
this.loadInitialProjects();
|
this.loadInitialProjects();
|
||||||
@@ -96,21 +118,26 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
|||||||
this.filteredProjects = [...this.projects];
|
this.filteredProjects = [...this.projects];
|
||||||
this.stateService.setProjects(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);
|
await this.loadMetadataForProjects(pubkeys);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError('Error fetching projects from service');
|
this.handleError('Error fetching projects from service');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private subscribeToMetadataUpdates(): void {
|
private subscribeToMetadataUpdates(): void {
|
||||||
this.indexedDBService.getMetadataStream()
|
this.indexedDBService
|
||||||
|
.getMetadataStream()
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe((updatedMetadata: any) => {
|
.subscribe((updatedMetadata: any) => {
|
||||||
if (updatedMetadata) {
|
if (updatedMetadata) {
|
||||||
const projectToUpdate = this.projects.find(p => p.nostrPubKey === updatedMetadata.pubkey);
|
const projectToUpdate = this.projects.find(
|
||||||
|
(p) => p.nostrPubKey === updatedMetadata.pubkey
|
||||||
|
);
|
||||||
if (projectToUpdate) {
|
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[] {
|
private getProjectsWithoutMetadata(): string[] {
|
||||||
return this.projects
|
return this.projects
|
||||||
.filter(project => !project.displayName || !project.about)
|
.filter((project) => !project.displayName || !project.about)
|
||||||
.map(project => project.nostrPubKey);
|
.map((project) => project.nostrPubKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadMetadataForProjects(pubkeys: string[]): Promise<void> {
|
private async loadMetadataForProjects(pubkeys: string[]): Promise<void> {
|
||||||
const metadataPromises = pubkeys.map(async (pubkey) => {
|
const metadataPromises = pubkeys.map(async (pubkey) => {
|
||||||
// Check cache first
|
// Check cache first
|
||||||
const cachedMetadata = await this.indexedDBService.getUserMetadata(pubkey);
|
const cachedMetadata =
|
||||||
|
await this.indexedDBService.getUserMetadata(pubkey);
|
||||||
if (cachedMetadata) {
|
if (cachedMetadata) {
|
||||||
return { pubkey, metadata: cachedMetadata };
|
return { pubkey, metadata: cachedMetadata };
|
||||||
}
|
}
|
||||||
@@ -136,13 +164,15 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Filter out nulls (which represent pubkeys without cached metadata)
|
// Filter out nulls (which represent pubkeys without cached metadata)
|
||||||
const missingPubkeys = metadataResults
|
const missingPubkeys = metadataResults
|
||||||
.filter(result => result === null)
|
.filter((result) => result === null)
|
||||||
.map((_, index) => pubkeys[index]);
|
.map((_, index) => pubkeys[index]);
|
||||||
|
|
||||||
// Update projects that have cached metadata
|
// Update projects that have cached metadata
|
||||||
metadataResults.forEach(result => {
|
metadataResults.forEach((result) => {
|
||||||
if (result && result.metadata) {
|
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) {
|
if (project) {
|
||||||
this.updateProjectMetadata(project, result.metadata);
|
this.updateProjectMetadata(project, result.metadata);
|
||||||
}
|
}
|
||||||
@@ -151,80 +181,103 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Fetch metadata for pubkeys that are not cached
|
// Fetch metadata for pubkeys that are not cached
|
||||||
if (missingPubkeys.length > 0) {
|
if (missingPubkeys.length > 0) {
|
||||||
await this.metadataService.fetchMetadataForMultipleKeys(missingPubkeys)
|
await this.metadataService
|
||||||
|
.fetchMetadataForMultipleKeys(missingPubkeys)
|
||||||
.then((metadataList: any[]) => {
|
.then((metadataList: any[]) => {
|
||||||
metadataList.forEach(metadata => {
|
metadataList.forEach((metadata) => {
|
||||||
const project = this.projects.find(p => p.nostrPubKey === metadata.pubkey);
|
const project = this.projects.find(
|
||||||
|
(p) => p.nostrPubKey === metadata.pubkey
|
||||||
|
);
|
||||||
if (project) {
|
if (project) {
|
||||||
this.updateProjectMetadata(project, metadata);
|
this.updateProjectMetadata(project, metadata);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.changeDetectorRef.detectChanges();
|
this.changeDetectorRef.detectChanges();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('Error fetching metadata for projects:', error);
|
console.error(
|
||||||
|
'Error fetching metadata for projects:',
|
||||||
|
error
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async loadProjects(): Promise<void> {
|
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.loading = true;
|
||||||
|
|
||||||
this.projectService.fetchProjects().then(async (projects: Project[]) => {
|
this.projectService
|
||||||
if (projects.length === 0 && this.projects.length === 0) {
|
.fetchProjects()
|
||||||
this.errorMessage = 'No projects found';
|
.then(async (projects: Project[]) => {
|
||||||
} else if (projects.length === 0) {
|
if (projects.length === 0 && this.projects.length === 0) {
|
||||||
this.errorMessage = 'No more projects found';
|
this.errorMessage = 'No projects found';
|
||||||
} else {
|
} else if (projects.length === 0) {
|
||||||
this.projects = [...this.projects, ...projects];
|
this.errorMessage = 'No more projects found';
|
||||||
this.filteredProjects = [...this.projects];
|
} 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.projects.forEach((project) =>
|
||||||
}
|
this.subscribeToProjectMetadata(project)
|
||||||
this.loading = false;
|
);
|
||||||
this.changeDetectorRef.detectChanges();
|
}
|
||||||
}).catch((error: any) => {
|
this.loading = false;
|
||||||
console.error('Error fetching projects:', error);
|
this.changeDetectorRef.detectChanges();
|
||||||
this.errorMessage = 'Error fetching projects. Please try again later.';
|
})
|
||||||
this.loading = false;
|
.catch((error: any) => {
|
||||||
this.changeDetectorRef.detectChanges();
|
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> {
|
async loadMetadataForProject(project: Project): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const metadata = await this.metadataService.fetchMetadataWithCache(project.nostrPubKey);
|
const metadata = await this.metadataService.fetchMetadataWithCache(
|
||||||
|
project.nostrPubKey
|
||||||
|
);
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
this.updateProjectMetadata(project, metadata);
|
this.updateProjectMetadata(project, metadata);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`No metadata found for project ${project.nostrPubKey}`);
|
console.warn(
|
||||||
|
`No metadata found for project ${project.nostrPubKey}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 {
|
updateProjectMetadata(project: Project, metadata: any): void {
|
||||||
|
|
||||||
const updatedProject: Project = {
|
const updatedProject: Project = {
|
||||||
...project,
|
...project,
|
||||||
displayName: metadata.name || '',
|
displayName: metadata.name || '',
|
||||||
about: metadata.about ? metadata.about.replace(/<\/?[^>]+(>|$)/g, '') : '',
|
about: metadata.about
|
||||||
|
? metadata.about.replace(/<\/?[^>]+(>|$)/g, '')
|
||||||
|
: '',
|
||||||
picture: metadata.picture || '',
|
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) {
|
if (index !== -1) {
|
||||||
this.projects[index] = updatedProject;
|
this.projects[index] = updatedProject;
|
||||||
this.projects = [...this.projects];
|
this.projects = [...this.projects];
|
||||||
@@ -235,11 +288,18 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscribeToProjectMetadata(project: Project): void {
|
subscribeToProjectMetadata(project: Project): void {
|
||||||
this.metadataService.getMetadataStream()
|
this.metadataService
|
||||||
|
.getMetadataStream()
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe((updatedMetadata: any) => {
|
.subscribe((updatedMetadata: any) => {
|
||||||
if (updatedMetadata && updatedMetadata.pubkey === project.nostrPubKey) {
|
if (
|
||||||
this.updateProjectMetadata(project, updatedMetadata.metadata);
|
updatedMetadata &&
|
||||||
|
updatedMetadata.pubkey === project.nostrPubKey
|
||||||
|
) {
|
||||||
|
this.updateProjectMetadata(
|
||||||
|
project,
|
||||||
|
updatedMetadata.metadata
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -247,7 +307,8 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
|||||||
goToProjectDetails(project: Project): void {
|
goToProjectDetails(project: Project): void {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
this.projectService.fetchAndSaveProjectStats(project.projectIdentifier)
|
this.projectService
|
||||||
|
.fetchAndSaveProjectStats(project.projectIdentifier)
|
||||||
.then((stats) => {
|
.then((stats) => {
|
||||||
if (stats) {
|
if (stats) {
|
||||||
this.navigateToProfile(project.nostrPubKey);
|
this.navigateToProfile(project.nostrPubKey);
|
||||||
@@ -278,17 +339,30 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const lowerCaseQuery = query.toLowerCase();
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
|
|
||||||
this.filteredProjects = this.projects.filter(project => {
|
this.filteredProjects = this.projects.filter((project) => {
|
||||||
return (
|
return (
|
||||||
(project.displayName && project.displayName.toLowerCase().includes(lowerCaseQuery)) ||
|
(project.displayName &&
|
||||||
(project.about && project.about.toLowerCase().includes(lowerCaseQuery)) ||
|
project.displayName
|
||||||
(project.displayName && project.displayName.toLowerCase().includes(lowerCaseQuery)) ||
|
.toLowerCase()
|
||||||
(project.nostrPubKey && project.nostrPubKey.toLowerCase().includes(lowerCaseQuery)) ||
|
.includes(lowerCaseQuery)) ||
|
||||||
(project.projectIdentifier && project.projectIdentifier.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();
|
this.changeDetectorRef.detectChanges();
|
||||||
}
|
}
|
||||||
@@ -299,9 +373,7 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
|||||||
this.showCloseSearchButton = false;
|
this.showCloseSearchButton = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCompleted(event: any): void {
|
toggleCompleted(event: any): void {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this._unsubscribeAll.next(null);
|
this._unsubscribeAll.next(null);
|
||||||
@@ -319,7 +391,9 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
|||||||
if (url && typeof url === 'string' && this.isImageUrl(url)) {
|
if (url && typeof url === 'string' && this.isImageUrl(url)) {
|
||||||
return this.sanitizer.bypassSecurityTrustUrl(url);
|
return this.sanitizer.bypassSecurityTrustUrl(url);
|
||||||
} else {
|
} 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);
|
return this.sanitizer.bypassSecurityTrustUrl(defaultImage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,26 +404,34 @@ export class ExploreComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
async openChat(publicKey: string): Promise<void> {
|
async openChat(publicKey: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const metadata = await this.metadataService.fetchMetadataWithCache(publicKey);
|
const metadata =
|
||||||
|
await this.metadataService.fetchMetadataWithCache(publicKey);
|
||||||
|
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
const contact: Contact = {
|
const contact: Contact = {
|
||||||
pubKey: publicKey,
|
pubKey: publicKey,
|
||||||
name: metadata.name || 'Unknown',
|
name: metadata.name || 'Unknown',
|
||||||
picture: metadata.picture || '/images/avatars/avatar-placeholder.png',
|
picture:
|
||||||
|
metadata.picture ||
|
||||||
|
'/images/avatars/avatar-placeholder.png',
|
||||||
about: metadata.about || '',
|
about: metadata.about || '',
|
||||||
displayName: metadata.displayName || metadata.name || 'Unknown',
|
displayName:
|
||||||
|
metadata.displayName || metadata.name || 'Unknown',
|
||||||
};
|
};
|
||||||
|
|
||||||
this._chatService.getChatById(contact.pubKey, contact).subscribe((chat) => {
|
this._chatService
|
||||||
this.router.navigate(['/chat', contact.pubKey]);
|
.getChatById(contact.pubKey, contact)
|
||||||
});
|
.subscribe((chat) => {
|
||||||
|
this.router.navigate(['/chat', contact.pubKey]);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error('No metadata found for the public key:', publicKey);
|
console.error(
|
||||||
|
'No metadata found for the public key:',
|
||||||
|
publicKey
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error opening chat:', error);
|
console.error('Error opening chat:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ExploreComponent } from 'app/components/explore/explore.component';
|
|||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
path : '',
|
path: '',
|
||||||
component: ExploreComponent,
|
component: ExploreComponent,
|
||||||
},
|
},
|
||||||
] as Routes;
|
] as Routes;
|
||||||
|
|||||||
@@ -3,10 +3,18 @@
|
|||||||
<div class="prose prose-sm mx-auto max-w-none">
|
<div class="prose prose-sm mx-auto max-w-none">
|
||||||
<h1>Angor Hub</h1>
|
<h1>Angor Hub</h1>
|
||||||
<p>
|
<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>
|
||||||
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { RouterLink } from '@angular/router';
|
|||||||
templateUrl: './home.component.html',
|
templateUrl: './home.component.html',
|
||||||
encapsulation: ViewEncapsulation.None,
|
encapsulation: ViewEncapsulation.None,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [MatButtonModule, RouterLink, MatIconModule],
|
imports: [MatButtonModule, RouterLink, MatIconModule ],
|
||||||
})
|
})
|
||||||
export class LandingHomeComponent {
|
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 { TextFieldModule } from '@angular/cdk/text-field';
|
||||||
import { CommonModule, NgClass } from '@angular/common';
|
import { CommonModule, NgClass } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
ViewEncapsulation,
|
ElementRef,
|
||||||
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
OnDestroy
|
ViewChild,
|
||||||
|
ViewEncapsulation,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatDividerModule } from '@angular/material/divider';
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
import { MatSlideToggle } from '@angular/material/slide-toggle';
|
||||||
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 { MatSnackBar } from '@angular/material/snack-bar';
|
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 { bech32 } from '@scure/base';
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { QRCodeModule } from 'angularx-qrcode';
|
import { QRCodeModule } from 'angularx-qrcode';
|
||||||
import { Clipboard } from '@angular/cdk/clipboard';
|
import { PaginatedEventService } from 'app/services/event.service';
|
||||||
import { SendDialogComponent } from './zap/send-dialog/send-dialog.component';
|
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 { 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({
|
@Component({
|
||||||
selector: 'profile',
|
selector: 'profile',
|
||||||
templateUrl: './profile.component.html',
|
templateUrl: './profile.component.html',
|
||||||
encapsulation: ViewEncapsulation.None,
|
styleUrls: ['./profile.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
RouterLink,
|
RouterLink,
|
||||||
@@ -56,28 +74,54 @@ import { ReceiveDialogComponent } from './zap/receive-dialog/receive-dialog.comp
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
QRCodeModule,
|
QRCodeModule,
|
||||||
|
PickerComponent,
|
||||||
|
MatSlideToggle,
|
||||||
|
|
||||||
|
SafeUrlPipe,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
InfiniteScrollModule,
|
||||||
|
EventListComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ProfileComponent implements OnInit, OnDestroy {
|
export class ProfileComponent implements OnInit, OnDestroy {
|
||||||
|
@ViewChild('eventInput', { static: false }) eventInput: ElementRef;
|
||||||
|
@ViewChild('commentInput') commentInput: ElementRef;
|
||||||
|
|
||||||
|
darkMode: boolean = false;
|
||||||
isLoading: boolean = true;
|
isLoading: boolean = true;
|
||||||
errorMessage: string | null = null;
|
errorMessage: string | null = null;
|
||||||
metadata: any;
|
metadata: any;
|
||||||
currentUserMetadata: any;
|
currentUserMetadata: any;
|
||||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||||
private userPubKey;
|
public currentUserPubKey: string;
|
||||||
private routePubKey;
|
public routePubKey;
|
||||||
followers: any[] = [];
|
followers: any[] = [];
|
||||||
following: any[] = [];
|
following: any[] = [];
|
||||||
allPublicKeys: string[] = [];
|
allPublicKeys: string[] = [];
|
||||||
suggestions: { pubkey: string, metadata: any }[] = [];
|
suggestions: { pubkey: string; metadata: any }[] = [];
|
||||||
isCurrentUserProfile: Boolean = false;
|
isCurrentUserProfile: Boolean = false;
|
||||||
isFollowing = false;
|
isFollowing = false;
|
||||||
|
|
||||||
|
showEmojiPicker = false;
|
||||||
|
showCommentEmojiPicker = false;
|
||||||
lightningResponse: LightningResponse | null = null;
|
lightningResponse: LightningResponse | null = null;
|
||||||
lightningInvoice: LightningInvoice | null = null;
|
lightningInvoice: LightningInvoice | null = null;
|
||||||
sats: string;
|
sats: string;
|
||||||
paymentInvoice: string = '';
|
paymentInvoice: string = '';
|
||||||
invoiceAmount: 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(
|
constructor(
|
||||||
@@ -90,52 +134,81 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
private _socialService: SocialService,
|
private _socialService: SocialService,
|
||||||
private snackBar: MatSnackBar,
|
private snackBar: MatSnackBar,
|
||||||
private lightning: LightningService,
|
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 {
|
ngOnInit(): void {
|
||||||
|
this._angorConfigService.config$.subscribe((config) => {
|
||||||
|
if (config.scheme === 'auto') {
|
||||||
|
this.detectSystemTheme();
|
||||||
|
} else {
|
||||||
|
this.darkMode = config.scheme === 'dark';
|
||||||
|
}
|
||||||
|
});
|
||||||
this._route.paramMap.subscribe((params) => {
|
this._route.paramMap.subscribe((params) => {
|
||||||
const routePubKey = params.get('pubkey');
|
const routePubKey = params.get('pubkey');
|
||||||
this.routePubKey = routePubKey;
|
this.routePubKey = routePubKey;
|
||||||
const userPubKey = this._signerService.getPublicKey();
|
const currentUserPubKey = this._signerService.getPublicKey();
|
||||||
this.isCurrentUserProfile = routePubKey === userPubKey;
|
this.currentUserPubKey = currentUserPubKey;
|
||||||
const pubKeyToLoad = routePubKey || userPubKey;
|
if (routePubKey || currentUserPubKey) {
|
||||||
this.loadProfile(pubKeyToLoad);
|
this.isCurrentUserProfile = routePubKey === currentUserPubKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.routePubKey = routePubKey || currentUserPubKey;
|
||||||
|
this.loadProfile(this.routePubKey);
|
||||||
if (!routePubKey) {
|
if (!routePubKey) {
|
||||||
this.isCurrentUserProfile = true;
|
this.isCurrentUserProfile = true;
|
||||||
}
|
}
|
||||||
this.loadCurrentUserProfile();
|
this.loadCurrentUserProfile();
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this._indexedDBService.getMetadataStream()
|
this._indexedDBService
|
||||||
|
.getMetadataStream()
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe((updatedMetadata) => {
|
.subscribe((updatedMetadata) => {
|
||||||
if (updatedMetadata && updatedMetadata.pubkey === this.userPubKey) {
|
if (
|
||||||
|
updatedMetadata &&
|
||||||
|
updatedMetadata.pubkey === this.currentUserPubKey
|
||||||
|
) {
|
||||||
this.currentUserMetadata = updatedMetadata.metadata;
|
this.currentUserMetadata = updatedMetadata.metadata;
|
||||||
this._changeDetectorRef.detectChanges();
|
this._changeDetectorRef.detectChanges();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (this.routePubKey) {
|
if (this.routePubKey) {
|
||||||
this._indexedDBService.getMetadataStream()
|
this._indexedDBService
|
||||||
|
.getMetadataStream()
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe((updatedMetadata) => {
|
.subscribe((updatedMetadata) => {
|
||||||
if (updatedMetadata && updatedMetadata.pubkey === this.routePubKey) {
|
if (
|
||||||
|
updatedMetadata &&
|
||||||
|
updatedMetadata.pubkey === this.routePubKey
|
||||||
|
) {
|
||||||
this.metadata = updatedMetadata.metadata;
|
this.metadata = updatedMetadata.metadata;
|
||||||
this._changeDetectorRef.detectChanges();
|
this._changeDetectorRef.detectChanges();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this._socialService.getFollowersObservable()
|
this._socialService
|
||||||
|
.getFollowersObservable()
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe((event) => {
|
.subscribe((event) => {
|
||||||
this.followers.push(event.pubkey);
|
this.followers.push(event.pubkey);
|
||||||
this._changeDetectorRef.detectChanges();
|
this._changeDetectorRef.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
this._socialService.getFollowingObservable()
|
this._socialService
|
||||||
|
.getFollowingObservable()
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe((event) => {
|
.subscribe((event) => {
|
||||||
const tags = event.tags.filter((tag) => tag[0] === 'p');
|
const tags = event.tags.filter((tag) => tag[0] === 'p');
|
||||||
@@ -144,8 +217,6 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this._changeDetectorRef.detectChanges();
|
this._changeDetectorRef.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateSuggestionList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@@ -153,6 +224,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
this._unsubscribeAll.complete();
|
this._unsubscribeAll.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async loadProfile(publicKey: string): Promise<void> {
|
async loadProfile(publicKey: string): Promise<void> {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.errorMessage = null;
|
this.errorMessage = null;
|
||||||
@@ -160,6 +232,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
this.metadata = null;
|
this.metadata = null;
|
||||||
this.followers = [];
|
this.followers = [];
|
||||||
this.following = [];
|
this.following = [];
|
||||||
|
|
||||||
this._changeDetectorRef.detectChanges();
|
this._changeDetectorRef.detectChanges();
|
||||||
|
|
||||||
if (!publicKey) {
|
if (!publicKey) {
|
||||||
@@ -170,27 +243,20 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const userMetadata = await this._metadataService.fetchMetadataWithCache(publicKey);
|
const userMetadata = await this._metadataService.fetchMetadataWithCache(publicKey);
|
||||||
if (userMetadata) {
|
if (userMetadata) {
|
||||||
this.metadata = userMetadata;
|
this.metadata = userMetadata;
|
||||||
this._changeDetectorRef.detectChanges();
|
this._changeDetectorRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._socialService.getFollowers(publicKey);
|
|
||||||
|
this.followers = await this._socialService.getFollowers(publicKey);
|
||||||
const currentUserPubKey = this._signerService.getPublicKey();
|
const currentUserPubKey = this._signerService.getPublicKey();
|
||||||
this.isFollowing = this.followers.includes(currentUserPubKey);
|
this.isFollowing = this.followers.includes(currentUserPubKey);
|
||||||
|
|
||||||
await this._socialService.getFollowing(publicKey);
|
this.following = await this._socialService.getFollowing(publicKey);
|
||||||
|
this._changeDetectorRef.detectChanges();
|
||||||
this._metadataService.getMetadataStream()
|
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
|
||||||
.subscribe((updatedMetadata) => {
|
|
||||||
if (updatedMetadata && updatedMetadata.pubkey === publicKey) {
|
|
||||||
this.metadata = updatedMetadata;
|
|
||||||
this._changeDetectorRef.detectChanges();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load profile data:', error);
|
console.error('Failed to load profile data:', error);
|
||||||
this.errorMessage = 'Failed to load profile data. Please try again later.';
|
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> {
|
private async loadCurrentUserProfile(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.currentUserMetadata = null;
|
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) {
|
if (currentUserMetadata) {
|
||||||
this.currentUserMetadata = currentUserMetadata;
|
this.currentUserMetadata = currentUserMetadata;
|
||||||
|
|
||||||
this._changeDetectorRef.detectChanges();
|
|
||||||
}
|
}
|
||||||
this._metadataService.getMetadataStream()
|
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
|
||||||
.subscribe((updatedMetadata) => {
|
this._changeDetectorRef.detectChanges();
|
||||||
if (updatedMetadata && updatedMetadata.pubkey === this.userPubKey) {
|
|
||||||
this.currentUserMetadata = updatedMetadata;
|
|
||||||
this._changeDetectorRef.detectChanges();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load profile data:', error);
|
console.error('Failed to load profile data:', error);
|
||||||
this.errorMessage = 'Failed to load profile data. Please try again later.';
|
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 {
|
getSafeUrl(url: string): SafeUrl {
|
||||||
return this._sanitizer.bypassSecurityTrustUrl(url);
|
return this._sanitizer.bypassSecurityTrustUrl(url);
|
||||||
@@ -246,7 +303,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
async toggleFollow(): Promise<void> {
|
async toggleFollow(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const userPubKey = this._signerService.getPublicKey();
|
const userPubKey = this._signerService.getPublicKey();
|
||||||
const routePubKey = this.routePubKey || this.userPubKey;
|
const routePubKey = this.routePubKey || this.currentUserPubKey;
|
||||||
|
|
||||||
if (!routePubKey || !userPubKey) {
|
if (!routePubKey || !userPubKey) {
|
||||||
console.error('Public key missing. Unable to toggle follow.');
|
console.error('Public key missing. Unable to toggle follow.');
|
||||||
@@ -257,7 +314,9 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
await this._socialService.unfollow(routePubKey);
|
await this._socialService.unfollow(routePubKey);
|
||||||
console.log(`Unfollowed ${routePubKey}`);
|
console.log(`Unfollowed ${routePubKey}`);
|
||||||
|
|
||||||
this.followers = this.followers.filter(pubkey => pubkey !== userPubKey);
|
this.followers = this.followers.filter(
|
||||||
|
(pubkey) => pubkey !== userPubKey
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await this._socialService.follow(routePubKey);
|
await this._socialService.follow(routePubKey);
|
||||||
console.log(`Followed ${routePubKey}`);
|
console.log(`Followed ${routePubKey}`);
|
||||||
@@ -268,19 +327,15 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
this.isFollowing = !this.isFollowing;
|
this.isFollowing = !this.isFollowing;
|
||||||
|
|
||||||
this._changeDetectorRef.detectChanges();
|
this._changeDetectorRef.detectChanges();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to toggle follow:', error);
|
console.error('Failed to toggle follow:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
openSnackBar(message: string, action: string) {
|
openSnackBar(message: string, action: string) {
|
||||||
this.snackBar.open(message, action, { duration: 1300 });
|
this.snackBar.open(message, action, { duration: 1300 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getLightningInfo() {
|
getLightningInfo() {
|
||||||
let lightningAddress = '';
|
let lightningAddress = '';
|
||||||
if (this.metadata?.lud06) {
|
if (this.metadata?.lud06) {
|
||||||
@@ -292,19 +347,29 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
const data = new Uint8Array(bech32.fromWords(words));
|
const data = new Uint8Array(bech32.fromWords(words));
|
||||||
lightningAddress = new TextDecoder().decode(Uint8Array.from(data));
|
lightningAddress = new TextDecoder().decode(Uint8Array.from(data));
|
||||||
} else if (this.metadata?.lud16) {
|
} else if (this.metadata?.lud16) {
|
||||||
lightningAddress = this.lightning.getLightningAddress(this.metadata.lud16);
|
lightningAddress = this.lightning.getLightningAddress(
|
||||||
|
this.metadata.lud16
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (lightningAddress !== '') {
|
if (lightningAddress !== '') {
|
||||||
this.lightning.getLightning(lightningAddress).subscribe((response) => {
|
this.lightning
|
||||||
this.lightningResponse = response;
|
.getLightning(lightningAddress)
|
||||||
if (this.lightningResponse.status === 'Failed') {
|
.subscribe((response) => {
|
||||||
this.openSnackBar('Failed to lookup lightning address', 'dismiss');
|
this.lightningResponse = response;
|
||||||
} else if (this.lightningResponse.callback) {
|
if (this.lightningResponse.status === 'Failed') {
|
||||||
this.openZapDialog(); // Open dialog when callback is available
|
this.openSnackBar(
|
||||||
} else {
|
'Failed to lookup lightning address',
|
||||||
this.openSnackBar("couldn't find user's lightning address", 'dismiss');
|
'dismiss'
|
||||||
}
|
);
|
||||||
});
|
} else if (this.lightningResponse.callback) {
|
||||||
|
this.openZapDialog();
|
||||||
|
} else {
|
||||||
|
this.openSnackBar(
|
||||||
|
"couldn't find user's lightning address",
|
||||||
|
'dismiss'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.openSnackBar('No lightning address found', 'dismiss');
|
this.openSnackBar('No lightning address found', 'dismiss');
|
||||||
}
|
}
|
||||||
@@ -322,7 +387,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
this._dialog.open(SendDialogComponent, {
|
this._dialog.open(SendDialogComponent, {
|
||||||
width: '405px',
|
width: '405px',
|
||||||
maxHeight: '90vh',
|
maxHeight: '90vh',
|
||||||
data: this.metadata
|
data: this.metadata,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,8 +395,95 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
this._dialog.open(ReceiveDialogComponent, {
|
this._dialog.open(ReceiveDialogComponent, {
|
||||||
width: '405px',
|
width: '405px',
|
||||||
maxHeight: '90vh',
|
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>
|
<h2>⚡ Receive Zap</h2>
|
||||||
<mat-dialog-content *ngIf="!displayQRCode">
|
<mat-dialog-content *ngIf="!displayQRCode">
|
||||||
<div class="preset-buttons">
|
<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>
|
<mat-icon>{{ button.icon }}</mat-icon>
|
||||||
<span>{{ button.label }}</span>
|
<span>{{ button.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -9,12 +22,14 @@
|
|||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<mat-form-field appearance="outline" class="sats-input">
|
<mat-form-field appearance="outline" class="sats-input">
|
||||||
<mat-label>Zap Amount</mat-label>
|
<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-form-field>
|
||||||
<mat-dialog-actions align="end">
|
<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()">
|
<button mat-raised-button color="primary" (click)="generateInvoice()">
|
||||||
Generate Invoice
|
Generate Invoice
|
||||||
</button>
|
</button>
|
||||||
@@ -25,14 +40,22 @@
|
|||||||
<div *ngIf="displayQRCode" class="qrcode">
|
<div *ngIf="displayQRCode" class="qrcode">
|
||||||
<span>Scan with phone to pay ({{ invoiceAmount }} sats)</span>
|
<span>Scan with phone to pay ({{ invoiceAmount }} sats)</span>
|
||||||
<mat-divider></mat-divider>
|
<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">
|
<mat-dialog-actions align="center">
|
||||||
<button mat-icon-button color="warn" (click)="closeDialog()" [matTooltip]="'Close'">
|
<button
|
||||||
<mat-icon [svgIcon]="'heroicons_solid:x-mark'"></mat-icon>
|
mat-icon-button
|
||||||
</button>
|
(click)="copyInvoice()"
|
||||||
<button mat-icon-button (click)="copyInvoice()" [matTooltip]="'Copy Invoice'">
|
[matTooltip]="'Copy Invoice'"
|
||||||
<mat-icon [svgIcon]="'heroicons_outline:clipboard-document'"></mat-icon>
|
>
|
||||||
|
<mat-icon
|
||||||
|
[svgIcon]="'heroicons_outline:clipboard-document'"
|
||||||
|
></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
gap: 15px;
|
gap: 15px;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-buttons button {
|
.preset-buttons button {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
width: 70px;
|
width: 70px;
|
||||||
@@ -16,24 +16,23 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-height: 60px !important;
|
max-height: 60px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sats-input {
|
.sats-input {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightning-buttons {
|
.lightning-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qrcode {
|
||||||
.qrcode {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qrcode-image {
|
.qrcode-image {
|
||||||
width: 100% !important;
|
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 { Clipboard } from '@angular/cdk/clipboard';
|
||||||
import { MatDialogActions, MatDialogContent, MatDialogRef } from '@angular/material/dialog';
|
import { CommonModule, NgClass } from '@angular/common';
|
||||||
import { webln } from '@getalby/sdk';
|
import { Component } from '@angular/core';
|
||||||
import { NgClass, CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatOption } from '@angular/material/core';
|
import { MatOption } from '@angular/material/core';
|
||||||
|
import {
|
||||||
|
MatDialogActions,
|
||||||
|
MatDialogClose,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogRef,
|
||||||
|
} from '@angular/material/dialog';
|
||||||
import { MatDivider } from '@angular/material/divider';
|
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 { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { MatTooltip } from '@angular/material/tooltip';
|
import { MatTooltip } from '@angular/material/tooltip';
|
||||||
|
import { webln } from '@getalby/sdk';
|
||||||
import { QRCodeModule } from 'angularx-qrcode';
|
import { QRCodeModule } from 'angularx-qrcode';
|
||||||
import { SettingsIndexerComponent } from 'app/components/settings/indexer/indexer.component';
|
import { SettingsIndexerComponent } from 'app/components/settings/indexer/indexer.component';
|
||||||
import { SettingsNetworkComponent } from 'app/components/settings/network/network.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';
|
import { SettingsSecurityComponent } from 'app/components/settings/security/security.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-receive-dialog',
|
selector: 'app-receive-dialog',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
MatSidenavModule,
|
MatSidenavModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
NgClass,
|
NgClass,
|
||||||
SettingsProfileComponent,
|
SettingsProfileComponent,
|
||||||
SettingsSecurityComponent,
|
SettingsSecurityComponent,
|
||||||
SettingsNotificationsComponent,
|
SettingsNotificationsComponent,
|
||||||
SettingsRelayComponent,
|
SettingsRelayComponent,
|
||||||
SettingsNetworkComponent,
|
SettingsNetworkComponent,
|
||||||
SettingsIndexerComponent,
|
SettingsIndexerComponent,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
MatOption,
|
MatOption,
|
||||||
MatLabel,
|
MatLabel,
|
||||||
MatFormField,
|
MatFormField,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatDialogContent,
|
MatDialogContent,
|
||||||
MatDialogActions,
|
MatDialogActions,
|
||||||
QRCodeModule,
|
QRCodeModule,
|
||||||
MatDivider,
|
MatDivider,
|
||||||
MatTooltip
|
MatTooltip,
|
||||||
],
|
MatDialogClose,
|
||||||
templateUrl: './receive-dialog.component.html',
|
],
|
||||||
styleUrls: ['./receive-dialog.component.scss']
|
templateUrl: './receive-dialog.component.html',
|
||||||
|
styleUrls: ['./receive-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class ReceiveDialogComponent {
|
export class ReceiveDialogComponent {
|
||||||
invoiceAmount: string = '';
|
invoiceAmount: string = '';
|
||||||
lightningInvoice: string = '';
|
lightningInvoice: string = '';
|
||||||
displayQRCode: boolean = false;
|
displayQRCode: boolean = false;
|
||||||
nwc: any;
|
nwc: any;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private dialogRef: MatDialogRef<ReceiveDialogComponent>,
|
private dialogRef: MatDialogRef<ReceiveDialogComponent>,
|
||||||
private snackBar: MatSnackBar,
|
private snackBar: MatSnackBar,
|
||||||
private clipboard: Clipboard
|
private clipboard: Clipboard
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
zapButtons = [
|
zapButtons = [
|
||||||
{ icon: 'thumb_up', label: '50', value: 50 },
|
{ icon: 'thumb_up', label: '50', value: 50 },
|
||||||
{ icon: 'favorite', label: '100', value: 100 },
|
{ icon: 'favorite', label: '100', value: 100 },
|
||||||
{ icon: 'emoji_emotions', label: '500', value: 500 },
|
{ icon: 'emoji_emotions', label: '500', value: 500 },
|
||||||
{ icon: 'star', label: '1k', value: 1000 },
|
{ icon: 'star', label: '1k', value: 1000 },
|
||||||
{ icon: 'celebration', label: '5k', value: 5000 },
|
{ icon: 'celebration', label: '5k', value: 5000 },
|
||||||
{ icon: 'rocket', label: '10k', value: 10000 },
|
{ icon: 'rocket', label: '10k', value: 10000 },
|
||||||
{ icon: 'local_fire_department', label: '100k', value: 100000 },
|
{ icon: 'local_fire_department', label: '100k', value: 100000 },
|
||||||
{ icon: 'flash_on', label: '500k', value: 500000 },
|
{ icon: 'flash_on', label: '500k', value: 500000 },
|
||||||
{ icon: 'diamond', label: '1M', value: 1000000 }
|
{ icon: 'diamond', label: '1M', value: 1000000 },
|
||||||
];
|
];
|
||||||
|
|
||||||
async generateInvoice(): Promise<void> {
|
async generateInvoice(): Promise<void> {
|
||||||
if (!this.invoiceAmount || Number(this.invoiceAmount) <= 0) {
|
if (!this.invoiceAmount || Number(this.invoiceAmount) <= 0) {
|
||||||
this.openSnackBar('Please enter a valid amount', 'dismiss');
|
this.openSnackBar('Please enter a valid amount', 'dismiss');
|
||||||
return;
|
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 {
|
async loadNWCUrl(): Promise<string> {
|
||||||
|
try {
|
||||||
this.nwc = new webln.NostrWebLNProvider({ nostrWalletConnectUrl: await this.loadNWCUrl() });
|
const nwc = webln.NostrWebLNProvider.withNewSecret();
|
||||||
await this.nwc.enable();
|
await nwc.initNWC({ name: 'Angor Hub' });
|
||||||
|
return nwc.getNostrWalletConnectUrl();
|
||||||
const invoiceResponse = await this.nwc.makeInvoice({ amount: Number(this.invoiceAmount) });
|
} catch (error) {
|
||||||
this.lightningInvoice = invoiceResponse.paymentRequest;
|
console.error('Error initializing NWC:', error);
|
||||||
|
throw new Error('Failed to initialize NWC provider');
|
||||||
this.showQRCode();
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating invoice:', error);
|
|
||||||
this.openSnackBar('Failed to generate invoice', 'dismiss');
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async loadNWCUrl(): Promise<string> {
|
showQRCode(): void {
|
||||||
try {
|
this.displayQRCode = !this.displayQRCode;
|
||||||
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 {
|
copyInvoice(): void {
|
||||||
this.displayQRCode = !this.displayQRCode;
|
if (this.lightningInvoice) {
|
||||||
}
|
this.clipboard.copy(this.lightningInvoice);
|
||||||
|
this.openSnackBar('Invoice copied', 'dismiss');
|
||||||
copyInvoice(): void {
|
} else {
|
||||||
if (this.lightningInvoice) {
|
this.openSnackBar('No invoice available to copy', 'dismiss');
|
||||||
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 {
|
openSnackBar(message: string, action: string): void {
|
||||||
this.snackBar.open(message, action, { duration: 1300 });
|
this.snackBar.open(message, action, { duration: 1300 });
|
||||||
}
|
}
|
||||||
|
|
||||||
closeDialog(): void {
|
closeDialog(): void {
|
||||||
this.dialogRef.close();
|
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>
|
<h1>⚡ Send Zap</h1>
|
||||||
<mat-dialog-content *ngIf="!showInvoiceSection || !lightningInvoice">
|
<mat-dialog-content *ngIf="!showInvoiceSection || !lightningInvoice">
|
||||||
<div class="preset-buttons">
|
<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>
|
<mat-icon>{{ button.icon }}</mat-icon>
|
||||||
<span>{{ button.label }}</span>
|
<span>{{ button.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -9,13 +22,15 @@
|
|||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<mat-form-field appearance="outline" class="sats-input">
|
<mat-form-field appearance="outline" class="sats-input">
|
||||||
<mat-label>Zap Amount</mat-label>
|
<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-form-field>
|
||||||
|
|
||||||
<mat-dialog-actions align="end">
|
<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()">
|
<button mat-raised-button color="primary" (click)="sendZap()">
|
||||||
Create invoice
|
Create invoice
|
||||||
</button>
|
</button>
|
||||||
@@ -26,20 +41,30 @@
|
|||||||
<div *ngIf="displayQRCode" class="qrcode">
|
<div *ngIf="displayQRCode" class="qrcode">
|
||||||
<span>Scan with phone to pay ({{ invoiceAmount }} sats)</span>
|
<span>Scan with phone to pay ({{ invoiceAmount }} sats)</span>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<qrcode [qrdata]="lightningInvoice" [matTooltip]="'Lightning Invoice'" [errorCorrectionLevel]="'M'"
|
<qrcode
|
||||||
class="qrcode-image"></qrcode>
|
[qrdata]="lightningInvoice"
|
||||||
|
[matTooltip]="'Lightning Invoice'"
|
||||||
|
[errorCorrectionLevel]="'M'"
|
||||||
|
class="qrcode-image"
|
||||||
|
></qrcode>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<mat-dialog-actions align="center">
|
<mat-dialog-actions align="center">
|
||||||
<button mat-icon-button color="warn" (click)="closeDialog()" [matTooltip]="'Close'">
|
<button
|
||||||
<mat-icon [svgIcon]="'heroicons_solid:x-mark'"></mat-icon>
|
mat-icon-button
|
||||||
|
(click)="copyInvoice()"
|
||||||
|
[matTooltip]="'Copy Invoice'"
|
||||||
|
>
|
||||||
|
<mat-icon
|
||||||
|
[svgIcon]="'heroicons_outline:clipboard-document'"
|
||||||
|
></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button mat-icon-button (click)="copyInvoice()" [matTooltip]="'Copy Invoice'">
|
<button
|
||||||
<mat-icon [svgIcon]="'heroicons_outline:clipboard-document'"></mat-icon>
|
mat-icon-button
|
||||||
</button>
|
(click)="payInvoice()"
|
||||||
|
[matTooltip]="'Pay Invoice'"
|
||||||
<button mat-icon-button (click)="payInvoice()" [matTooltip]="'Pay Invoice'">
|
>
|
||||||
<mat-icon color="#f79318" [svgIcon]="'feather:zap'"></mat-icon>
|
<mat-icon color="#f79318" [svgIcon]="'feather:zap'"></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
gap: 15px;
|
gap: 15px;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-buttons button {
|
.preset-buttons button {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
width: 70px;
|
width: 70px;
|
||||||
@@ -16,24 +16,23 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-height: 60px !important;
|
max-height: 60px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sats-input {
|
.sats-input {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightning-buttons {
|
.lightning-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qrcode {
|
||||||
.qrcode {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qrcode-image {
|
.qrcode-image {
|
||||||
width: 100% !important;
|
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 { Clipboard } from '@angular/cdk/clipboard';
|
||||||
import { webln } from '@getalby/sdk';
|
import { CommonModule, NgClass } from '@angular/common';
|
||||||
import { decode } from '@gandlaf21/bolt11-decode';
|
import { Component, Inject } from '@angular/core';
|
||||||
import { bech32 } from '@scure/base';
|
|
||||||
import { NgClass, CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatOption } from '@angular/material/core';
|
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 { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
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 { SettingsIndexerComponent } from 'app/components/settings/indexer/indexer.component';
|
||||||
import { SettingsNetworkComponent } from 'app/components/settings/network/network.component';
|
import { SettingsNetworkComponent } from 'app/components/settings/network/network.component';
|
||||||
import { SettingsNotificationsComponent } from 'app/components/settings/notifications/notifications.component';
|
import { SettingsNotificationsComponent } from 'app/components/settings/notifications/notifications.component';
|
||||||
import { SettingsProfileComponent } from 'app/components/settings/profile/profile.component';
|
import { SettingsProfileComponent } from 'app/components/settings/profile/profile.component';
|
||||||
import { SettingsRelayComponent } from 'app/components/settings/relay/relay.component';
|
import { SettingsRelayComponent } from 'app/components/settings/relay/relay.component';
|
||||||
import { SettingsSecurityComponent } from 'app/components/settings/security/security.component';
|
import { SettingsSecurityComponent } from 'app/components/settings/security/security.component';
|
||||||
import { QRCodeModule } from 'angularx-qrcode';
|
import { LightningService } from 'app/services/lightning.service';
|
||||||
import { MatDivider } from '@angular/material/divider';
|
|
||||||
import { MatTooltip } from '@angular/material/tooltip';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-send-dialog',
|
selector: 'app-send-dialog',
|
||||||
@@ -54,10 +65,11 @@ import { MatTooltip } from '@angular/material/tooltip';
|
|||||||
QRCodeModule,
|
QRCodeModule,
|
||||||
MatDivider,
|
MatDivider,
|
||||||
MatTooltip,
|
MatTooltip,
|
||||||
MatDialogTitle
|
MatDialogTitle,
|
||||||
|
MatDialogClose,
|
||||||
],
|
],
|
||||||
templateUrl: './send-dialog.component.html',
|
templateUrl: './send-dialog.component.html',
|
||||||
styleUrls: ['./send-dialog.component.scss']
|
styleUrls: ['./send-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class SendDialogComponent {
|
export class SendDialogComponent {
|
||||||
sats: string;
|
sats: string;
|
||||||
@@ -66,7 +78,7 @@ export class SendDialogComponent {
|
|||||||
showInvoiceSection: boolean = false;
|
showInvoiceSection: boolean = false;
|
||||||
displayQRCode: boolean = false;
|
displayQRCode: boolean = false;
|
||||||
invoiceAmount: string = '?';
|
invoiceAmount: string = '?';
|
||||||
nwc :any;
|
nwc: any;
|
||||||
constructor(
|
constructor(
|
||||||
private dialogRef: MatDialogRef<SendDialogComponent>,
|
private dialogRef: MatDialogRef<SendDialogComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public metadata: any,
|
@Inject(MAT_DIALOG_DATA) public metadata: any,
|
||||||
@@ -86,7 +98,7 @@ export class SendDialogComponent {
|
|||||||
{ icon: 'rocket', label: '10k', value: 10000 },
|
{ icon: 'rocket', label: '10k', value: 10000 },
|
||||||
{ icon: 'local_fire_department', label: '100k', value: 100000 },
|
{ icon: 'local_fire_department', label: '100k', value: 100000 },
|
||||||
{ icon: 'flash_on', label: '500k', value: 500000 },
|
{ icon: 'flash_on', label: '500k', value: 500000 },
|
||||||
{ icon: 'diamond', label: '1M', value: 1000000 }
|
{ icon: 'diamond', label: '1M', value: 1000000 },
|
||||||
];
|
];
|
||||||
|
|
||||||
getLightningInfo(): void {
|
getLightningInfo(): void {
|
||||||
@@ -100,20 +112,30 @@ export class SendDialogComponent {
|
|||||||
const data = new Uint8Array(bech32.fromWords(words));
|
const data = new Uint8Array(bech32.fromWords(words));
|
||||||
lightningAddress = new TextDecoder().decode(Uint8Array.from(data));
|
lightningAddress = new TextDecoder().decode(Uint8Array.from(data));
|
||||||
} else if (this.metadata?.lud16) {
|
} else if (this.metadata?.lud16) {
|
||||||
lightningAddress = this.lightning.getLightningAddress(this.metadata.lud16);
|
lightningAddress = this.lightning.getLightningAddress(
|
||||||
|
this.metadata.lud16
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lightningAddress !== '') {
|
if (lightningAddress !== '') {
|
||||||
this.lightning.getLightning(lightningAddress).subscribe((response) => {
|
this.lightning
|
||||||
this.lightningResponse = response;
|
.getLightning(lightningAddress)
|
||||||
if (this.lightningResponse.status === 'Failed') {
|
.subscribe((response) => {
|
||||||
this.openSnackBar('Failed to lookup lightning address', 'dismiss');
|
this.lightningResponse = response;
|
||||||
} else if (this.lightningResponse.callback) {
|
if (this.lightningResponse.status === 'Failed') {
|
||||||
this.showInvoiceSection = true;
|
this.openSnackBar(
|
||||||
} else {
|
'Failed to lookup lightning address',
|
||||||
this.openSnackBar("Couldn't find user's lightning address", 'dismiss');
|
'dismiss'
|
||||||
}
|
);
|
||||||
});
|
} else if (this.lightningResponse.callback) {
|
||||||
|
this.showInvoiceSection = true;
|
||||||
|
} else {
|
||||||
|
this.openSnackBar(
|
||||||
|
"Couldn't find user's lightning address",
|
||||||
|
'dismiss'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.openSnackBar('No lightning address found', 'dismiss');
|
this.openSnackBar('No lightning address found', 'dismiss');
|
||||||
}
|
}
|
||||||
@@ -121,8 +143,9 @@ export class SendDialogComponent {
|
|||||||
|
|
||||||
getLightningInvoice(amount: string): void {
|
getLightningInvoice(amount: string): void {
|
||||||
if (this.lightningResponse && this.lightningResponse.callback) {
|
if (this.lightningResponse && this.lightningResponse.callback) {
|
||||||
this.lightning.getLightningInvoice(this.lightningResponse.callback, amount)
|
this.lightning
|
||||||
.subscribe(async response => {
|
.getLightningInvoice(this.lightningResponse.callback, amount)
|
||||||
|
.subscribe(async (response) => {
|
||||||
this.lightningInvoice = response.pr;
|
this.lightningInvoice = response.pr;
|
||||||
this.setInvoiceAmount(this.lightningInvoice);
|
this.setInvoiceAmount(this.lightningInvoice);
|
||||||
this.showInvoiceSection = true;
|
this.showInvoiceSection = true;
|
||||||
@@ -134,7 +157,9 @@ export class SendDialogComponent {
|
|||||||
setInvoiceAmount(invoice: string): void {
|
setInvoiceAmount(invoice: string): void {
|
||||||
if (invoice) {
|
if (invoice) {
|
||||||
const decodedInvoice = decode(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) {
|
if (amountSection) {
|
||||||
this.invoiceAmount = String(Number(amountSection.value) / 1000);
|
this.invoiceAmount = String(Number(amountSection.value) / 1000);
|
||||||
}
|
}
|
||||||
@@ -149,29 +174,32 @@ export class SendDialogComponent {
|
|||||||
this.getLightningInvoice(String(Number(this.sats) * 1000));
|
this.getLightningInvoice(String(Number(this.sats) * 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async payInvoice(): Promise<void> {
|
async payInvoice(): Promise<void> {
|
||||||
if (!this.lightningInvoice) {
|
if (!this.lightningInvoice) {
|
||||||
console.error('Lightning invoice is not set');
|
console.error('Lightning invoice is not set');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nwc = new webln.NostrWebLNProvider({ nostrWalletConnectUrl: await this.loadNWCUrl() });
|
const nwc = new webln.NostrWebLNProvider({
|
||||||
|
nostrWalletConnectUrl: await this.loadNWCUrl(),
|
||||||
|
});
|
||||||
|
|
||||||
nwc.enable()
|
nwc.enable()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return nwc.sendPayment(this.lightningInvoice);
|
return nwc.sendPayment(this.lightningInvoice);
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response && response.preimage) {
|
if (response && response.preimage) {
|
||||||
console.log(`Payment successful, preimage: ${response.preimage}`);
|
console.log(
|
||||||
|
`Payment successful, preimage: ${response.preimage}`
|
||||||
|
);
|
||||||
this.openSnackBar('Zapped!', 'dismiss');
|
this.openSnackBar('Zapped!', 'dismiss');
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
} else {
|
} else {
|
||||||
this.listenForPaymentStatus(nwc);
|
this.listenForPaymentStatus(nwc);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('Payment failed:', error);
|
console.error('Payment failed:', error);
|
||||||
this.openSnackBar('Failed to pay invoice', 'dismiss');
|
this.openSnackBar('Failed to pay invoice', 'dismiss');
|
||||||
this.listenForPaymentStatus(nwc);
|
this.listenForPaymentStatus(nwc);
|
||||||
@@ -179,13 +207,14 @@ export class SendDialogComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadNWCUrl(): Promise<string> {
|
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(() => {
|
.then(() => {
|
||||||
return nwc.getNostrWalletConnectUrl();
|
return nwc.getNostrWalletConnectUrl();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('Error initializing NWC:', error);
|
console.error('Error initializing NWC:', error);
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
@@ -194,16 +223,19 @@ export class SendDialogComponent {
|
|||||||
listenForPaymentStatus(nwc): void {
|
listenForPaymentStatus(nwc): void {
|
||||||
const checkPaymentStatus = () => {
|
const checkPaymentStatus = () => {
|
||||||
nwc.sendPayment(this.lightningInvoice)
|
nwc.sendPayment(this.lightningInvoice)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response && response.preimage) {
|
if (response && response.preimage) {
|
||||||
console.log('Payment confirmed, preimage:', response.preimage);
|
console.log(
|
||||||
|
'Payment confirmed, preimage:',
|
||||||
|
response.preimage
|
||||||
|
);
|
||||||
this.openSnackBar('Payment confirmed!', 'dismiss');
|
this.openSnackBar('Payment confirmed!', 'dismiss');
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
} else {
|
} else {
|
||||||
setTimeout(checkPaymentStatus, 5000);
|
setTimeout(checkPaymentStatus, 5000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('Error checking payment status:', error);
|
console.error('Error checking payment status:', error);
|
||||||
setTimeout(checkPaymentStatus, 5000);
|
setTimeout(checkPaymentStatus, 5000);
|
||||||
});
|
});
|
||||||
@@ -228,5 +260,4 @@ export class SendDialogComponent {
|
|||||||
closeDialog(): void {
|
closeDialog(): void {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
<div class="w-full max-w-3xl">
|
<div class="w-full max-w-3xl">
|
||||||
<!-- Add Mainnet Indexer -->
|
<!-- Add Mainnet Indexer -->
|
||||||
<div class="w-full mb-8">
|
<div class="mb-8 w-full">
|
||||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||||
<mat-label>Add Mainnet Indexer</mat-label>
|
<mat-label>Add Mainnet Indexer</mat-label>
|
||||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:link'" matPrefix></mat-icon>
|
<mat-icon
|
||||||
<input matInput [(ngModel)]="newMainnetIndexerUrl" placeholder="Mainnet Indexer URL" />
|
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')">
|
<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>
|
</button>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@@ -15,22 +26,43 @@
|
|||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h3>Mainnet Indexers</h3>
|
<h3>Mainnet Indexers</h3>
|
||||||
<div class="flex flex-col divide-y border-b border-t">
|
<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="flex items-center">
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<div class="font-medium">{{ indexer.url }}</div>
|
<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>
|
</div>
|
||||||
<div class="mt-4 flex items-center sm:ml-auto sm:mt-0">
|
<div class="mt-4 flex items-center sm:ml-auto sm:mt-0">
|
||||||
<button mat-icon-button (click)="setPrimaryIndexer('mainnet', indexer)">
|
<button
|
||||||
<mat-icon *ngIf="indexer.primary; else nonPrimaryIcon" class="text-primary" [svgIcon]="'heroicons_solid:check-circle'"></mat-icon>
|
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>
|
<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>
|
</ng-template>
|
||||||
</button>
|
</button>
|
||||||
<button mat-icon-button (click)="removeIndexer('mainnet', indexer)">
|
<button
|
||||||
<mat-icon class="text-hint" [svgIcon]="'heroicons_outline:trash'"></mat-icon>
|
mat-icon-button
|
||||||
|
(click)="removeIndexer('mainnet', indexer)"
|
||||||
|
>
|
||||||
|
<mat-icon
|
||||||
|
class="text-hint"
|
||||||
|
[svgIcon]="'heroicons_outline:trash'"
|
||||||
|
></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,13 +70,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Testnet Indexer -->
|
<!-- 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-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||||
<mat-label>Add Testnet Indexer</mat-label>
|
<mat-label>Add Testnet Indexer</mat-label>
|
||||||
<mat-icon class="icon-size-5" [svgIcon]="'heroicons_solid:link'" matPrefix></mat-icon>
|
<mat-icon
|
||||||
<input matInput [(ngModel)]="newTestnetIndexerUrl" placeholder="Testnet Indexer URL" />
|
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')">
|
<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>
|
</button>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,22 +96,43 @@
|
|||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h3>Testnet Indexers</h3>
|
<h3>Testnet Indexers</h3>
|
||||||
<div class="flex flex-col divide-y border-b border-t">
|
<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="flex items-center">
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<div class="font-medium">{{ indexer.url }}</div>
|
<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>
|
</div>
|
||||||
<div class="mt-4 flex items-center sm:ml-auto sm:mt-0">
|
<div class="mt-4 flex items-center sm:ml-auto sm:mt-0">
|
||||||
<button mat-icon-button (click)="setPrimaryIndexer('testnet', indexer)">
|
<button
|
||||||
<mat-icon *ngIf="indexer.primary; else nonPrimaryIcon" class="text-primary" [svgIcon]="'heroicons_solid:check-circle'"></mat-icon>
|
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>
|
<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>
|
</ng-template>
|
||||||
</button>
|
</button>
|
||||||
<button mat-icon-button (click)="removeIndexer('testnet', indexer)">
|
<button
|
||||||
<mat-icon class="text-hint" [svgIcon]="'heroicons_outline:trash'"></mat-icon>
|
mat-icon-button
|
||||||
|
(click)="removeIndexer('testnet', indexer)"
|
||||||
|
>
|
||||||
|
<mat-icon
|
||||||
|
class="text-hint"
|
||||||
|
[svgIcon]="'heroicons_outline:trash'"
|
||||||
|
></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AngorAlertComponent } from '@angor/components/alert';
|
||||||
import { CommonModule, CurrencyPipe, NgClass } from '@angular/common';
|
import { CommonModule, CurrencyPipe, NgClass } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -5,12 +6,7 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
UntypedFormBuilder,
|
|
||||||
UntypedFormGroup,
|
|
||||||
} from '@angular/forms';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatOptionModule } from '@angular/material/core';
|
import { MatOptionModule } from '@angular/material/core';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
@@ -18,7 +14,6 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatRadioModule } from '@angular/material/radio';
|
import { MatRadioModule } from '@angular/material/radio';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { AngorAlertComponent } from '@angor/components/alert';
|
|
||||||
import { IndexerService } from 'app/services/indexer.service';
|
import { IndexerService } from 'app/services/indexer.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -40,59 +35,76 @@ import { IndexerService } from 'app/services/indexer.service';
|
|||||||
MatOptionModule,
|
MatOptionModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
CurrencyPipe,
|
CurrencyPipe,
|
||||||
CommonModule
|
CommonModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsIndexerComponent implements OnInit {
|
export class SettingsIndexerComponent implements OnInit {
|
||||||
mainnetIndexers: Array<{ url: string, primary: boolean }> = [];
|
mainnetIndexers: Array<{ url: string; primary: boolean }> = [];
|
||||||
testnetIndexers: Array<{ url: string, primary: boolean }> = [];
|
testnetIndexers: Array<{ url: string; primary: boolean }> = [];
|
||||||
newMainnetIndexerUrl: string = '';
|
newMainnetIndexerUrl: string = '';
|
||||||
newTestnetIndexerUrl: string = '';
|
newTestnetIndexerUrl: string = '';
|
||||||
|
|
||||||
constructor(private indexerService: IndexerService) {}
|
constructor(private indexerService: IndexerService) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadIndexers();
|
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 = '';
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
removeIndexer(network: 'mainnet' | 'testnet', indexer: { url: string, primary: boolean }): void {
|
loadIndexers(): void {
|
||||||
this.indexerService.removeIndexer(indexer.url, network);
|
this.mainnetIndexers = this.indexerService
|
||||||
this.loadIndexers();
|
.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 {
|
console.log('Mainnet Indexers:', this.mainnetIndexers);
|
||||||
this.indexerService.setPrimaryIndexer(indexer.url, network);
|
console.log('Testnet Indexers:', this.testnetIndexers);
|
||||||
this.loadIndexers();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
trackByFn(index: number, item: any): any {
|
addIndexer(network: 'mainnet' | 'testnet'): void {
|
||||||
return item.url;
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- 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 -->
|
<!-- 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-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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { AngorAlertComponent } from '@angor/components/alert';
|
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 { 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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatOptionModule } from '@angular/material/core';
|
import { MatOptionModule } from '@angular/material/core';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
@@ -12,49 +17,52 @@ import { MatSelectModule } from '@angular/material/select';
|
|||||||
import { IndexerService } from 'app/services/indexer.service';
|
import { IndexerService } from 'app/services/indexer.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'settings-network',
|
selector: 'settings-network',
|
||||||
templateUrl: './network.component.html',
|
templateUrl: './network.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
AngorAlertComponent,
|
AngorAlertComponent,
|
||||||
MatRadioModule,
|
MatRadioModule,
|
||||||
NgClass,
|
NgClass,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatOptionModule,
|
MatOptionModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
CurrencyPipe,
|
CurrencyPipe,
|
||||||
CommonModule
|
CommonModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsNetworkComponent implements OnInit {
|
export class SettingsNetworkComponent implements OnInit {
|
||||||
networkForm: FormGroup;
|
networkForm: FormGroup;
|
||||||
selectedNetwork: 'mainnet' | 'testnet' = 'testnet';
|
selectedNetwork: 'mainnet' | 'testnet' = 'testnet';
|
||||||
constructor(private fb: FormBuilder, private indexerService: IndexerService) {}
|
constructor(
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private indexerService: IndexerService
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.networkForm = this.fb.group({
|
this.networkForm = this.fb.group({
|
||||||
network: [this.indexerService.getNetwork()]
|
network: [this.indexerService.getNetwork()],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.selectedNetwork = this.indexerService.getNetwork();
|
this.selectedNetwork = this.indexerService.getNetwork();
|
||||||
}
|
}
|
||||||
|
|
||||||
setNetwork(network: 'mainnet' | 'testnet'): void {
|
setNetwork(network: 'mainnet' | 'testnet'): void {
|
||||||
this.selectedNetwork = network;
|
this.selectedNetwork = network;
|
||||||
this.indexerService.setNetwork(this.selectedNetwork);
|
this.indexerService.setNetwork(this.selectedNetwork);
|
||||||
}
|
}
|
||||||
|
|
||||||
save(): void {
|
save(): void {
|
||||||
this.indexerService.setNetwork(this.selectedNetwork);
|
this.indexerService.setNetwork(this.selectedNetwork);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(): void {
|
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">
|
<div class="mt-8 grid w-full grid-cols-1 gap-6">
|
||||||
<!-- Mention -->
|
<!-- Mention -->
|
||||||
<div class="flex items-center justify-between">
|
<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="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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Private Message -->
|
<!-- Private Message -->
|
||||||
<div class="flex items-center justify-between">
|
<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="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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Zap -->
|
<!-- Zap -->
|
||||||
<div class="flex items-center justify-between">
|
<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="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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- New Follower -->
|
<!-- New Follower -->
|
||||||
<div class="flex items-center justify-between">
|
<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="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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -45,7 +85,15 @@
|
|||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex items-center justify-end">
|
||||||
<button mat-stroked-button type="button">Cancel</button>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ export class SettingsNotificationsComponent implements OnInit {
|
|||||||
|
|
||||||
this.notificationsForm = this._formBuilder.group({
|
this.notificationsForm = this._formBuilder.group({
|
||||||
mention: [savedSettings.includes(this.notificationKinds.mention)],
|
mention: [savedSettings.includes(this.notificationKinds.mention)],
|
||||||
privateMessage: [savedSettings.includes(this.notificationKinds.privateMessage)],
|
privateMessage: [
|
||||||
|
savedSettings.includes(this.notificationKinds.privateMessage),
|
||||||
|
],
|
||||||
zap: [savedSettings.includes(this.notificationKinds.zap)],
|
zap: [savedSettings.includes(this.notificationKinds.zap)],
|
||||||
follower: [savedSettings.includes(this.notificationKinds.follower)],
|
follower: [savedSettings.includes(this.notificationKinds.follower)],
|
||||||
});
|
});
|
||||||
@@ -75,6 +77,6 @@ export class SettingsNotificationsComponent implements OnInit {
|
|||||||
|
|
||||||
private loadNotificationSettings(): number[] {
|
private loadNotificationSettings(): number[] {
|
||||||
const storedSettings = localStorage.getItem('notificationSettings');
|
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()">
|
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
|
||||||
<!-- Section -->
|
<!-- Section -->
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="text-secondary">
|
<div class="text-secondary">
|
||||||
Following information is publicly displayed, be careful!
|
Following information is publicly displayed, be careful!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -13,7 +13,11 @@
|
|||||||
<div class="sm:col-span-4">
|
<div class="sm:col-span-4">
|
||||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||||
<mat-label>Name</mat-label>
|
<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 />
|
<input [formControlName]="'name'" matInput />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,10 +50,16 @@
|
|||||||
<div class="sm:col-span-4">
|
<div class="sm:col-span-4">
|
||||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||||
<mat-label>About</mat-label>
|
<mat-label>About</mat-label>
|
||||||
<textarea matInput [formControlName]="'about'" cdkTextareaAutosize [cdkAutosizeMinRows]="5"></textarea>
|
<textarea
|
||||||
|
matInput
|
||||||
|
[formControlName]="'about'"
|
||||||
|
cdkTextareaAutosize
|
||||||
|
[cdkAutosizeMinRows]="5"
|
||||||
|
></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div class="text-hint mt-1 text-md">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -69,45 +79,51 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- LUD06 -->
|
<!-- LUD06 -->
|
||||||
<div class="sm:col-span-4">
|
<div class="sm:col-span-4">
|
||||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||||
<mat-label>LUD06</mat-label>
|
<mat-label>LUD06</mat-label>
|
||||||
<input [formControlName]="'lud06'" matInput />
|
<input [formControlName]="'lud06'" matInput />
|
||||||
<mat-hint>
|
<mat-hint>
|
||||||
LUD06 is an LNURL (Lightning Network URL) for receiving Bitcoin payments over the Lightning Network.
|
LUD06 is an LNURL (Lightning Network URL) for receiving
|
||||||
</mat-hint>
|
Bitcoin payments over the Lightning Network.
|
||||||
</mat-form-field>
|
</mat-hint>
|
||||||
</div>
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- LUD16 -->
|
<!-- LUD16 -->
|
||||||
<div class="sm:col-span-4">
|
<div class="sm:col-span-4">
|
||||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||||
<mat-label>LUD16</mat-label>
|
<mat-label>LUD16</mat-label>
|
||||||
<input [formControlName]="'lud16'" matInput />
|
<input [formControlName]="'lud16'" matInput />
|
||||||
<mat-hint>
|
<mat-hint>
|
||||||
LUD16 is a Lightning address, similar to an email format, used to receive Bitcoin payments via the Lightning Network.
|
LUD16 is a Lightning address, similar to an email
|
||||||
</mat-hint>
|
format, used to receive Bitcoin payments via the
|
||||||
</mat-form-field>
|
Lightning Network.
|
||||||
</div>
|
</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
<!-- NIP05 -->
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
|
<!-- 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>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- 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 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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, ViewEncapsulation, ChangeDetectionStrategy, OnInit } from '@angular/core';
|
import {
|
||||||
import { FormsModule, ReactiveFormsModule, FormGroup, FormBuilder, Validators } from '@angular/forms';
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
ViewEncapsulation,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
FormBuilder,
|
||||||
|
FormGroup,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
Validators,
|
||||||
|
} from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatOptionModule } from '@angular/material/core';
|
import { MatOptionModule } from '@angular/material/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
@@ -14,8 +25,8 @@ import { hexToBytes } from '@noble/hashes/utils';
|
|||||||
import { MetadataService } from 'app/services/metadata.service';
|
import { MetadataService } from 'app/services/metadata.service';
|
||||||
import { RelayService } from 'app/services/relay.service';
|
import { RelayService } from 'app/services/relay.service';
|
||||||
import { SignerService } from 'app/services/signer.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 { PasswordDialogComponent } from 'app/shared/password-dialog/password-dialog.component';
|
||||||
|
import { NostrEvent, UnsignedEvent, finalizeEvent } from 'nostr-tools';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'settings-profile',
|
selector: 'settings-profile',
|
||||||
@@ -33,7 +44,7 @@ import { PasswordDialogComponent } from 'app/shared/password-dialog/password-dia
|
|||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatOptionModule,
|
MatOptionModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
CommonModule
|
CommonModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsProfileComponent implements OnInit {
|
export class SettingsProfileComponent implements OnInit {
|
||||||
@@ -46,8 +57,8 @@ export class SettingsProfileComponent implements OnInit {
|
|||||||
private metadataService: MetadataService,
|
private metadataService: MetadataService,
|
||||||
private relayService: RelayService,
|
private relayService: RelayService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.profileForm = this.fb.group({
|
this.profileForm = this.fb.group({
|
||||||
@@ -59,15 +70,23 @@ export class SettingsProfileComponent implements OnInit {
|
|||||||
picture: [''],
|
picture: [''],
|
||||||
banner: [''],
|
banner: [''],
|
||||||
lud06: [''],
|
lud06: [''],
|
||||||
lud16: ['', Validators.pattern("^[a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,4}$")],
|
lud16: [
|
||||||
nip05: ['', Validators.pattern("^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$")]
|
'',
|
||||||
|
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();
|
this.setValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setValues() {
|
async setValues() {
|
||||||
let kind0 = await this.metadataService.getUserMetadata(this.signerService.getPublicKey());
|
let kind0 = await this.metadataService.getUserMetadata(
|
||||||
|
this.signerService.getPublicKey()
|
||||||
|
);
|
||||||
if (kind0) {
|
if (kind0) {
|
||||||
this.profileForm.setValue({
|
this.profileForm.setValue({
|
||||||
name: kind0.name || '',
|
name: kind0.name || '',
|
||||||
@@ -100,7 +119,8 @@ export class SettingsProfileComponent implements OnInit {
|
|||||||
const storedPassword = this.signerService.getPassword();
|
const storedPassword = this.signerService.getPassword();
|
||||||
if (storedPassword) {
|
if (storedPassword) {
|
||||||
try {
|
try {
|
||||||
const privateKey = await this.signerService.getSecretKey(storedPassword);
|
const privateKey =
|
||||||
|
await this.signerService.getSecretKey(storedPassword);
|
||||||
this.signEvent(privateKey);
|
this.signEvent(privateKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -108,46 +128,54 @@ export class SettingsProfileComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
const dialogRef = this.dialog.open(PasswordDialogComponent, {
|
const dialogRef = this.dialog.open(PasswordDialogComponent, {
|
||||||
width: '300px',
|
width: '300px',
|
||||||
disableClose: true
|
disableClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(async result => {
|
dialogRef.afterClosed().subscribe(async (result) => {
|
||||||
if (result && result.password) {
|
if (result && result.password) {
|
||||||
try {
|
try {
|
||||||
const privateKey = await this.signerService.getSecretKey(result.password);
|
const privateKey =
|
||||||
|
await this.signerService.getSecretKey(
|
||||||
|
result.password
|
||||||
|
);
|
||||||
this.signEvent(privateKey);
|
this.signEvent(privateKey);
|
||||||
if (result.duration != 0) {
|
if (result.duration != 0) {
|
||||||
this.signerService.savePassword(result.password, result.duration);
|
this.signerService.savePassword(
|
||||||
|
result.password,
|
||||||
|
result.duration
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Password not provided');
|
console.error('Password not provided');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} else if (this.signerService.isUsingExtension()) {
|
} else if (this.signerService.isUsingExtension()) {
|
||||||
const unsignedEvent: UnsignedEvent = this.signerService.getUnsignedEvent(0, [], this.content);
|
const unsignedEvent: UnsignedEvent =
|
||||||
const signedEvent = await this.signerService.signEventWithExtension(unsignedEvent);
|
this.signerService.getUnsignedEvent(0, [], this.content);
|
||||||
|
const signedEvent =
|
||||||
|
await this.signerService.signEventWithExtension(unsignedEvent);
|
||||||
this.publishSignedEvent(signedEvent);
|
this.publishSignedEvent(signedEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async signEvent(privateKey: string) {
|
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 privateKeyBytes = hexToBytes(privateKey);
|
||||||
const signedEvent: NostrEvent = finalizeEvent(unsignedEvent, privateKeyBytes);
|
const signedEvent: NostrEvent = finalizeEvent(
|
||||||
|
unsignedEvent,
|
||||||
|
privateKeyBytes
|
||||||
|
);
|
||||||
this.publishSignedEvent(signedEvent);
|
this.publishSignedEvent(signedEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
publishSignedEvent(signedEvent: NostrEvent) {
|
publishSignedEvent(signedEvent: NostrEvent) {
|
||||||
this.relayService.publishEventToRelays(signedEvent);
|
this.relayService.publishEventToRelays(signedEvent);
|
||||||
console.log("Profile Updated!");
|
console.log('Profile Updated!');
|
||||||
this.router.navigate([`/profile`]);
|
this.router.navigate([`/profile`]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,42 +3,72 @@
|
|||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
|
||||||
<mat-label>Add Relay</mat-label>
|
<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" />
|
<input matInput [(ngModel)]="newRelayUrl" placeholder="Relay URL" />
|
||||||
<button mat-icon-button matSuffix (click)="addRelay()">
|
<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>
|
</button>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Relays -->
|
<!-- Relays -->
|
||||||
<div class="mt-8 flex flex-col divide-y border-b border-t">
|
<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 items-center">
|
||||||
<div class="flex h-10 w-10 flex-0 items-center justify-center overflow-hidden rounded-full">
|
<div
|
||||||
<img class="h-full w-full object-cover" [src]="getSafeUrl(relayFavIcon(relay.url))"
|
class="flex h-10 w-10 flex-0 items-center justify-center overflow-hidden rounded-full"
|
||||||
onerror="this.src='/images/avatars/avatar-placeholder.png'" alt="relay avatar" />
|
>
|
||||||
|
<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>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<div class="font-medium">{{ relay.url }}</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex items-center sm:ml-auto sm:mt-0">
|
<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-form-field
|
||||||
<mat-select [(ngModel)]="relay.accessType" (selectionChange)="updateRelayAccess(relay)">
|
class="angor-mat-dense w-50"
|
||||||
|
[subscriptSizing]="'dynamic'"
|
||||||
|
>
|
||||||
|
<mat-select
|
||||||
|
[(ngModel)]="relay.accessType"
|
||||||
|
(selectionChange)="updateRelayAccess(relay)"
|
||||||
|
>
|
||||||
<mat-select-trigger class="text-md">
|
<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-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>
|
<div class="font-medium">{{ option.label }}</div>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button mat-icon-button (click)="removeRelay(relay.url)">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { CommonModule, TitleCasePipe } from '@angular/common';
|
import { CommonModule, TitleCasePipe } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
|
NgZone,
|
||||||
OnInit,
|
OnInit,
|
||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
ChangeDetectorRef,
|
|
||||||
NgZone,
|
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@@ -52,7 +52,7 @@ export class SettingsRelayComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// Subscribe to relays observable
|
// Subscribe to relays observable
|
||||||
this.subscriptions.add(
|
this.subscriptions.add(
|
||||||
this.relayService.getRelays().subscribe(relays => {
|
this.relayService.getRelays().subscribe((relays) => {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
this.relays = relays;
|
this.relays = relays;
|
||||||
this.cdr.markForCheck(); // Mark the component for check
|
this.cdr.markForCheck(); // Mark the component for check
|
||||||
@@ -65,17 +65,20 @@ export class SettingsRelayComponent implements OnInit {
|
|||||||
{
|
{
|
||||||
label: 'Read',
|
label: 'Read',
|
||||||
value: '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',
|
label: 'Write',
|
||||||
value: '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',
|
label: 'Read and Write',
|
||||||
value: 'read-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 {
|
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';
|
return safeUrl + '/favicon.ico';
|
||||||
}
|
}
|
||||||
|
|
||||||
getSafeUrl(url: string): SafeUrl {
|
getSafeUrl(url: string): SafeUrl {
|
||||||
return this.sanitizer.bypassSecurityTrustUrl(url);
|
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