├── src ├── assets │ ├── .gitkeep │ ├── blur1.png │ ├── blur2.png │ ├── blur3.png │ ├── blur4.png │ ├── blur5.png │ ├── youtubeplay.png │ ├── community-banner-1.png │ ├── icons │ │ └── logo-without-text-zapddit.svg │ └── loader.svg ├── app │ ├── app.component.scss │ ├── component │ │ ├── hashtag │ │ │ ├── hashtag.component.scss │ │ │ ├── hashtag.component.html │ │ │ ├── hashtag.component.spec.ts │ │ │ └── hashtag.component.ts │ │ ├── event-feed │ │ │ ├── event-feed.component.scss │ │ │ ├── home-feed.component.ts │ │ │ └── event-feed.component.spec.ts │ │ ├── quoted-event │ │ │ ├── quoted-event.component.scss │ │ │ ├── quoted-event.component.html │ │ │ ├── quoted-event.component.spec.ts │ │ │ └── quoted-event.component.ts │ │ ├── single-post │ │ │ ├── single-post.component.scss │ │ │ ├── single-post.component.html │ │ │ ├── single-post.component.ts │ │ │ └── single-post.component.spec.ts │ │ ├── user-mention │ │ │ ├── user-mention.component.scss │ │ │ ├── user-mention.component.html │ │ │ ├── user-mention.component.spec.ts │ │ │ └── user-mention.component.ts │ │ ├── userprofile │ │ │ ├── userprofile.component.scss │ │ │ ├── userprofile.component.html │ │ │ ├── userprofile.component.spec.ts │ │ │ └── userprofile.component.ts │ │ ├── community-card │ │ │ ├── community-card.component.scss │ │ │ ├── community-card.component.spec.ts │ │ │ ├── community-card.component.ts │ │ │ └── community-card.component.html │ │ ├── note-composer │ │ │ ├── note-composer.component.scss │ │ │ ├── note-composer.component.spec.ts │ │ │ ├── note-composer.component.html │ │ │ └── note-composer.component.ts │ │ ├── peopleifollow │ │ │ ├── peopleifollow.component.scss │ │ │ ├── peopleifollow.component.spec.ts │ │ │ ├── peopleifollow.component.ts │ │ │ └── peopleifollow.component.html │ │ ├── onboarding-wizard │ │ │ ├── onboarding-wizard.component.scss │ │ │ ├── onboarding-wizard.component.spec.ts │ │ │ └── onboarding-wizard.component.ts │ │ ├── preferences-page │ │ │ ├── preferences-page.component.scss │ │ │ └── preferences-page.component.spec.ts │ │ ├── user-pic-and-name │ │ │ ├── user-pic-and-name.component.scss │ │ │ ├── user-pic-and-name.component.spec.ts │ │ │ ├── user-pic-and-name.component.html │ │ │ └── user-pic-and-name.component.ts │ │ ├── contact-card │ │ │ ├── contact-card.component.scss │ │ │ ├── contact-card.component.spec.ts │ │ │ ├── contact-card.component.html │ │ │ └── contact-card.component.ts │ │ ├── topic │ │ │ ├── topic.component.scss │ │ │ ├── topic.component.html │ │ │ ├── topic.component.spec.ts │ │ │ └── topic.component.ts │ │ ├── create-community │ │ │ ├── create-community.component.scss │ │ │ ├── create-community.component.spec.ts │ │ │ ├── create-community.component.ts │ │ │ └── create-community.component.html │ │ ├── zapdialog │ │ │ ├── zapdialog.component.scss │ │ │ ├── zapdialog.component.spec.ts │ │ │ ├── zapdialog.component.html │ │ │ └── zapdialog.component.ts │ │ ├── profile │ │ │ ├── profile.component.spec.ts │ │ │ └── profile.component.scss │ │ └── event-card │ │ │ ├── event-card.component.scss │ │ │ └── event-card.component.spec.ts │ ├── model │ │ ├── index.ts │ │ ├── NDKUserProfileWithNpub.ts │ │ ├── user.ts │ │ ├── ZapSplitConfig.ts │ │ ├── community.ts │ │ └── relay.ts │ ├── page │ │ ├── login-page │ │ │ ├── login-page.component.scss │ │ │ ├── login-page.component.spec.ts │ │ │ ├── login-page.component.ts │ │ │ └── login-page.component.html │ │ └── community-list │ │ │ ├── community-list.component.scss │ │ │ ├── community-list.component.spec.ts │ │ │ ├── community-list.component.ts │ │ │ └── community-list.component.html │ ├── enum │ │ └── FeedType.ts │ ├── sortlogic │ │ ├── SortLogic.ts │ │ └── ReverseChrono.ts │ ├── custom.d.ts │ ├── pipe │ │ ├── newLineToBr.pipe.ts │ │ ├── abbreviateId.pipe.ts │ │ ├── formatTimeStamp.pipe.ts │ │ └── short-number.pipe.ts │ ├── directive │ │ ├── ClickStopPropagation.ts │ │ └── ImageLoaderDirective.ts │ ├── service │ │ ├── relay.service.spec.ts │ │ ├── topic.service.spec.ts │ │ ├── community.service.spec.ts │ │ ├── btc-connect.service.spec.ts │ │ ├── zappeditdb.service.spec.ts │ │ ├── ndkprovider.service.spec.ts │ │ ├── object-cache.service.spec.ts │ │ ├── community-page.service.spec.ts │ │ ├── community-cache.service.spec.ts │ │ ├── btc-connect.service.ts │ │ ├── zappeditdb.service.ts │ │ ├── community-cache.service.ts │ │ ├── relay.service.ts │ │ ├── community-page.service.ts │ │ ├── object-cache.service.ts │ │ └── community.service.ts │ ├── observable-service │ │ ├── community-event.service.spec.ts │ │ └── community-event.service.ts │ ├── util │ │ ├── Util.ts │ │ ├── Uploader.ts │ │ ├── IntlHashtagLinkifyPlugin.ts │ │ ├── Constants.ts │ │ ├── Translators.ts │ │ ├── ZapdditRouteReuseStrategy.ts │ │ ├── ZapSplitUtil.ts │ │ └── LoginUtil.ts │ ├── filter │ │ ├── HashTagFilter.ts │ │ └── HashTagFilter.spec.ts │ ├── testing │ │ ├── mock-active-router.ts │ │ └── CommonTestingModule.ts │ ├── buffer │ │ ├── EventBuffer.ts │ │ └── EventBuffer.spec.ts │ ├── app.component.spec.ts │ ├── app-routing.module.ts │ ├── app.module.ts │ └── app.component.html ├── favicon.ico ├── .well-known │ └── nostr.json ├── main.ts ├── manifest.webmanifest ├── snow.scss └── index.html ├── screenshot.png ├── .prettierrc ├── .prettierignore ├── crowdin.yml ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── tsconfig.spec.json ├── tsconfig.app.json ├── .editorconfig ├── .gitignore ├── ngsw-config.json ├── tsconfig.json ├── LICENSE ├── .github └── workflows │ └── gh-pages.yml ├── README.md ├── package.json └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/hashtag/hashtag.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './relay'; 2 | -------------------------------------------------------------------------------- /src/app/page/login-page/login-page.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/event-feed/event-feed.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/quoted-event/quoted-event.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/single-post/single-post.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/user-mention/user-mention.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/userprofile/userprofile.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/page/community-list/community-list.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/community-card/community-card.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/note-composer/note-composer.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/peopleifollow/peopleifollow.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/onboarding-wizard/onboarding-wizard.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/preferences-page/preferences-page.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/user-pic-and-name/user-pic-and-name.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivganes/zapddit/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivganes/zapddit/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/assets/blur1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivganes/zapddit/HEAD/src/assets/blur1.png -------------------------------------------------------------------------------- /src/assets/blur2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivganes/zapddit/HEAD/src/assets/blur2.png -------------------------------------------------------------------------------- /src/assets/blur3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivganes/zapddit/HEAD/src/assets/blur3.png -------------------------------------------------------------------------------- /src/assets/blur4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivganes/zapddit/HEAD/src/assets/blur4.png -------------------------------------------------------------------------------- /src/assets/blur5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivganes/zapddit/HEAD/src/assets/blur5.png -------------------------------------------------------------------------------- /src/app/component/contact-card/contact-card.component.scss: -------------------------------------------------------------------------------- 1 | .ml-10{ 2 | margin-left: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "bracketSameLine": true, 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/youtubeplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivganes/zapddit/HEAD/src/assets/youtubeplay.png -------------------------------------------------------------------------------- /src/assets/community-banner-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivganes/zapddit/HEAD/src/assets/community-banner-1.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .angular/cache 2 | .vscode 3 | 4 | node_modules 5 | angular.json 6 | tsconfig.json 7 | tsconfig.app.json -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /src/assets/i18n/en.json 3 | translation: /src/assets/i18n/%two_letters_code%.json 4 | -------------------------------------------------------------------------------- /src/app/component/topic/topic.component.scss: -------------------------------------------------------------------------------- 1 | .pl-10{ 2 | margin-left: 10px; 3 | } 4 | 5 | .p-2{ 6 | margin: 2px; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/component/create-community/create-community.component.scss: -------------------------------------------------------------------------------- 1 | .ptb-3{ 2 | padding-top: 3px; 3 | padding-bottom: 3px; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/component/user-mention/user-mention.component.html: -------------------------------------------------------------------------------- 1 | @{{displayName}} -------------------------------------------------------------------------------- /src/app/component/userprofile/userprofile.component.html: -------------------------------------------------------------------------------- 1 | {{ getCurrentUserProfile()?.name }} 2 | -------------------------------------------------------------------------------- /src/app/enum/FeedType.ts: -------------------------------------------------------------------------------- 1 | export enum FeedType{ 2 | TOPICS_FEED = "TOPICS_FEED", 3 | COMMUNITIES_FEED = "COMMUNITIES_FEED" 4 | } -------------------------------------------------------------------------------- /src/.well-known/nostr.json: -------------------------------------------------------------------------------- 1 | { 2 | "names": { 3 | "zapddit": "748bfa87c437b294164d1784b324a0d2e9495c8268e0044e3c7796a3b158c9d8" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /src/app/sortlogic/SortLogic.ts: -------------------------------------------------------------------------------- 1 | import { NDKEvent } from "@nostr-dev-kit/ndk"; 2 | 3 | export interface SortLogic{ 4 | compare(event1:NDKEvent, event2:NDKEvent): number 5 | } -------------------------------------------------------------------------------- /src/app/component/hashtag/hashtag.component.html: -------------------------------------------------------------------------------- 1 | #{{topic}} 2 | -------------------------------------------------------------------------------- /src/app/model/NDKUserProfileWithNpub.ts: -------------------------------------------------------------------------------- 1 | import { NDKUserProfile } from '@nostr-dev-kit/ndk'; 2 | 3 | export type NDKUserProfileWithNpub = { 4 | profile:NDKUserProfile | undefined, 5 | npub:string, 6 | hexPubKey:string 7 | } 8 | -------------------------------------------------------------------------------- /src/app/component/single-post/single-post.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Loading event... 4 | 5 |
6 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | platformBrowserDynamic() 6 | .bootstrapModule(AppModule) 7 | .catch(err => { 8 | console.error(err); 9 | }); 10 | -------------------------------------------------------------------------------- /src/app/model/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | npub?: string; 3 | hexPubKey: string; 4 | displayName?: string; 5 | name?:string; 6 | nip05?:string; 7 | pictureUrl?:string; 8 | about?:string; 9 | lud06?:string; 10 | lud16?:string; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/component/quoted-event/quoted-event.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Loading quoted event... 4 | 5 |
6 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/app/sortlogic/ReverseChrono.ts: -------------------------------------------------------------------------------- 1 | import { NDKEvent } from "@nostr-dev-kit/ndk"; 2 | import { SortLogic } from "./SortLogic"; 3 | 4 | export class ReverseChrono implements SortLogic{ 5 | compare(event1: NDKEvent, event2: NDKEvent): number { 6 | return event2.created_at! - event1.created_at!; 7 | } 8 | } -------------------------------------------------------------------------------- /src/app/model/ZapSplitConfig.ts: -------------------------------------------------------------------------------- 1 | 2 | export default interface ZapSplitConfig{ 3 | developers: HexKeyWithSplitPercentage[]; 4 | translators: HexKeyWithSplitPercentage[]; 5 | } 6 | 7 | export interface HexKeyWithSplitPercentage{ 8 | hexKey: string; 9 | percentage: number 10 | } 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/app/component/zapdialog/zapdialog.component.scss: -------------------------------------------------------------------------------- 1 | .card-media-image{ 2 | height:56px; 3 | width:56px; 4 | } 5 | 6 | .center{ 7 | text-align: center; 8 | } 9 | 10 | .space-around{ 11 | padding: 0 50px; 12 | } 13 | 14 | .pt-5{ 15 | padding-top: 5px; 16 | } 17 | 18 | .clr-control-container, .clr-input { 19 | width: 100%; 20 | } 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/app/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "light-bolt11-decoder" { 2 | export function decode(pr?: string): ParsedInvoice; 3 | 4 | export interface ParsedInvoice { 5 | paymentRequest: string; 6 | sections: Section[]; 7 | } 8 | 9 | export interface Section { 10 | name: string; 11 | value: string | Uint8Array | number | undefined; 12 | } 13 | } -------------------------------------------------------------------------------- /src/app/pipe/newLineToBr.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'newLineToBr' 5 | }) 6 | export class NewLineToBrPipe implements PipeTransform { 7 | 8 | transform(text?:string): string { 9 | if(text) 10 | return text?.replaceAll('\n','
') 11 | else 12 | return ''; 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/directive/ClickStopPropagation.ts: -------------------------------------------------------------------------------- 1 | import {Directive, HostListener} from "@angular/core"; 2 | 3 | @Directive({ 4 | selector: "[click-stop-propagation]" 5 | }) 6 | export class ClickStopPropagation 7 | { 8 | @HostListener("click", ["$event"]) 9 | public onClick(event: any): void 10 | { 11 | event.stopImmediatePropagation(); 12 | event.preventDefault(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/component/event-feed/home-feed.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { EventFeedComponent } from "./event-feed.component"; 3 | 4 | @Component({ 5 | selector: 'app-event-feed', 6 | templateUrl: './event-feed.component.html', 7 | styleUrls: ['./event-feed.component.scss'], 8 | }) 9 | export class HomeFeedComponent extends EventFeedComponent{ 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/component/topic/topic.component.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | {{ topic }} 5 | 6 | -------------------------------------------------------------------------------- /src/app/pipe/abbreviateId.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'abbreviateId' 5 | }) 6 | export class AbbreviateIdPipe implements PipeTransform { 7 | 8 | transform(id?:string): string { 9 | if(id){ 10 | const len = id.length; 11 | return id.slice(0,9)+'...'+id.slice(len-10,len-1); 12 | } 13 | return ''; 14 | } 15 | } -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zapddit", 3 | "short_name": "zapddit", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [{ 10 | "src": "assets/icons/logo-without-text-zapddit.svg", 11 | "sizes": "48x48 72x72 96x96 128x128 256x256", 12 | "type": "image/svg+xml", 13 | "purpose": "any" 14 | }] 15 | } 16 | -------------------------------------------------------------------------------- /src/app/model/community.ts: -------------------------------------------------------------------------------- 1 | import { NDKUserProfile } from "@nostr-dev-kit/ndk"; 2 | 3 | export interface Community{ 4 | creatorHexKey?:string, 5 | name?:string, 6 | displayName?:string, 7 | id?:string, 8 | description?:string, 9 | rules?:string, 10 | image?:string, 11 | creatorProfile?:NDKUserProfile, 12 | moderatorHexKeys?:string[], 13 | followersHexKeys?:string[], 14 | created_at?: number 15 | } 16 | -------------------------------------------------------------------------------- /src/app/service/relay.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { RelayService } from './relay.service'; 4 | 5 | describe('RelayService', () => { 6 | let service: RelayService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(RelayService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/service/topic.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { TopicService } from './topic.service'; 4 | 5 | describe('TopicService', () => { 6 | let service: TopicService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(TopicService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/service/community.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { CommunityService } from './community.service'; 4 | 5 | describe('CommunityService', () => { 6 | let service: CommunityService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(CommunityService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/directive/ImageLoaderDirective.ts: -------------------------------------------------------------------------------- 1 | import { Directive, HostListener, Input, HostBinding } from '@angular/core'; 2 | 3 | @Directive({ selector: '[imageLoader]' }) 4 | export class ImageLoaderDirective { 5 | @Input('src') imageSrc?: string; 6 | @HostListener('load') 7 | loadImage() { 8 | if (this.imageSrc) { 9 | this.srcAttr = this.imageSrc; 10 | } 11 | } 12 | 13 | @HostBinding('attr.src') srcAttr = '/assets/loader.svg'; 14 | constructor() {} 15 | } 16 | -------------------------------------------------------------------------------- /src/app/pipe/formatTimeStamp.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import moment from 'moment'; 3 | 4 | @Pipe({ 5 | name: 'formatTimestamp' 6 | }) 7 | export class formatTimestampPipe implements PipeTransform { 8 | 9 | transform(timestamp?: number, args?: any): any { 10 | if (timestamp) { 11 | return moment(timestamp * 1000).fromNow(); 12 | } else { 13 | return 'Unknown time'; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/app/service/btc-connect.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { BtcConnectService } from './btc-connect.service'; 4 | 5 | describe('BtcConnectService', () => { 6 | let service: BtcConnectService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(BtcConnectService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/service/zappeditdb.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ZappeditdbService } from './zappeditdb.service'; 4 | 5 | describe('ZappeditdbService', () => { 6 | let service: ZappeditdbService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ZappeditdbService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/service/ndkprovider.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { NdkproviderService } from './ndkprovider.service'; 4 | 5 | describe('NdkproviderService', () => { 6 | let service: NdkproviderService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(NdkproviderService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/service/object-cache.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ObjectCacheService } from './object-cache.service'; 4 | 5 | describe('ObjectCacheService', () => { 6 | let service: ObjectCacheService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ObjectCacheService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /src/app/service/community-page.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { CommunityPageService } from './community-page.service'; 4 | 5 | describe('CommunityPageService', () => { 6 | let service: CommunityPageService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(CommunityPageService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/service/community-cache.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { CommunityCacheService } from './community-cache.service'; 4 | 5 | describe('CommunityCacheService', () => { 6 | let service: CommunityCacheService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(CommunityCacheService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/model/relay.ts: -------------------------------------------------------------------------------- 1 | export interface IRelay { 2 | name: string; 3 | url: string; 4 | read: boolean; 5 | write: boolean; 6 | } 7 | 8 | export class Relay implements IRelay { 9 | name: string; 10 | url: string; 11 | read: boolean; 12 | write: boolean; 13 | 14 | constructor(name: string, url: string, read: boolean = true, write: boolean = true) { 15 | this.name = name; 16 | this.url = url; 17 | this.read = read; 18 | this.write = write; 19 | } 20 | } -------------------------------------------------------------------------------- /src/app/observable-service/community-event.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { CommunityEventService } from './community-event.service'; 4 | 5 | describe('CommunityEventService', () => { 6 | let service: CommunityEventService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(CommunityEventService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/service/btc-connect.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class BtcConnectService { 7 | 8 | constructor() { 9 | console.log("Adding btc-connect/disconnect listener...") 10 | window.addEventListener('bc:connected', () => { 11 | console.log("btc CONNECT") 12 | window.addEventListener('bc:disconnected', () => { 13 | console.log("btc DISCONNECT") 14 | window.location.reload(); 15 | }) 16 | }) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/app/util/Util.ts: -------------------------------------------------------------------------------- 1 | import { decode as invoiceDecode } from "light-bolt11-decoder"; 2 | 3 | 4 | export class Util{ 5 | static getAmountFromInvoice(invoice: string): number|undefined { 6 | try { 7 | const parsed = invoiceDecode(invoice); 8 | 9 | const amountSection = parsed.sections.find((a:any) => a.name === "amount"); 10 | const amount = amountSection ? Number(amountSection.value as number | string) : undefined; 11 | return amount; 12 | } catch (e) { 13 | console.error(e); 14 | } 15 | return undefined; 16 | } 17 | } -------------------------------------------------------------------------------- /src/app/component/topic/topic.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TopicComponent } from './topic.component'; 4 | 5 | describe('TopicComponent', () => { 6 | let component: TopicComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ TopicComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(TopicComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/component/hashtag/hashtag.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HashtagComponent } from './hashtag.component'; 4 | 5 | describe('HashtagComponent', () => { 6 | let component: HashtagComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ HashtagComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(HashtagComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/component/peopleifollow/peopleifollow.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PeopleIFollowComponent } from './peopleifollow.component'; 4 | 5 | describe('PeopleIFollowComponent', () => { 6 | let component: PeopleIFollowComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [PeopleIFollowComponent] 12 | }); 13 | fixture = TestBed.createComponent(PeopleIFollowComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | 45 | src/debug -------------------------------------------------------------------------------- /src/app/component/zapdialog/zapdialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ZapdialogComponent } from './zapdialog.component'; 4 | 5 | describe('ZapdialogComponent', () => { 6 | let component: ZapdialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ZapdialogComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ZapdialogComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "lazy", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/app/component/userprofile/userprofile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { type ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserprofileComponent } from './userprofile.component'; 4 | 5 | describe('UserprofileComponent', () => { 6 | let component: UserprofileComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [UserprofileComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(UserprofileComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/component/contact-card/contact-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ContactCardComponent } from './contact-card.component'; 4 | 5 | describe('ContactCardComponent', () => { 6 | let component: ContactCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ContactCardComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ContactCardComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/component/quoted-event/quoted-event.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { QuotedEventComponent } from './quoted-event.component'; 4 | 5 | describe('QuotedEventComponent', () => { 6 | let component: QuotedEventComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ QuotedEventComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(QuotedEventComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/component/user-mention/user-mention.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserMentionComponent } from './user-mention.component'; 4 | 5 | describe('UserMentionComponent', () => { 6 | let component: UserMentionComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ UserMentionComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(UserMentionComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/page/community-list/community-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CommunityListComponent } from './community-list.component'; 4 | 5 | describe('CommunityListComponent', () => { 6 | let component: CommunityListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ CommunityListComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CommunityListComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/component/community-card/community-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CommunityCardComponent } from './community-card.component'; 4 | 5 | describe('CommunityCardComponent', () => { 6 | let component: CommunityCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ CommunityCardComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CommunityCardComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/component/create-community/create-community.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateCommunityComponent } from './create-community.component'; 4 | 5 | describe('CreateCommunityComponent', () => { 6 | let component: CreateCommunityComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ CreateCommunityComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CreateCommunityComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/component/user-pic-and-name/user-pic-and-name.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserPicAndNameComponent } from './user-pic-and-name.component'; 4 | 5 | describe('UserPicAndNameComponent', () => { 6 | let component: UserPicAndNameComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ UserPicAndNameComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(UserPicAndNameComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/filter/HashTagFilter.ts: -------------------------------------------------------------------------------- 1 | import { NDKEvent, NDKTag } from "@nostr-dev-kit/ndk"; 2 | 3 | export class HashTagFilter{ 4 | static notHashTags(mutedTags:string[]):(value: NDKEvent, index: number, array: NDKEvent[]) => boolean{ 5 | console.log("notHashTags: mutedTags "+ mutedTags); 6 | let filter:(value: NDKEvent) => boolean 7 | filter = (event:NDKEvent):boolean=>{ 8 | const eventTextLowerCase = event.content?.toLowerCase(); 9 | if(!eventTextLowerCase) return true; 10 | for (let tag of mutedTags) { 11 | if (eventTextLowerCase.includes('#' + tag.toLowerCase())) { 12 | return false; 13 | } 14 | } 15 | return true; 16 | } 17 | return filter; 18 | } 19 | } -------------------------------------------------------------------------------- /src/app/util/Uploader.ts: -------------------------------------------------------------------------------- 1 | export interface UploaderResult { 2 | url?: string; 3 | error?: string; 4 | } 5 | 6 | export default class Uploader { 7 | static async upload(file: File | Blob): Promise { 8 | const formData = new FormData(); 9 | formData.append('fileToUpload', file); 10 | formData.append('submit', 'Upload'); 11 | 12 | const response = await fetch('https://nostr.build/api/v2/upload/files', { 13 | headers: { 14 | accept: 'application/json', 15 | }, 16 | method: 'POST', 17 | body: formData, 18 | }); 19 | if (response.ok) { 20 | const data = await response.json(); 21 | console.log(data); 22 | return { 23 | url: new URL(data.data[0].url).toString(), 24 | }; 25 | } 26 | return { 27 | error: 'Upload failed', 28 | }; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/app/component/profile/profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProfileComponent } from './profile.component'; 4 | import { CommonTestingModule } from 'src/app/testing/CommonTestingModule'; 5 | 6 | describe('ProfileComponent', () => { 7 | CommonTestingModule.setUpTestBed(ProfileComponent) 8 | 9 | let component: ProfileComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | declarations: [ ProfileComponent ] 15 | }) 16 | .compileComponents(); 17 | 18 | fixture = TestBed.createComponent(ProfileComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/component/event-feed/event-feed.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EventFeedComponent } from './event-feed.component'; 4 | import { CommonTestingModule } from 'src/app/testing/CommonTestingModule'; 5 | 6 | describe('EventFeedComponent', () => { 7 | CommonTestingModule.setUpTestBed(EventFeedComponent) 8 | 9 | let component: EventFeedComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | declarations: [EventFeedComponent], 15 | }).compileComponents(); 16 | 17 | fixture = TestBed.createComponent(EventFeedComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/page/login-page/login-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginPageComponent } from './login-page.component'; 4 | import { CommonTestingModule } from 'src/app/testing/CommonTestingModule'; 5 | 6 | describe('LoginPageComponent', () => { 7 | let component: LoginPageComponent; 8 | let fixture: ComponentFixture; 9 | CommonTestingModule.setUpTestBed(LoginPageComponent); 10 | 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | declarations: [ LoginPageComponent ] 15 | }) 16 | .compileComponents(); 17 | 18 | fixture = TestBed.createComponent(LoginPageComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/component/single-post/single-post.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { NDKEvent } from '@nostr-dev-kit/ndk'; 4 | import { NdkproviderService } from 'src/app/service/ndkprovider.service'; 5 | 6 | @Component({ 7 | selector: 'app-single-post', 8 | templateUrl: './single-post.component.html', 9 | styleUrls: ['./single-post.component.scss'], 10 | }) 11 | export class SinglePostComponent { 12 | 13 | event?:NDKEvent|null 14 | 15 | constructor(route:ActivatedRoute,private ndkProvider: NdkproviderService){ 16 | route.params.subscribe(async params => { 17 | this.event = undefined; 18 | let noteid = params['noteid']; 19 | this.event = await this.getNote(noteid) 20 | }); 21 | } 22 | 23 | async getNote(id:string){ 24 | return await this.ndkProvider.fetchEventFromId(id) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/component/userprofile/userprofile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { type NDKUserProfile } from '@nostr-dev-kit/ndk'; 3 | import { NdkproviderService } from 'src/app/service/ndkprovider.service'; 4 | 5 | @Component({ 6 | selector: 'app-userprofile', 7 | templateUrl: './userprofile.component.html', 8 | styleUrls: ['./userprofile.component.scss'], 9 | }) 10 | export class UserprofileComponent { 11 | ndkProvider: NdkproviderService; 12 | constructor(ndkProvider: NdkproviderService) { 13 | this.ndkProvider = ndkProvider; 14 | } 15 | 16 | isLoggedIn(): boolean { 17 | return this.ndkProvider.isLoggedIn(); 18 | } 19 | 20 | isProfileLoaded(): boolean { 21 | return this.ndkProvider.currentUserProfile !== undefined; 22 | } 23 | 24 | getCurrentUserProfile(): NDKUserProfile | undefined { 25 | return this.ndkProvider.getCurrentUserProfile(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/component/note-composer/note-composer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NoteComposerComponent } from './note-composer.component'; 4 | import { CommonTestingModule } from 'src/app/testing/CommonTestingModule'; 5 | 6 | describe('NoteComposerComponent', () => { 7 | let component: NoteComposerComponent; 8 | let fixture: ComponentFixture; 9 | CommonTestingModule.setUpTestBed(NoteComposerComponent) 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | declarations: [ NoteComposerComponent ] 14 | }) 15 | .compileComponents(); 16 | 17 | fixture = TestBed.createComponent(NoteComposerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/testing/mock-active-router.ts: -------------------------------------------------------------------------------- 1 | // Refer - https://gist.github.com/dvaJi/cf552bbe6725535955f7a5eeb92d7d2e 2 | 3 | import { Params } from '@angular/router'; 4 | import { BehaviorSubject } from 'rxjs'; 5 | 6 | export class MockActivatedRoute { 7 | private innerTestParams?: any; 8 | private subject?: BehaviorSubject = new BehaviorSubject(this.testParams); 9 | 10 | params = this.subject?.asObservable(); 11 | queryParams = this.subject?.asObservable(); 12 | 13 | constructor(params?: Params) { 14 | if (params) { 15 | this.testParams = params; 16 | } else { 17 | this.testParams = {}; 18 | } 19 | } 20 | 21 | get testParams() { 22 | return this.innerTestParams; 23 | } 24 | 25 | set testParams(params: {}) { 26 | this.innerTestParams = params; 27 | this.subject?.next(params); 28 | } 29 | 30 | get snapshot() { 31 | return { params: this.testParams, queryParams: this.testParams }; 32 | } 33 | } -------------------------------------------------------------------------------- /src/app/component/preferences-page/preferences-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PreferencesPageComponent } from './preferences-page.component'; 4 | import { CommonTestingModule } from 'src/app/testing/CommonTestingModule'; 5 | 6 | describe('PreferencesPageComponent', () => { 7 | 8 | CommonTestingModule.setUpTestBed(PreferencesPageComponent) 9 | 10 | let component: PreferencesPageComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(async () => { 14 | await TestBed.configureTestingModule({ 15 | declarations: [PreferencesPageComponent], 16 | }).compileComponents(); 17 | 18 | fixture = TestBed.createComponent(PreferencesPageComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/component/onboarding-wizard/onboarding-wizard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { OnboardingWizardComponent } from './onboarding-wizard.component'; 4 | import { CommonTestingModule } from 'src/app/testing/CommonTestingModule'; 5 | 6 | describe('OnboardingWizardComponent', () => { 7 | CommonTestingModule.setUpTestBed(OnboardingWizardComponent) 8 | 9 | let component: OnboardingWizardComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | declarations: [ OnboardingWizardComponent ] 15 | }) 16 | .compileComponents(); 17 | 18 | fixture = TestBed.createComponent(OnboardingWizardComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/component/event-card/event-card.component.scss: -------------------------------------------------------------------------------- 1 | .iframe-placeholder 2 | { 3 | background: url('data:image/svg+xml;charset=utf-8,Loading video..') 0px 0px no-repeat; 4 | } 5 | 6 | .video { 7 | max-width: 100% !important; 8 | } 9 | 10 | .p-8{ 11 | padding:8px 12 | } 13 | 14 | .follow-ml8{ 15 | margin-left:8px 16 | } 17 | 18 | .next{ 19 | display: flex; 20 | flex-direction: row; 21 | } 22 | 23 | @media only screen 24 | and (max-width : 576px) { 25 | .next{ 26 | display: flex; 27 | flex-direction: column; 28 | } 29 | 30 | .follow-ml8{ 31 | margin-left:0px 32 | } 33 | } 34 | 35 | .youtube-player{ 36 | width:560px; 37 | } 38 | 39 | .shortened-content{ 40 | overflow: hidden; 41 | height: 300px; 42 | } 43 | 44 | .show-more-button{ 45 | bottom: 128px !important; 46 | z-index:999; 47 | } 48 | -------------------------------------------------------------------------------- /src/app/service/zappeditdb.service.ts: -------------------------------------------------------------------------------- 1 | import { User } from './../model/user'; 2 | import { Injectable } from '@angular/core'; 3 | import Dexie, { Table } from 'dexie'; 4 | import { Relay } from '../model'; 5 | 6 | const DATASTORE = { 7 | peopleIFollow: "++hexPubKey, name, displayName, pictureUrl, nip05, npub, about", 8 | mutedPeople:"++hexPubKey, name, displayName, pictureUrl, nip05, npub, about", 9 | subscribedRelays: "++name, url, read, write" 10 | }; 11 | const VERSION = 1; 12 | @Injectable({ 13 | providedIn: 'root' 14 | }) 15 | export class ZappeditdbService extends Dexie{ 16 | ready = false; 17 | peopleIFollow!: Table; 18 | mutedPeople!: Table; 19 | subscribedRelays!: Table; 20 | 21 | constructor() { 22 | super("ZappedItDB"); 23 | 24 | this.version(VERSION).stores(DATASTORE); 25 | 26 | this.version(Math.round(this.verno + 2)).stores({mutedPeople: DATASTORE.mutedPeople}); 27 | 28 | this.version(Math.round(this.verno + 3)).stores({subscribedRelays: DATASTORE.subscribedRelays}); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/component/hashtag/hashtag.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-hashtag', 6 | templateUrl: './hashtag.component.html', 7 | styleUrls: ['./hashtag.component.scss'] 8 | }) 9 | export class HashtagComponent { 10 | 11 | 12 | @Input() 13 | topic:string|undefined 14 | 15 | 16 | constructor(private router:Router){ 17 | 18 | } 19 | 20 | navigateToNewTopic(mouseEvent:MouseEvent){ 21 | mouseEvent.stopImmediatePropagation(); 22 | mouseEvent.preventDefault(); 23 | if(mouseEvent.metaKey || mouseEvent.ctrlKey || mouseEvent.button===1){ 24 | var url = this.router.serializeUrl(this.router.createUrlTree(['t/'+this.topic])); 25 | window.open(url, '_blank'); 26 | return 27 | } 28 | 29 | this.router.navigateByUrl('t/'+this.topic) 30 | } 31 | 32 | onMouseDown(event: MouseEvent) { 33 | // this is to be prevented for the middleclick event to be triggered as auxclick event 34 | event.preventDefault(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/buffer/EventBuffer.ts: -------------------------------------------------------------------------------- 1 | export class EventBuffer{ 2 | 3 | events?:T[] 4 | 5 | /** 6 | * 7 | * @param startIndex 8 | * @param endIndex 9 | * @returns items with indexes including startIndex and endIndex 10 | * NULL if startIndex > events.length 11 | * If endIndex > events.length, return upto last element of events 12 | */ 13 | getItemsWithIndexes(startIndex:number, endIndex:number):(T[]|null){ 14 | if(this.events){ 15 | if(startIndex>this.events.length-1){ 16 | return null; 17 | } 18 | if(endIndex >= this.events.length){ 19 | return this.events.slice(startIndex,this.events.length); 20 | } 21 | return this.events?.slice(startIndex,endIndex+1); 22 | } 23 | return [] 24 | } 25 | 26 | refillWithEntries(newEntries : T[]){ 27 | if(this.events){ 28 | this.events = this.events.concat(newEntries); 29 | } else{ 30 | this.events = newEntries; 31 | } 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/app/component/event-card/event-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EventCardComponent } from './event-card.component'; 4 | import { CommonTestingModule } from 'src/app/testing/CommonTestingModule'; 5 | import { ShortNumberPipe } from 'src/app/pipe/short-number.pipe'; 6 | import { formatTimestampPipe } from 'src/app/pipe/formatTimeStamp.pipe'; 7 | 8 | describe('EventCardComponent', () => { 9 | 10 | CommonTestingModule.setUpTestBed(EventCardComponent) 11 | 12 | let component: EventCardComponent; 13 | let fixture: ComponentFixture; 14 | 15 | beforeEach(async () => { 16 | await TestBed.configureTestingModule({ 17 | declarations: [EventCardComponent, 18 | ShortNumberPipe, 19 | formatTimestampPipe], 20 | }).compileComponents(); 21 | 22 | fixture = TestBed.createComponent(EventCardComponent); 23 | component = fixture.componentInstance; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/app/component/single-post/single-post.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SinglePostComponent } from './single-post.component'; 4 | import { ActivatedRoute } from '@angular/router'; 5 | import { MockActivatedRoute } from 'src/app/testing/mock-active-router'; 6 | 7 | describe('SinglePostComponent', () => { 8 | let component: SinglePostComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async () => { 12 | const activatedRouteStub = new MockActivatedRoute(); 13 | await TestBed.configureTestingModule({ 14 | declarations: [SinglePostComponent], 15 | providers: 16 | [ 17 | { 18 | provide: ActivatedRoute, 19 | useValue: activatedRouteStub 20 | } 21 | ] 22 | }).compileComponents(); 23 | 24 | fixture = TestBed.createComponent(SinglePostComponent); 25 | component = fixture.componentInstance; 26 | fixture.detectChanges(); 27 | }); 28 | 29 | it('should create', () => { 30 | expect(component).toBeTruthy(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | import { ClarityModule } from '@clr/angular'; 5 | import { CommonTestingModule } from './testing/CommonTestingModule'; 6 | 7 | describe('AppComponent', () => { 8 | CommonTestingModule.setUpTestBed(AppComponent) 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [RouterTestingModule,ClarityModule], 13 | declarations: [AppComponent], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | fixture.detectChanges() 20 | const app = fixture.componentInstance; 21 | expect(app).toBeTruthy(); 22 | }); 23 | 24 | it("should have as title 'zapddit'", () => { 25 | const fixture = TestBed.createComponent(AppComponent); 26 | fixture.detectChanges() 27 | const app = fixture.componentInstance; 28 | expect(app.title).toEqual('zapddit'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/app/service/community-cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ObjectCacheService } from './object-cache.service'; 3 | import { Community } from '../model/community'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class CommunityCacheService { 9 | 10 | constructor(private objectCache:ObjectCacheService) { } 11 | 12 | async fetchCommunityWithId(id:string):Promise{ 13 | const community:Community|undefined = await this.objectCache.communities.get(id); 14 | if(community){ 15 | console.log("community:hit"); 16 | return community; 17 | } 18 | console.log("community:miss"); 19 | return undefined; 20 | } 21 | 22 | async addCommunity(item:Community){ 23 | this.objectCache.communities.put(item, item.id); 24 | 25 | const self = this; 26 | setTimeout(() => { 27 | console.log("Deleting item with key "+ item.id) 28 | self.deleteCommunityWithId(item.id!) 29 | }, this.objectCache.defaultTTL*1000); 30 | } 31 | 32 | deleteCommunityWithId(id:string){ 33 | this.objectCache.communities.delete(id); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "skipLibCheck": true, 6 | "strictPropertyInitialization": false, 7 | "baseUrl": "./", 8 | "outDir": "./dist/out-tsc", 9 | "forceConsistentCasingInFileNames": true, 10 | "allowSyntheticDefaultImports":true, 11 | "strict": true, 12 | "noImplicitOverride": true, 13 | "noPropertyAccessFromIndexSignature": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "sourceMap": true, 17 | "declaration": false, 18 | "downlevelIteration": true, 19 | "experimentalDecorators": true, 20 | "moduleResolution": "node", 21 | "importHelpers": true, 22 | "target": "ES2022", 23 | "module": "ES2022", 24 | "useDefineForClassFields": false, 25 | "lib": [ 26 | "ES2022", 27 | "dom" 28 | ] 29 | }, 30 | "angularCompilerOptions": { 31 | "enableI18nLegacyMessageIdFormat": false, 32 | "strictInjectionParameters": true, 33 | "strictInputAccessModifiers": true, 34 | "strictTemplates": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/component/topic/topic.component.ts: -------------------------------------------------------------------------------- 1 | import { Component,Input } from '@angular/core'; 2 | import { TopicService } from '../../service/topic.service'; 3 | import { Constants } from 'src/app/util/Constants'; 4 | import { NdkproviderService } from '../../service/ndkprovider.service'; 5 | 6 | @Component({ 7 | selector: 'app-topic', 8 | templateUrl: './topic.component.html', 9 | styleUrls: ['./topic.component.scss'] 10 | }) 11 | export class TopicComponent { 12 | hideElement:boolean = true; 13 | 14 | @Input() 15 | isMobileScreen:boolean = false; 16 | 17 | @Input() 18 | topic:string; 19 | 20 | @Input() 21 | sidebarCollapsed:boolean = false; 22 | 23 | constructor(private topicService:TopicService, private ndkProvider:NdkproviderService){ 24 | } 25 | 26 | async onTopicDelete(evt:any){ 27 | evt.preventDefault(); 28 | evt.stopImmediatePropagation(); 29 | 30 | if(this.ndkProvider.isTryingZapddit){ 31 | this.topicService.unFollowTryZapddit(this.topic); 32 | return; 33 | } 34 | 35 | this.topicService.unfollowTopicInteroperableList(this.topic); 36 | 37 | await this.topicService.clearTopicsFromAppData(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Vivek Ganesan (https://vivekganesan.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/app/component/peopleifollow/peopleifollow.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { NdkproviderService } from 'src/app/service/ndkprovider.service'; 3 | import { ZappeditdbService } from 'src/app/service/zappeditdb.service'; 4 | import '@cds/core/icon/register.js'; 5 | import '@cds/core/button/register.js'; 6 | import { User } from 'src/app/model/user'; 7 | 8 | @Component({ 9 | selector: 'app-people-i-follow', 10 | templateUrl: './peopleifollow.component.html', 11 | styleUrls: ['./peopleifollow.component.scss'] 12 | }) 13 | 14 | export class PeopleIFollowComponent implements OnInit{ 15 | 16 | users: User[] = []; 17 | loadingPeopleYouFollow: boolean = false; 18 | 19 | constructor(private ndkProvider: NdkproviderService, private dbService:ZappeditdbService) { 20 | this.fetchContactList(); 21 | } 22 | 23 | fetchContactList(){ 24 | this.ndkProvider.fetchFollowersFromCache().then(cachedUsers =>{ 25 | this.users = cachedUsers; 26 | this.loadingPeopleYouFollow = false; 27 | }); 28 | } 29 | 30 | ngOnInit() { 31 | this.loadingPeopleYouFollow = true; 32 | } 33 | 34 | onContactListChange(){ 35 | this.fetchContactList(); 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | env: 8 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 9 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 10 | AWS_DEFAULT_REGION: "us-east-1" 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: '16' 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Build 28 | run: npm run build:prod 29 | 30 | - name: Deploy 31 | run: aws s3 rm s3://${{ secrets.AWS_S3_BUCKET_NAME }} --recursive && aws s3 sync ./dist/zapddit/ s3://${{ secrets.AWS_S3_BUCKET_NAME }} 32 | 33 | - name: "Create AWS Cloudfront Invalidation" 34 | run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*" 35 | 36 | # - name: Deploy 37 | # if: success() 38 | # uses: peaceiris/actions-gh-pages@v3 39 | # with: 40 | # github_token: ${{ secrets.ZAPPEDIT_TOKEN }} 41 | # publish_dir: dist/zapddit 42 | # enable_jekyll: true 43 | -------------------------------------------------------------------------------- /src/app/component/peopleifollow/peopleifollow.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | 🕸 {{'People I follow'|translate}} 4 |

5 |

{{'You can see the people you follow here'|translate}} 👇

6 |
7 | 8 |
9 |
10 |
11 | Loading people you follow... 12 |
13 |
14 | 15 |
16 |
17 |
18 | {{'Not following anyone yet. May be try following some people?'|translate}}
{{'like'|translate}} @vivganes?
19 |
20 |
21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 | -------------------------------------------------------------------------------- /src/app/pipe/short-number.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'shortNumber' 5 | }) 6 | export class ShortNumberPipe implements PipeTransform { 7 | 8 | transform(number: number, args?: any): any { 9 | if (isNaN(number)) return null; // will only work value is a number 10 | if (number === null) return null; 11 | if (number === 0) return 0; 12 | let abs = Math.abs(number); 13 | const rounder = Math.pow(10, 1); 14 | const isNegative = number < 0; // will also work for Negetive numbers 15 | let key = ''; 16 | 17 | const powers = [ 18 | {key: 'Q', value: Math.pow(10, 15)}, 19 | {key: 'T', value: Math.pow(10, 12)}, 20 | {key: 'B', value: Math.pow(10, 9)}, 21 | {key: 'M', value: Math.pow(10, 6)}, 22 | {key: 'K', value: 1000} 23 | ]; 24 | 25 | for (let i = 0; i < powers.length; i++) { 26 | let reduced = abs / powers[i].value; 27 | reduced = Math.round(reduced * rounder) / rounder; 28 | if (reduced >= 1) { 29 | abs = reduced; 30 | key = powers[i].key; 31 | break; 32 | } 33 | } 34 | return (isNegative ? '-' : '') + abs + key; 35 | } 36 | } -------------------------------------------------------------------------------- /src/app/filter/HashTagFilter.spec.ts: -------------------------------------------------------------------------------- 1 | import { NDKEvent } from "@nostr-dev-kit/ndk"; 2 | import { HashTagFilter } from "./HashTagFilter"; 3 | 4 | describe('HashTagFilter', () => { 5 | describe('notHashTags', () => { 6 | const filter = HashTagFilter.notHashTags(['test', 'mute']); 7 | 8 | it('should return true for events without hashtags', () => { 9 | const event = { content: 'This is a normal post' } as NDKEvent; 10 | expect(filter(event, 0, [])).toBe(true); 11 | }); 12 | 13 | it('should return false for events with muted hashtags', () => { 14 | const event = { content: 'This post contains a #test hashtag' } as NDKEvent; 15 | expect(filter(event, 0, [])).toBe(false); 16 | }); 17 | 18 | it('should return true for events with non-muted hashtags', () => { 19 | const event = { content: 'This post contains a #allowed hashtag' } as NDKEvent; 20 | expect(filter(event, 0, [])).toBe(true); 21 | }); 22 | 23 | it('should be case-insensitive', () => { 24 | const event = { content: 'This post contains a #TEST hashtag' } as NDKEvent; 25 | expect(filter(event, 0, [])).toBe(false); 26 | }); 27 | 28 | it('should return true for events with undefined content', () => { 29 | const event = { } as NDKEvent; 30 | expect(filter(event, 0, [])).toBe(true); 31 | }); 32 | }); 33 | }); -------------------------------------------------------------------------------- /src/snow.scss: -------------------------------------------------------------------------------- 1 | $snowflakes: 40; 2 | 3 | %snowflake { 4 | display: block; 5 | color: #fff; 6 | position: absolute; 7 | top: -1em; 8 | 9 | &:before { 10 | display: block; 11 | content: '❄'; 12 | } 13 | } 14 | 15 | @mixin snowflake($nth) { 16 | snowflake { 17 | @extend %snowflake; 18 | &:nth-of-type(#{$nth}) { 19 | font-size: 2vmin + random(); 20 | left: 100vw * random(); 21 | will-change: transform, top; 22 | $delay: 3s + 4 * random(); 23 | $fall-duration: 10s + 4 * random(); 24 | $shake-duration: 15s + 4 * random(); 25 | animation: snowflake-fall $fall-duration linear $delay infinite normal, 26 | snowflake-shake $shake-duration ease-in-out $delay infinite alternate, 27 | snowflake-wind-w $fall-duration linear $delay infinite normal; 28 | } 29 | } 30 | } 31 | 32 | html, body { 33 | height: 100%; 34 | overflow: hidden; 35 | 36 | } 37 | 38 | @keyframes snowflake-fall { 39 | 0% {top: -1em} 40 | 100% {top: 100vh} 41 | } 42 | 43 | @keyframes snowflake-shake { 44 | 0% {transform: translateX(0)} 45 | 33% {transform: translateX(-10vh)} 46 | 100% {transform: translateX(10vh)} 47 | } 48 | 49 | @keyframes snowflake-wind-w { 50 | 0% {transform: translateX(0)} 51 | 100% {transform: translateX(20vw)} 52 | } 53 | 54 | snowflakes { 55 | @for $i from 1 through $snowflakes { 56 | @include snowflake($i); 57 | } 58 | } -------------------------------------------------------------------------------- /src/app/component/quoted-event/quoted-event.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { treeFeaturesFactory } from '@clr/angular/data/tree-view/tree-features.service'; 3 | import { NDKEvent } from '@nostr-dev-kit/ndk'; 4 | import { NdkproviderService } from 'src/app/service/ndkprovider.service'; 5 | import { LoginUtil } from 'src/app/util/LoginUtil'; 6 | 7 | @Component({ 8 | selector: 'app-quoted-event', 9 | templateUrl: './quoted-event.component.html', 10 | styleUrls: ['./quoted-event.component.scss'] 11 | }) 12 | export class QuotedEventComponent { 13 | 14 | event?:NDKEvent|null 15 | loading:boolean = true; 16 | 17 | @Input() 18 | id?:string 19 | 20 | constructor(private ndkProvider: NdkproviderService){ 21 | } 22 | 23 | ngOnInit(){ 24 | if(this.id){ 25 | this.getNote(this.id); 26 | } 27 | } 28 | 29 | async getNote(id:string){ 30 | this.loading = true; 31 | let bech32Id = ''; 32 | if(id.startsWith('nevent')){ 33 | let decodedValue = LoginUtil.decodeTLV(id); 34 | bech32Id = (decodedValue[0].value as string); 35 | } else { 36 | bech32Id = LoginUtil.bech32ToHex(id); 37 | } 38 | let fetchedEvent = await this.ndkProvider.fetchEventFromId(bech32Id) 39 | if(fetchedEvent?.kind == 1){ 40 | this.event = fetchedEvent; 41 | } 42 | this.loading = false; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/util/IntlHashtagLinkifyPlugin.ts: -------------------------------------------------------------------------------- 1 | import { State, createTokenClass } from 'linkifyjs'; 2 | 3 | // Create a new token that class that the parser emits when it finds a hashtag 4 | const HashtagToken = createTokenClass('hashtag', { isLink: true }); 5 | 6 | /** 7 | * @type {import('linkifyjs').Plugin} 8 | */ 9 | export default function hashtag({ scanner, parser }:any) { 10 | // Various tokens that may compose a hashtag 11 | const { POUND, UNDERSCORE, SYM } = scanner.tokens; 12 | const { alpha, numeric, alphanumeric, emoji } = scanner.tokens.groups; 13 | 14 | // Take or create a transition from start to the '#' sign (non-accepting) 15 | // Take transition from '#' to any text token to yield valid hashtag state 16 | // Account for leading underscore (non-accepting unless followed by alpha) 17 | const Hash = parser.start.tt(POUND); 18 | const HashPrefix = Hash.tt(UNDERSCORE); 19 | const Hashtag = new State(HashtagToken); 20 | 21 | Hash.ta(numeric, HashPrefix); 22 | Hash.ta(alpha, Hashtag); 23 | Hash.ta(emoji, Hashtag); 24 | HashPrefix.ta(alpha, Hashtag); 25 | HashPrefix.ta(emoji, Hashtag); 26 | HashPrefix.ta(numeric, HashPrefix); 27 | HashPrefix.tt(UNDERSCORE, HashPrefix); 28 | Hashtag.ta(alphanumeric, Hashtag); 29 | Hashtag.ta(emoji, Hashtag); 30 | Hashtag.ta(SYM,Hashtag); 31 | Hashtag.tt(UNDERSCORE, Hashtag); // Trailing underscore is okay 32 | Hashtag.tt(SYM, Hashtag); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/app/component/user-mention/user-mention.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, Input } from '@angular/core'; 2 | import { NdkproviderService } from 'src/app/service/ndkprovider.service'; 3 | 4 | @Component({ 5 | selector: 'app-user-mention', 6 | templateUrl: './user-mention.component.html', 7 | styleUrls: ['./user-mention.component.scss'] 8 | }) 9 | export class UserMentionComponent { 10 | 11 | @Input() 12 | npub:string|undefined; 13 | 14 | @Input() 15 | hexKey:string|undefined; 16 | 17 | displayName?:string; 18 | href?:string; 19 | 20 | ndkProvider:NdkproviderService; 21 | 22 | constructor(ndkProvider:NdkproviderService){ 23 | this.ndkProvider = ndkProvider 24 | } 25 | 26 | ngOnInit(){ 27 | if(this.npub){ 28 | this.ndkProvider.getProfileFromNpub(this.npub).then((profile => { 29 | if(profile){ 30 | this.displayName = (profile.displayName? profile.displayName : (profile.name?profile.name : this.npub)); 31 | this.href="https://snort.social/p/"+this.npub 32 | } 33 | })) 34 | } else if(this.hexKey){ 35 | this.ndkProvider.getProfileFromHex(this.hexKey).then((profile => { 36 | if(profile){ 37 | this.displayName = (profile.displayName? profile.displayName : (profile.name?profile.name : this.hexKey)); 38 | this.href="https://snort.social/p/"+this.hexKey 39 | } 40 | })) 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/app/component/user-pic-and-name/user-pic-and-name.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{'Profile picture'|translate}} 8 | temp avatar 11 | 12 | {{ (user?.profile?.displayName) ? 13 | user?.profile?.displayName:((user?.profile?.name)? user?.profile?.name : user?.pubkey|abbreviateId) }} 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/app/page/login-page/login-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NdkproviderService } from 'src/app/service/ndkprovider.service'; 3 | import { Router } from '@angular/router'; 4 | 5 | 6 | @Component({ 7 | selector: 'app-login-page', 8 | templateUrl: './login-page.component.html', 9 | styleUrls: ['./login-page.component.scss'] 10 | }) 11 | export class LoginPageComponent { 12 | 13 | ndkProvider:NdkproviderService; 14 | notices:string[] = []; 15 | 16 | constructor(ndkProvider:NdkproviderService, private router:Router){ 17 | this.ndkProvider = ndkProvider; 18 | } 19 | 20 | ngOnInit(){ 21 | this.ndkProvider.notice$.subscribe((message)=>{ 22 | this.notices.push(message); 23 | }) 24 | 25 | this.ndkProvider.loginCompleteEmitter.subscribe((complete: boolean) => { 26 | this.router.navigateByUrl('/feed'); 27 | }) 28 | } 29 | 30 | attemptLoginWithNip07(){ 31 | this.ndkProvider.attemptLoginWithNip07(); 32 | } 33 | 34 | attemptLoginWithPrivateOrPubKey(){ 35 | let enteredKey = (document.getElementById('pkey')).value; 36 | this.ndkProvider.attemptLoginUsingPrivateOrPubKey(enteredKey); 37 | } 38 | 39 | attemptGenerateNewCredential(){ 40 | this.ndkProvider.setAsNewToNostr(); 41 | this.ndkProvider.attemptToGenerateNewCredential(); 42 | } 43 | 44 | attemptLoginWithoutAccount(){ 45 | this.ndkProvider.attemptToTryUnauthenticated(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/service/relay.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NdkproviderService } from './ndkprovider.service'; 3 | import { ZappeditdbService } from './zappeditdb.service'; 4 | import { Relay } from '../model'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class RelayService { 10 | ndkProvider: NdkproviderService; 11 | dbProvider: ZappeditdbService; 12 | 13 | constructor(ndkProviderService: NdkproviderService, zappeditdbService: ZappeditdbService) { 14 | this.ndkProvider = ndkProviderService; 15 | this.dbProvider = zappeditdbService; 16 | } 17 | 18 | async getRelays(): Promise { 19 | const relays: Relay[] = await this.dbProvider.subscribedRelays.toArray(); 20 | // console.log(relays); 21 | return relays; 22 | } 23 | 24 | async removeRelay(relay: string) { 25 | let relayList: Relay[] = []; 26 | await this.dbProvider.subscribedRelays.delete(relay) 27 | relayList = await this.getRelays(); 28 | this.ndkProvider.updateRelays(relayList); 29 | } 30 | 31 | async addRelay(relay: string, read: boolean, write: boolean) { 32 | let relayList: Relay[] = []; 33 | const relayName: string = relay.replace('wss://', '').replace('/', ''); 34 | const newRelay: Relay = new Relay(relayName, relay, read, write); 35 | await this.ndkProvider.addRelayToDB(this.dbProvider.subscribedRelays, newRelay); 36 | relayList = await this.getRelays(); 37 | this.ndkProvider.updateRelays(relayList); 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ⚡zapddit - A reddit-style nostr client 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/app/util/Constants.ts: -------------------------------------------------------------------------------- 1 | export class Constants { 2 | public static readonly DARKTHEME = 'darkTheme'; 3 | public static readonly PRIVATEKEY = 'privateKey'; 4 | public static readonly NPUB = 'npub'; 5 | public static readonly LOGGEDINUSINGPUBKEY = 'loggedInUsingPubKey'; 6 | public static readonly TRYING_ZAPDDIT = 'tryingZapddit'; 7 | public static readonly FOLLOWEDTOPICS = 'followedTopics'; 8 | public static readonly FOLLOWEDCOMMUNITIES = 'followedCommunities'; 9 | public static readonly DOWNZAPRECIPIENTS = 'downzapRecipients'; 10 | public static readonly DEFAULTSATSFORZAPS = 'defaultSatsForZaps'; 11 | public static readonly MUTEDTOPICS = 'mutedTopics'; 12 | public static readonly TOPICS_CLEARED = 'topicsCleared'; 13 | public static readonly COMMUNITIES_CLEARED = 'communitiesCleared'; 14 | public static readonly RECIPIENTS_CLEARED = 'recipientsCleared'; 15 | public static readonly SHOWMEDIA = 'loadMediaContentOnlyForPeopleIFollow'; 16 | public static readonly LANGUAGE = 'language'; 17 | public static readonly ZAP_SPLIT_PERCENTAGE = 'zapSplitPercentage'; 18 | public static readonly ZAP_SPLIT_CONFIG = 'zapSplitConfig'; 19 | public static readonly FOLLOWERS_FROM_RELAY = 'loadingFollowersFromRelay'; 20 | public static readonly HIDE_NONZAP_REACTIONS = 'hideNonZapReactions'; 21 | public static readonly SHOW_UNAPPROVED = 'showUnapproved'; 22 | public static readonly RELAYSUBS = 'subscribedRelays'; 23 | public static readonly DEFAULT_FEED_IS_COMMUNITY = 'defaultFeedIsCommunity'; 24 | public static readonly INDEX_ROUTE:string='/' 25 | public static readonly FEED_ROUTE:string='/feed' 26 | public static readonly ZAPDDIT_PUBKEY = '748bfa87c437b294164d1784b324a0d2e9495c8268e0044e3c7796a3b158c9d8'; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/observable-service/community-event.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NdkproviderService } from '../service/ndkprovider.service'; 3 | import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'; 4 | import { BehaviorSubject, Observable } from 'rxjs'; 5 | import {List} from 'immutable'; 6 | 7 | interface CommunityWithRecentActivityTime{ 8 | communityId:string, 9 | recentActivityAt?:number 10 | } 11 | 12 | @Injectable({ 13 | providedIn: 'root' 14 | }) 15 | export class CommunityEventService { 16 | private _communityEvents: BehaviorSubject> = new BehaviorSubject>(List([])); 17 | public readonly communityEvents: Observable> = this._communityEvents.asObservable(); 18 | 19 | 20 | constructor(private ndk:NdkproviderService) { 21 | this.startSubscription(); 22 | } 23 | 24 | startSubscription() { 25 | const subscription: NDKSubscription = this.ndk.createSubscriptionForCommunityEvents(); 26 | subscription.addListener('event',(event: NDKEvent)=> { 27 | const communityId = event.getMatchingTags('a')[0][1]; 28 | const activityAt = event.created_at; 29 | const packedObj:CommunityWithRecentActivityTime = { 30 | communityId: communityId, 31 | recentActivityAt: activityAt 32 | } 33 | let existingArray = this._communityEvents.getValue(); 34 | existingArray = existingArray.filter((item)=> item.communityId !== communityId); 35 | existingArray = existingArray.push(packedObj); 36 | existingArray = existingArray.sortBy(item => -item.recentActivityAt!); 37 | this._communityEvents.next(existingArray.slice(0,10)); 38 | }); 39 | } 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/assets/icons/logo-without-text-zapddit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/contact-card/contact-card.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {'Profile picture'|translate} 5 | temp avatar 10 |
11 |
12 | {{ contact?.displayName }} @{{ contact?.name }} 13 | 14 | 17 |
18 | {{ contact?.npub}} 19 | {{ contact?.about }} 20 |
21 |
22 |
23 | Loading profile... 24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /src/app/buffer/EventBuffer.spec.ts: -------------------------------------------------------------------------------- 1 | import { EventBuffer } from "./EventBuffer" 2 | 3 | describe('Event Buffer',() => { 4 | 5 | it('should return items for 2 and 4',()=>{ 6 | const eventBuffer = new EventBuffer() 7 | eventBuffer.events = [0,1,2,3,4,5,6,7]; 8 | expect(eventBuffer.getItemsWithIndexes(2,4)).toEqual([2,3,4]); 9 | }) 10 | 11 | it('should return items for 2 and 7',()=>{ 12 | const eventBuffer = new EventBuffer() 13 | eventBuffer.events = [0,1,2,3,4,5,6,7]; 14 | expect(eventBuffer.getItemsWithIndexes(2,7)).toEqual([2,3,4,5,6,7]); 15 | }) 16 | 17 | it('should return items for 2 and 8',()=>{ 18 | const eventBuffer = new EventBuffer() 19 | eventBuffer.events = [0,1,2,3,4,5,6,7]; 20 | expect(eventBuffer.getItemsWithIndexes(2,8)).toEqual([2,3,4,5,6,7]); 21 | }) 22 | 23 | it('should return items for 2 and 10',()=>{ 24 | const eventBuffer = new EventBuffer() 25 | eventBuffer.events = [0,1,2,3,4,5,6,7]; 26 | expect(eventBuffer.getItemsWithIndexes(2,10)).toEqual([2,3,4,5,6,7]); 27 | }) 28 | 29 | it('should return [] for 8 and 9',()=>{ 30 | const eventBuffer = new EventBuffer() 31 | eventBuffer.events = [0,1,2,3,4,5,6,7]; 32 | expect(eventBuffer.getItemsWithIndexes(8,9)).toEqual(null); 33 | }) 34 | 35 | it('should return null for 9 and 10',()=>{ 36 | const eventBuffer = new EventBuffer() 37 | eventBuffer.events = [0,1,2,3,4,5,6,7]; 38 | expect(eventBuffer.getItemsWithIndexes(9,10)).toEqual(null); 39 | }) 40 | 41 | it('should return null for 3 and 5',()=>{ 42 | const eventBuffer = new EventBuffer() 43 | eventBuffer.events = undefined; 44 | expect(eventBuffer.getItemsWithIndexes(3,5)).toEqual([]); 45 | }) 46 | }) -------------------------------------------------------------------------------- /src/assets/loader.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/util/Translators.ts: -------------------------------------------------------------------------------- 1 | // Tamil Translators 2 | const VIVEK = 'facdaf1ce758bdf04cdf1a1fa32a3564a608d4abc2481a286ffc178f86953ef0'; 3 | const KAMAL = 'c094ccc6b3b3f969bc37a900a01a3c69c692532fedab909aaa6cce8bf5f06a03'; 4 | 5 | // Persian Translators 6 | const L = 'c07a2ea48b6753d11ad29d622925cb48bab48a8f38e954e85aec46953a0752a2'; 7 | 8 | // Finnish Translators 9 | const PETRI = 'e417ee3d910253993ae0ce6b41d4a24609970f132958d75b2d9b634d60a3cc08'; 10 | 11 | // Thai Translators 12 | const VAZ = '58f5a23008ba5a8730a435f68f18da0b10ce538c6e2aa5a1b7812076304d59f7'; 13 | 14 | // French Translators 15 | const SOLO = '31da2214d943b6db29848bfe7e3cf8ec0380014414f06cddb0eeacc9af2508e2'; 16 | 17 | // Swahili Translators 18 | const TURISMO = '06830f6cb5925bd82cca59bda848f0056666dff046c5382963a997a234da40c5'; 19 | 20 | // German translators 21 | const FLOBSTR = '4b6147b45bbde75c2ce4cf93444675945c47f41ffd51e3446287bbd56ba668d2'; 22 | 23 | // Spanish translators 24 | const LOU = '18e3af1edeecb70542eb7e000cf5c43ea0d6d3b79ebb64c8e2c98b341d42e5df'; 25 | 26 | export const Translators: TranslatorsForLanguage[] = [ 27 | { 28 | language: 'de', 29 | translatorPubKeys: [FLOBSTR] 30 | }, 31 | { 32 | language: 'es', 33 | translatorPubKeys: [LOU] 34 | }, 35 | { 36 | language: 'fa', 37 | translatorPubKeys: [L] 38 | }, 39 | { 40 | language: 'fi', 41 | translatorPubKeys: [PETRI] 42 | }, 43 | { 44 | language: 'fr', 45 | translatorPubKeys: [SOLO] 46 | }, 47 | { 48 | language: 'sw', 49 | translatorPubKeys: [TURISMO] 50 | }, 51 | { 52 | language: 'ta', 53 | translatorPubKeys: [VIVEK,KAMAL] 54 | }, 55 | { 56 | language: 'th', 57 | translatorPubKeys: [VAZ] 58 | } 59 | ] 60 | 61 | 62 | export interface TranslatorsForLanguage{ 63 | language: string; 64 | translatorPubKeys: string[] 65 | } 66 | -------------------------------------------------------------------------------- /src/app/component/profile/profile.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep.nav>.active { 2 | font-size: large; 3 | font-weight: bold; 4 | } 5 | 6 | .center-text{ 7 | text-align: center; 8 | } 9 | 10 | .profile-field{ 11 | padding: 1em 12 | } 13 | 14 | input, 15 | textarea{ 16 | width: 80% !important; 17 | margin-left: 10rem; 18 | } 19 | 20 | 21 | 22 | .pb-2{ 23 | padding-bottom: 2em; 24 | } 25 | 26 | .pt-2{ 27 | padding-top:2em; 28 | } 29 | 30 | .avatar-image { 31 | max-height: 100px; 32 | clip-path: circle(); 33 | margin-right: 3px; 34 | } 35 | 36 | .middle{ 37 | vertical-align: middle; 38 | position: relative; 39 | } 40 | 41 | .text-vmiddle{ 42 | top:50%; 43 | position:absolute 44 | } 45 | 46 | @media only screen and (max-width: 995px) { 47 | .text-vmiddle{ 48 | top:0%; 49 | position:absolute; 50 | } 51 | 52 | .middle{ 53 | vertical-align: middle; 54 | position: relative; 55 | margin-bottom: 10px; 56 | } 57 | 58 | .avatar-image { 59 | max-height: 100px; 60 | border-radius: 50%; 61 | margin-right: 3px; 62 | margin-top:12px; 63 | } 64 | 65 | input, 66 | textarea{ 67 | width: 80% !important; 68 | margin-left: 0rem; 69 | } 70 | } 71 | 72 | .mb-20{ 73 | margin-bottom: 20px; 74 | } 75 | 76 | .large-text{ 77 | font-size: medium; 78 | } 79 | 80 | .mr-5{ 81 | margin-right: 5px; 82 | } 83 | .fright{ 84 | float: right; 85 | } 86 | 87 | .about{ 88 | width: 80%; 89 | height: 4rem; 90 | } 91 | 92 | .ml-4{ 93 | margin-left: 4px; 94 | } 95 | 96 | .profile-pic { 97 | position: relative; 98 | display: inline-block; 99 | } 100 | 101 | .profile-pic:hover .edit { 102 | display: block; 103 | } 104 | 105 | .edit { 106 | padding-top: 7px; 107 | position: absolute; 108 | right: 60%; 109 | top: -12%; 110 | display: none; 111 | } 112 | 113 | .edit a { 114 | color: #000; 115 | } 116 | -------------------------------------------------------------------------------- /src/app/util/ZapdditRouteReuseStrategy.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from "@angular/router"; 2 | /* 3 | shouldDetach – This determines if the route the user is leaving should save the component state. 4 | store – This stores the detached route if the method above returns true. 5 | 6 | shouldAttach – This determines if the route the user is navigating to should load the component state. 7 | retrieve – This loads the detached route if the method above returns true. 8 | */ 9 | 10 | export class ZapdditRouteReuseStrategy implements RouteReuseStrategy { 11 | handlers = new Map(); 12 | 13 | shouldDetach(route: ActivatedRouteSnapshot): boolean { 14 | return (route.data['reuseComponent'] ?? false); 15 | } 16 | 17 | store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { 18 | const key = this.getRouteKey(route); 19 | if(!this.handlers.has(key)){ 20 | this.handlers.set(key, handle); 21 | } 22 | } 23 | 24 | shouldAttach(route: ActivatedRouteSnapshot): boolean { 25 | return this.handlers.has(this.getRouteKey(route)); 26 | } 27 | 28 | private getRouteKey(route: ActivatedRouteSnapshot): string { 29 | return route.pathFromRoot.filter(u => u.url).map(u => u.url).join('/'); 30 | } 31 | 32 | retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { 33 | var result = this.handlers.get(this.getRouteKey(route)); 34 | return result??null; 35 | } 36 | 37 | shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { 38 | if (future.routeConfig === curr.routeConfig) { 39 | return future.data['reuseComponent']; 40 | } else { 41 | return false; 42 | } 43 | } 44 | 45 | public clearSavedHandle(key: string): void { 46 | const handle = this.handlers.get(key); 47 | if (handle) { 48 | //(handle as any).componentRef.destroy(); 49 | this.handlers.delete(key); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/testing/CommonTestingModule.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { DynamicHooksModule, HookParserEntry } from 'ngx-dynamic-hooks'; 6 | import { HashtagComponent } from '../component/hashtag/hashtag.component'; 7 | import { UserMentionComponent } from '../component/user-mention/user-mention.component'; 8 | import { QuotedEventComponent } from '../component/quoted-event/quoted-event.component'; 9 | import { BrowserModule } from '@angular/platform-browser'; 10 | import { MentionModule } from 'angular-mentions'; 11 | import { AppRoutingModule } from '../app-routing.module'; 12 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 13 | import { ClarityModule } from '@clr/angular'; 14 | import { DatePipe } from '@angular/common'; 15 | 16 | const componentParsers: Array = [ 17 | { component: HashtagComponent }, 18 | { component: UserMentionComponent }, 19 | { component: QuotedEventComponent }, 20 | // ... 21 | ]; 22 | 23 | @NgModule({ 24 | declarations: [], 25 | }) 26 | export class CommonTestingModule { 27 | public static setUpTestBed = (TestingComponent: any) => { 28 | beforeEach(() => { 29 | TestBed.configureTestingModule({ 30 | imports: [ 31 | ReactiveFormsModule, 32 | FormsModule, 33 | RouterTestingModule, 34 | DynamicHooksModule.forRoot({ 35 | globalParsers: componentParsers, 36 | }), 37 | BrowserModule, 38 | FormsModule, 39 | MentionModule, 40 | AppRoutingModule, 41 | BrowserAnimationsModule, 42 | ClarityModule, 43 | ], 44 | providers: [DatePipe], 45 | declarations: [TestingComponent], 46 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 47 | }); 48 | }); 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zapddit 2 | 3 | A reddit-style nostr client 4 | 5 | [![Crowdin](https://badges.crowdin.net/zapddit/localized.svg)](https://crowdin.com/project/zapddit) 6 | 7 | * [reddit Vs. zapddit](#reddit-vs-zapddit) 8 | * [Screenshot](#screenshot) 9 | 10 | ## reddit Vs. zapddit 11 | 12 | **reddit** | **zapddit** 13 | ---------- | ------------ 14 | Users search for sub-reddits like r/nostr, r/tifu, etc. and follow them. | Users search for hashtags like #coffeechain, #foodstr, etc. and follow them. 15 | Home feed is filled with posts from their subscribed sub-reddits. |User's feed is filled with recent notes mentioning the followed hashtags, in the reverse-chronological order. 16 | Users express appreciation through upvotes. | Users express appreciation through Upzaps. Sats in the Upzaps are sent to the **author of the upzapped note**. 17 | Users express disagreement through downvotes | Users express disagreement through Downzaps. Sats in the Downzaps are sent to the **Downzap recipient**, who is a nostr user chosen by the down-zapper 18 | Users see a tally of upvotes vs downvotes for each post | Users see a tally of upzap sats and downzap sats for each note 19 | 20 | ## Screenshot 21 | ![Screenshot](https://github.com/vivganes/zappedit/assets/2035886/72e75686-cc5f-4982-ad0d-76444db228bb) 22 | 23 | ## Features Checklist 24 | - [x] NIP-07 login 25 | - [x] Search hashtag 26 | - [x] Follow, Unfollow hashtag 27 | - [x] Feed with notes 28 | - [x] Hashtags linking 29 | - [x] Image display in note 30 | - [x] Set a downzap recipient 31 | - [x] Upzaps and Downzaps using QR Code 32 | - [x] Open user profile/post in snort 33 | - [x] Show user mentions in notes 34 | - [x] Load images only for notes by 'followed' users 35 | - [x] Switch light mode/dark mode 36 | - [x] Mute Hashtags 37 | - [x] Show replies/comments in a nice tree 38 | - [x] Show note mentions 39 | - [x] Compose notes 40 | - [x] Compose replies to notes 41 | - [x] Show videos 42 | - [x] Non-zap upvotes and downvotes 43 | - [x] Relay list 44 | - [x] Browse without logging in 45 | - [ ] LN wallet connect 46 | - [ ] Anything else? Feel free to suggest! 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/app/component/user-pic-and-name/user-pic-and-name.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core'; 2 | import { NDKUser, NDKUserProfile } from '@nostr-dev-kit/ndk'; 3 | import { NdkproviderService } from 'src/app/service/ndkprovider.service'; 4 | import { ObjectCacheService } from 'src/app/service/object-cache.service'; 5 | 6 | @Component({ 7 | selector: 'app-user-pic-and-name', 8 | templateUrl: './user-pic-and-name.component.html', 9 | styleUrls: ['./user-pic-and-name.component.scss'] 10 | }) 11 | export class UserPicAndNameComponent { 12 | 13 | @Input() 14 | hexKey?:string; 15 | 16 | @Input() 17 | npub?:string; 18 | 19 | @Input() 20 | user?:NDKUser; 21 | 22 | @Input() 23 | onlyPic:boolean = false; 24 | 25 | @Input() 26 | showClickableDeleteIcon:boolean = false; 27 | 28 | @Output() 29 | deleteIconClicked:EventEmitter = new EventEmitter(); 30 | 31 | constructor(private ndkProvider:NdkproviderService, private objectCache:ObjectCacheService){ 32 | 33 | } 34 | 35 | ngOnChanges(changes:SimpleChanges){ 36 | if(changes['npub'].currentValue !== changes['npub'].previousValue){ 37 | this.populateUserUsingNpub(); 38 | } 39 | } 40 | 41 | ngOnInit(){ 42 | if(!this.user){ 43 | if(this.hexKey){ 44 | this.populateUser(); 45 | }else if(this.npub){ 46 | this.populateUserUsingNpub(); 47 | } 48 | } 49 | else{ 50 | if(!this.user.profile){ 51 | const user = this.user; 52 | this.user.fetchProfile().then((event)=>{ 53 | this.objectCache.addUser(user); 54 | }) 55 | } 56 | } 57 | } 58 | 59 | async populateUserUsingNpub(){ 60 | this.user = await this.ndkProvider.getNdkUserFromNpub(this.npub!); 61 | } 62 | 63 | async populateUser(){ 64 | this.user = await this.ndkProvider.getNdkUserFromHex(this.hexKey!); 65 | } 66 | 67 | openAuthorInSnort(){ 68 | if(this.user?.npub) 69 | window.open('https://snort.social/p/'+this.user?.npub,'_blank') 70 | } 71 | 72 | onDeleteIconClicked(evt:any){ 73 | this.deleteIconClicked.emit(this.user) 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouteReuseStrategy, RouterModule, provideRouter, type Routes, withInMemoryScrolling, withDebugTracing } from '@angular/router'; 3 | import { EventFeedComponent } from './component/event-feed/event-feed.component'; 4 | import { SinglePostComponent } from './component/single-post/single-post.component'; 5 | import { PreferencesPageComponent } from './component/preferences-page/preferences-page.component'; 6 | import { PeopleIFollowComponent } from './component/peopleifollow/peopleifollow.component'; 7 | import { ProfileComponent } from './component/profile/profile.component'; 8 | import { CommunityListComponent } from './page/community-list/community-list.component'; 9 | import { LoginPageComponent } from './page/login-page/login-page.component'; 10 | import { ZapdditRouteReuseStrategy } from './util/ZapdditRouteReuseStrategy'; 11 | import { HomeFeedComponent } from './component/event-feed/home-feed.component'; 12 | 13 | const routes: Routes = [ 14 | { path: 't/:topic', component: EventFeedComponent }, 15 | { path: 'login', component: LoginPageComponent }, 16 | { path: 'profile', component: ProfileComponent }, 17 | { path: 'n/:noteid', component: SinglePostComponent }, 18 | { path: '', redirectTo: '/feed', pathMatch: 'full' }, // redirect to `first-component` 19 | { path: 'preferences', component: PreferencesPageComponent}, 20 | { path: 'feed', component: HomeFeedComponent, data:{ reuseComponent:true} }, 21 | { path: 'communities/discover', component: CommunityListComponent }, 22 | { path: 'communities/joined', component: CommunityListComponent }, 23 | { path: 'communities/own', component: CommunityListComponent }, 24 | { path: 'communities/moderating', component: CommunityListComponent }, 25 | { path: 'communities/recently-active', component: CommunityListComponent }, 26 | { path: 'n/:communityName/:creatorHexKey', component: EventFeedComponent }, 27 | { path: '**', component: EventFeedComponent }, 28 | ]; 29 | 30 | @NgModule({ 31 | imports: [RouterModule.forRoot(routes,{scrollPositionRestoration: 'enabled'})], 32 | exports: [RouterModule], 33 | providers:[{provide:RouteReuseStrategy, useClass:ZapdditRouteReuseStrategy}] 34 | }) 35 | export class AppRoutingModule {} 36 | -------------------------------------------------------------------------------- /src/app/service/community-page.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ObjectCacheService } from './object-cache.service'; 3 | import { Community } from '../model/community'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class CommunityPageService { 9 | constructor(private objectCache: ObjectCacheService) {} 10 | 11 | fastForward(lastRow: Community, idProp: string) { 12 | let fastForwardComplete = false; 13 | return (item: Community) => { 14 | if (fastForwardComplete) return true; 15 | //@ts-ignore 16 | if (item[idProp] === lastRow[idProp]) { 17 | fastForwardComplete = true; 18 | } 19 | return false; 20 | }; 21 | } 22 | 23 | async getPageWithNumber(pageNumber:number) { 24 | const PAGE_SIZE = 10; 25 | 26 | // A helper function we will use below. 27 | // It will prevent the same results to be returned again for next page. 28 | 29 | // Criterion filter in plain JS: 30 | const criterionFunction = (community: Community) => community.id !== undefined; // Just an example... 31 | 32 | // 33 | // Query First Page 34 | // 35 | let page = await this.objectCache.communities 36 | .orderBy('created_at') // Utilize index for sorting 37 | .filter(criterionFunction) 38 | .limit(PAGE_SIZE) 39 | .toArray(); 40 | if(pageNumber === 0){ 41 | return page; 42 | } 43 | 44 | // 45 | // Page 2 46 | // 47 | // "page" variable is an array of results from last request: 48 | if (page.length < PAGE_SIZE) return; // Done 49 | let lastEntry = page[page.length - 1]; 50 | page = await this.objectCache.communities 51 | // Use index to fast forward as much as possible 52 | // This line is what makes the paging optimized 53 | .where('created_at') 54 | .belowOrEqual(lastEntry.created_at) // makes it sorted by lastName 55 | 56 | // Use helper function to fast forward to the exact last result: 57 | .filter(this.fastForward(lastEntry, 'id')) 58 | .limit(PAGE_SIZE) 59 | .toArray(); 60 | if (pageNumber === 1){ 61 | return page; 62 | } 63 | // 64 | // Page N 65 | // 66 | if (page.length < PAGE_SIZE) return; // Done 67 | lastEntry = page[page.length - 1]; 68 | page = await this.objectCache.communities 69 | .where('created_at') 70 | .belowOrEqual(lastEntry.created_at) 71 | .filter(this.fastForward(lastEntry, 'id')) 72 | .limit(PAGE_SIZE) 73 | .toArray(); 74 | return page; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zapddit", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "build:prod": "ng build --configuration production --base-href=/", 11 | "test:headless": "ng test --watch=false --browsers=ChromeHeadless", 12 | "i18n:extract": "ngx-translate-extract --input ./src --output ./src/assets/i18n/en.json --key-as-default-value --replace --format json" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "^16.2.0", 17 | "@angular/common": "^16.2.0", 18 | "@angular/compiler": "^16.2.0", 19 | "@angular/core": "^16.2.0", 20 | "@angular/forms": "^16.2.0", 21 | "@angular/platform-browser": "^16.2.0", 22 | "@angular/platform-browser-dynamic": "^16.2.0", 23 | "@angular/router": "^16.2.0", 24 | "@angular/service-worker": "^16.2.0", 25 | "@cds/core": "6.9.1", 26 | "@clr/angular": "16.2.0", 27 | "@clr/ui": "16.2.0", 28 | "@fractalsoftware/random-avatar-generator": "^1.0.11", 29 | "@getalby/bitcoin-connect": "^3.2.2", 30 | "@ngx-translate/core": "^15.0.0", 31 | "@ngx-translate/http-loader": "^8.0.0", 32 | "@noble/curves": "^1.1.0", 33 | "@noble/hashes": "^1.2.0", 34 | "@noble/secp256k1": "^1.7.0", 35 | "@nostr-dev-kit/ndk": "2.7.1", 36 | "@nostr-dev-kit/ndk-cache-dexie": "2.3.1", 37 | "@types/linkifyjs": "^2.1.4", 38 | "angular-animations": "^0.11.0", 39 | "angular-mentions": "^1.5.0", 40 | "angular-toastify": "^1.0.6", 41 | "bech32": "^2.0.0", 42 | "dexie": "^3.2.3", 43 | "light-bolt11-decoder": "^3.0.0", 44 | "linkify-html": "^4.1.1", 45 | "linkify-plugin-hashtag": "^4.1.1", 46 | "linkifyjs": "^4.1.1", 47 | "moment": "^2.29.4", 48 | "ng-in-viewport": "^16.0.0", 49 | "ngx-dynamic-hooks": "^2.1.0", 50 | "qr-code-styling": "^1.6.0-rc.1", 51 | "rxjs": "~7.8.0", 52 | "tslib": "^2.3.0", 53 | "zone.js": "~0.13.0" 54 | }, 55 | "devDependencies": { 56 | "@angular-devkit/build-angular": "^16.2.6", 57 | "@angular/cli": "^16.2.6", 58 | "@angular/compiler-cli": "^16.2.0", 59 | "@types/debug": "^4.1.7", 60 | "@types/jasmine": "~4.3.0", 61 | "@vendure/ngx-translate-extract": "^8.2.3", 62 | "jasmine-core": "~4.6.0", 63 | "karma": "~6.4.0", 64 | "karma-chrome-launcher": "~3.2.0", 65 | "karma-coverage": "~2.2.0", 66 | "karma-jasmine": "~5.1.0", 67 | "karma-jasmine-html-reporter": "~2.1.0", 68 | "prettier": "^2.8.8", 69 | "typescript": "~5.1.3" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/component/note-composer/note-composer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | 15 |
16 |
17 | 18 | {{ item.name }} 19 |   20 | {{ item.displayName }} 21 |   22 | {{ item.npub }} 23 | 24 |
25 | 32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 |

Create new note

40 |
41 |
42 | 43 |
44 | -------------------------------------------------------------------------------- /src/app/util/ZapSplitUtil.ts: -------------------------------------------------------------------------------- 1 | import { ToastService } from "angular-toastify"; 2 | import ZapSplitConfig, { HexKeyWithSplitPercentage } from "../model/ZapSplitConfig"; 3 | import { Constants } from "./Constants"; 4 | import { Translators } from "./Translators"; 5 | 6 | export class ZapSplitUtil{ 7 | 8 | static prepareDefaultZapSplitConfig():ZapSplitConfig{ 9 | const language = ZapSplitUtil.getCurrentLanguage(); 10 | const translatorZapSplitEntries: HexKeyWithSplitPercentage[] = ZapSplitUtil.getZapSplitEntriesForTranslators(language); 11 | return { 12 | developers: [{ 13 | hexKey: Constants.ZAPDDIT_PUBKEY, 14 | percentage: 0.5 15 | }], 16 | translators: translatorZapSplitEntries 17 | } 18 | } 19 | 20 | static validateZapSplitConfig(zapSplitConfig:ZapSplitConfig):ZapSplitConfig{ 21 | //validate if percentage totals greater than 100% 22 | let devPercentageTotal = 0; 23 | zapSplitConfig.developers.forEach((d) => { 24 | devPercentageTotal += d.percentage; 25 | }); 26 | 27 | let translatorTotal = 0; 28 | zapSplitConfig.translators.forEach((t) => { 29 | translatorTotal += t.percentage 30 | }); 31 | 32 | if(devPercentageTotal + translatorTotal > 100){ 33 | new ToastService().warn('Zap Split configuration was invalid. We have reset this to default. You can change this from Preferences.'); 34 | const resetConfig = ZapSplitUtil.prepareDefaultZapSplitConfig(); 35 | localStorage.setItem(Constants.ZAP_SPLIT_CONFIG, JSON.stringify(resetConfig)) 36 | return resetConfig; 37 | } 38 | return zapSplitConfig; 39 | 40 | } 41 | 42 | static getZapSplitEntriesForTranslators(language: string) { 43 | const translatorPubKeys = this.findTranslators(language); 44 | const defaultZapSplitForTranslators = 0.5; 45 | const zapSplitPerTranslator = defaultZapSplitForTranslators / translatorPubKeys.length; 46 | const translatorZapSplitEntries: HexKeyWithSplitPercentage[] = translatorPubKeys.map((t) => { 47 | return { hexKey: t, percentage: zapSplitPerTranslator }; 48 | }); 49 | return translatorZapSplitEntries; 50 | } 51 | 52 | static findTranslators(language:string){ 53 | const translatorsForLanguage = Translators.filter(entry => entry.language === language) 54 | if(translatorsForLanguage && translatorsForLanguage.length>0){ 55 | return translatorsForLanguage[0].translatorPubKeys; 56 | } 57 | return []; 58 | } 59 | 60 | static getCurrentLanguage(){ 61 | var language = localStorage.getItem(Constants.LANGUAGE); 62 | if (language != null || language != undefined || language != '') { 63 | const currentLanguage = language as string; 64 | return currentLanguage || 'en'; 65 | } else { 66 | return 'en'; 67 | } 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/app/service/object-cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import NDK, { NDKUser, NDKUserProfile } from '@nostr-dev-kit/ndk'; 3 | import Dexie, { Table } from 'dexie'; 4 | import { User } from '../model/user'; 5 | import { NdkproviderService } from './ndkprovider.service'; 6 | import { Community } from '../model/community'; 7 | 8 | const DATASTORE = { 9 | users: "hexPubKey, name, displayName, nip05, npub", 10 | communities: "id,name,displayName,creatorHexKey,description,created_at" 11 | }; 12 | 13 | const VERSION = 1; 14 | 15 | 16 | @Injectable({ 17 | providedIn: 'root' 18 | }) 19 | export class ObjectCacheService extends Dexie { 20 | 21 | //default TTL in seconds 22 | defaultTTL:number = 60*60; //1 hour 23 | users!:Table; 24 | communities!:Table; 25 | 26 | constructor() { 27 | super('object-cache') 28 | this.version(1).stores({ 29 | users: DATASTORE.users 30 | }); 31 | this.version(Math.round(this.verno + 2)).stores(DATASTORE); 32 | } 33 | 34 | 35 | 36 | async addUser(item:NDKUser){ 37 | this.users.put({ 38 | hexPubKey: item.pubkey, 39 | name: item.profile?.name!, 40 | displayName: item.profile?.displayName!, 41 | nip05: item.profile?.nip05!, 42 | npub: item.npub, 43 | pictureUrl: item.profile?.image!, 44 | about: item.profile?.about!, 45 | lud06: item.profile?.lud06!, 46 | lud16: item.profile?.lud16 47 | }, item.pubkey) 48 | 49 | const self = this; 50 | setTimeout(() => { 51 | console.log("Deleting item with key "+ item.pubkey) 52 | self.deleteUserWithHexKey(item.pubkey) 53 | }, this.defaultTTL*1000); 54 | } 55 | 56 | 57 | deleteUserWithHexKey(hexPubKey:string){ 58 | this.users.delete(hexPubKey) 59 | } 60 | 61 | async fetchUserWithNpub(npub:string, ndkToInject:NDK):Promise{ 62 | const user:User|undefined = await this.users.where('npub').equals(npub).first(); 63 | if(user){ 64 | console.log("hit") 65 | const profile:NDKUserProfile = { 66 | name: user.name, 67 | displayName: user.displayName, 68 | image: user.pictureUrl, 69 | bio:user.about, 70 | about:user.about, 71 | nip05:user.nip05 72 | } 73 | const ndkUser = new NDKUser({npub: user.npub}); 74 | ndkUser.ndk = ndkToInject; 75 | ndkUser.profile = profile; 76 | return ndkUser; 77 | } 78 | console.log("miss") 79 | return undefined; 80 | } 81 | 82 | async fetchUserWithHexKey(hexPubKey:string):Promise{ 83 | const user:User|undefined = await this.users.get(hexPubKey); 84 | if(user){ 85 | console.log("hit") 86 | const profile:NDKUserProfile = { 87 | name: user.name, 88 | displayName: user.displayName, 89 | image: user.pictureUrl, 90 | bio:user.about, 91 | about:user.about, 92 | nip05:user.nip05 93 | } 94 | const ndkUser = new NDKUser({npub: user.npub}); 95 | ndkUser.profile = profile; 96 | return ndkUser; 97 | } 98 | console.log("miss") 99 | return undefined; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/app/component/contact-card/contact-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, EventEmitter, Output, ChangeDetectorRef } from '@angular/core'; 2 | import { User } from '../../model/user'; 3 | import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk'; 4 | import { ZappeditdbService } from 'src/app/service/zappeditdb.service'; 5 | import { NdkproviderService } from '../../service/ndkprovider.service'; 6 | import { Util } from 'src/app/util/Util'; 7 | import { LoginUtil } from 'src/app/util/LoginUtil'; 8 | 9 | @Component({ 10 | selector: 'app-contact-card', 11 | templateUrl: './contact-card.component.html', 12 | styleUrls: ['./contact-card.component.scss'] 13 | }) 14 | export class ContactCardComponent implements OnInit { 15 | 16 | @Input() 17 | contact:User | undefined; 18 | event: NDKEvent | undefined; 19 | unfollowed:boolean = false; 20 | eventInProgress:boolean = false; 21 | @Input() 22 | showFollow:boolean = true; 23 | @Output() 24 | contactListUpdated = new EventEmitter(); 25 | contactLoading: boolean = false; 26 | isNIP05Verified:boolean = false; 27 | 28 | constructor(private ndkProvider: NdkproviderService, private dbService:ZappeditdbService, private changeDetector:ChangeDetectorRef) { 29 | } 30 | 31 | ngOnInit(): void { 32 | this.populateContactDetails(); 33 | } 34 | 35 | populateContactDetails(){ 36 | this.contactLoading = true; 37 | this.ndkProvider.getProfileFromHex(this.contact?.hexPubKey!).then(async (userProfile)=> { 38 | console.log("Got for "+ this.contact?.hexPubKey + " - "+ userProfile?.displayName) 39 | if(userProfile){ 40 | this.contact = this.convertToUser(userProfile, this.contact?.hexPubKey!); 41 | } 42 | 43 | var nip05Address = userProfile?.nip05; 44 | if(nip05Address) 45 | this.isNIP05Verified = await this.ndkProvider.checkIfNIP05Verified(nip05Address, this.contact?.hexPubKey!); 46 | 47 | this.contactLoading = false; 48 | this.changeDetector.detectChanges(); 49 | }) 50 | } 51 | 52 | convertToUser(profile : NDKUserProfile, hexPubKey: string){ 53 | let user:User = { 54 | hexPubKey: hexPubKey, 55 | displayName: profile.displayName, 56 | name: profile.name, 57 | about: profile.about, 58 | pictureUrl: profile.image, 59 | npub: LoginUtil.hexToBech32('npub',hexPubKey) 60 | } 61 | return user; 62 | } 63 | 64 | getImageUrls(): RegExpMatchArray | null | undefined { 65 | const urlRegex = /https:.*?\.(?:png|jpg|svg|gif|jpeg|webp)/gi; 66 | const imgArray = this.event?.content.match(urlRegex); 67 | return imgArray; 68 | } 69 | 70 | openInSnort(item?:User){ 71 | window.open('https://snort.social/p/'+item?.npub,'_blank'); 72 | } 73 | 74 | unFollow(authorHexPubKey?:string){ 75 | this.eventInProgress = true; 76 | this.ndkProvider.followUnfollowContact(authorHexPubKey!, false).then(async res => { 77 | this.dbService.peopleIFollow.where('hexPubKey').equalsIgnoreCase(authorHexPubKey!.toString()).delete().then(()=>{ 78 | console.log("Contact removed"); 79 | this.eventInProgress = false; 80 | this.unfollowed = true; 81 | this.contactListUpdated.emit(true); 82 | }) 83 | }, err=>{ 84 | console.log(err); 85 | this.eventInProgress = false; 86 | this.unfollowed = false; 87 | }).catch((error) => { 88 | this.eventInProgress = false; 89 | this.unfollowed = false; 90 | console.error ("Error from unfollow: " + error); 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "zapddit": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/zapddit", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": [ 24 | "zone.js" 25 | ], 26 | "tsConfig": "tsconfig.app.json", 27 | "inlineStyleLanguage": "scss", 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets", 31 | "src/manifest.webmanifest", 32 | "src/.well-known" 33 | ], 34 | "styles": [ 35 | "node_modules/@clr/ui/clr-ui.min.css", "src/styles.scss" 36 | ], 37 | "scripts": [], 38 | "serviceWorker": true, 39 | "ngswConfigPath": "ngsw-config.json" 40 | }, 41 | "configurations": { 42 | "production": { 43 | "budgets": [ 44 | { 45 | "type": "initial", 46 | "maximumWarning": "3.9mb", 47 | "maximumError": "4mb" 48 | }, 49 | { 50 | "type": "anyComponentStyle", 51 | "maximumWarning": "2kb", 52 | "maximumError": "4kb" 53 | } 54 | ], 55 | "outputHashing": "all" 56 | }, 57 | "development": { 58 | "buildOptimizer": false, 59 | "optimization": false, 60 | "vendorChunk": true, 61 | "extractLicenses": false, 62 | "sourceMap": true, 63 | "namedChunks": true 64 | } 65 | }, 66 | "defaultConfiguration": "production" 67 | }, 68 | "serve": { 69 | "builder": "@angular-devkit/build-angular:dev-server", 70 | "configurations": { 71 | "production": { 72 | "browserTarget": "zapddit:build:production" 73 | }, 74 | "development": { 75 | "browserTarget": "zapddit:build:development" 76 | } 77 | }, 78 | "defaultConfiguration": "development" 79 | }, 80 | "extract-i18n": { 81 | "builder": "@angular-devkit/build-angular:extract-i18n", 82 | "options": { 83 | "browserTarget": "zapddit:build" 84 | } 85 | }, 86 | "test": { 87 | "builder": "@angular-devkit/build-angular:karma", 88 | "options": { 89 | "polyfills": [ 90 | "zone.js", 91 | "zone.js/testing" 92 | ], 93 | "tsConfig": "tsconfig.spec.json", 94 | "inlineStyleLanguage": "scss", 95 | "assets": [ 96 | "src/favicon.ico", 97 | "src/assets", 98 | "src/manifest.webmanifest", 99 | "src/.well-known" 100 | ], 101 | "styles": [ 102 | "node_modules/@clr/ui/clr-ui.min.css", "src/styles.scss" 103 | ], 104 | "scripts": [] 105 | } 106 | } 107 | } 108 | } 109 | }, 110 | "cli": { 111 | "analytics": false 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/app/component/create-community/create-community.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter, OnInit, ViewChild } from '@angular/core'; 2 | import { NdkproviderService } from '../../service/ndkprovider.service'; 3 | import { NgForm } from '@angular/forms'; 4 | import { BehaviorSubject, debounceTime } from 'rxjs'; 5 | import { Community } from '../../model/community'; 6 | import { CommunityService } from '../../service/community.service'; 7 | import { LoginUtil } from 'src/app/util/LoginUtil'; 8 | import { NDKUser } from '@nostr-dev-kit/ndk'; 9 | 10 | @Component({ 11 | selector: 'app-create-community', 12 | templateUrl: './create-community.component.html', 13 | styleUrls: ['./create-community.component.scss'] 14 | }) 15 | export class CreateCommunityComponent implements OnInit{ 16 | 17 | @Input() 18 | show:boolean = false; 19 | 20 | @Output() 21 | onClose = new EventEmitter(); 22 | 23 | @Output() 24 | onEditComplete = new EventEmitter(); 25 | 26 | @ViewChild("newCommunityForm") 27 | newCommunityForm:NgForm; 28 | createDisabled:boolean=true; 29 | currentModSuggestionNpub:string; 30 | @Input() 31 | editMode:boolean = false; 32 | 33 | displayNameChange = new BehaviorSubject(''); 34 | @Input() 35 | newCommunity:Community; 36 | 37 | constructor(private ndkproviderService:NdkproviderService, private communityService:CommunityService){ 38 | } 39 | 40 | ngOnInit(): void { 41 | if(this.ndkproviderService.currentUser){ 42 | if(!this.editMode){ 43 | this.newCommunity = { 44 | creatorHexKey: this.ndkproviderService.currentUser.pubkey, 45 | moderatorHexKeys:[this.ndkproviderService.currentUser.pubkey] 46 | } 47 | } else { 48 | if(this.newCommunity && !this.newCommunity.moderatorHexKeys){ 49 | this.newCommunity.moderatorHexKeys = [this.newCommunity.creatorHexKey!] 50 | } else if (this.newCommunity?.moderatorHexKeys?.indexOf(this.newCommunity.creatorHexKey!) === -1){ 51 | this.newCommunity.moderatorHexKeys.push(this.newCommunity.creatorHexKey!); 52 | } 53 | } 54 | } 55 | if(this.editMode){ 56 | this.createDisabled = false; 57 | } 58 | 59 | if(!this.editMode){ 60 | this.displayNameChange 61 | .asObservable() 62 | .pipe(debounceTime(250)) 63 | .subscribe(value => { 64 | if (value) 65 | { 66 | this.createDisabled=false; 67 | this.sanitizeDisplayName(); 68 | } 69 | else 70 | this.createDisabled=true; 71 | }); 72 | } 73 | } 74 | 75 | onChange($event:any){ 76 | this.onClose.emit(false); 77 | } 78 | 79 | displayNameUpdate($event:any){ 80 | this.displayNameChange.next($event); 81 | } 82 | 83 | async onCreate(){ 84 | await this.communityService.createCommunity(this.newCommunity); 85 | if(this.editMode){ 86 | this.onEditComplete.emit(this.newCommunity) 87 | } 88 | this.onClose.emit(true); 89 | this.newCommunity={}; 90 | } 91 | 92 | addModerator(evt:any){ 93 | evt.preventDefault(); 94 | if(this.currentModSuggestionNpub.trim() !== ''){ 95 | const hexKey:string = LoginUtil.bech32ToHex(this.currentModSuggestionNpub.trim()); 96 | if(this.newCommunity.moderatorHexKeys?.indexOf(hexKey) === -1){ 97 | this.newCommunity.moderatorHexKeys?.push(hexKey) 98 | } 99 | } 100 | } 101 | 102 | deleteModerator(user:NDKUser){ 103 | this.newCommunity.moderatorHexKeys = this.newCommunity.moderatorHexKeys?.filter((key) => key!==user.pubkey); 104 | } 105 | 106 | sanitizeDisplayName(){ 107 | this.newCommunity.name = this.newCommunity.displayName!.replace(/[:\s]/g,''); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/app/component/community-card/community-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, EventEmitter, HostListener, Input, Output, ViewChild } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Community } from 'src/app/model/community'; 4 | import { CommunityService } from 'src/app/service/community.service'; 5 | import { NdkproviderService } from 'src/app/service/ndkprovider.service'; 6 | import { Constants } from 'src/app/util/Constants'; 7 | 8 | @Component({ 9 | selector: 'app-community-card', 10 | templateUrl: './community-card.component.html', 11 | styleUrls: ['./community-card.component.scss'] 12 | }) 13 | export class CommunityCardComponent { 14 | 15 | @Input() 16 | community?:Community; 17 | 18 | @Input() 19 | id: string; 20 | 21 | @Input() 22 | lastActive: number; 23 | 24 | followingNow:boolean = false; 25 | showEditCommunity:boolean = false; 26 | currentUserHexKey?:string; 27 | joinOrLeaveInProgress:boolean = false; 28 | ndkProvider:NdkproviderService; 29 | showHeaderImage:boolean = false; 30 | 31 | @Output() 32 | onLeave:EventEmitter = new EventEmitter(); 33 | 34 | constructor(ndkProvider:NdkproviderService, private router:Router, private communityService: CommunityService){ 35 | this.ndkProvider = ndkProvider; 36 | } 37 | 38 | ngOnInit(){ 39 | this.currentUserHexKey = this.ndkProvider.currentUser?.pubkey; 40 | if(!this.community){ 41 | var self = this; 42 | this.ndkProvider.getCommunityDetails(this.id).then((community)=>{ 43 | self.community = community; 44 | self.setIsFollowed(); 45 | 46 | if(!self.community?.creatorProfile){ 47 | self.fetchCreatorProfile(); 48 | } 49 | }) 50 | }else{ // required to set the join/leave based on the list of communities that are joined 51 | this.setIsFollowed(); 52 | } 53 | } 54 | 55 | onShowInViewPort({ target, visible }: { target: Element; visible: boolean }): void{ 56 | if(visible){ 57 | this.showHeaderImage=true; 58 | this.fetchFollowers(); 59 | } 60 | } 61 | 62 | setIsFollowed(){ 63 | if(this.ndkProvider.appData.followedCommunities !== ''){ 64 | const followedArr:string[] = this.ndkProvider.appData.followedCommunities.split(',') 65 | if(followedArr.findIndex(id => this.community?.id === id)>-1){ 66 | this.followingNow = true; 67 | } 68 | } 69 | } 70 | 71 | async fetchCreatorProfile(){ 72 | const profile = await this.ndkProvider.getProfileFromHex(this.community?.creatorHexKey!); 73 | if(this.community!== undefined && profile){ 74 | this.community.creatorProfile = profile; 75 | } 76 | } 77 | 78 | async fetchFollowers(){ 79 | if(this.community!== undefined && !this.community?.followersHexKeys){ 80 | const followers = await this.ndkProvider.fetchFollowersForCommunity(this.community?.id!) 81 | this.community.followersHexKeys = followers; 82 | } 83 | } 84 | 85 | popupClosed(evt:any){ 86 | this.showEditCommunity = false; 87 | } 88 | 89 | editFinished(edited:Community){ 90 | this.community = edited; 91 | this.showEditCommunity = false; 92 | } 93 | 94 | openCommunityPage(){ 95 | this.router.navigateByUrl('n/'+this.community?.name+'/'+this.community?.creatorHexKey) 96 | } 97 | 98 | openCommunityCreatorInSnort(){ 99 | window.open('https://snort.social/p/'+this.community?.creatorHexKey!,'_blank') 100 | } 101 | 102 | async joinCommunity(){ 103 | this.joinOrLeaveInProgress = true; 104 | await this.communityService.joinCommunity(this.community!); 105 | this.followingNow = true; 106 | await this.communityService.clearCommunitiesFromAppData(); 107 | this.joinOrLeaveInProgress = false; 108 | } 109 | 110 | async leaveCommunity(){ 111 | this.joinOrLeaveInProgress = true; 112 | await this.communityService.leaveCommunity(this.community!); 113 | 114 | this.followingNow = false; 115 | this.onLeave.emit(this.community); 116 | 117 | await this.communityService.clearCommunitiesFromAppData(); 118 | this.joinOrLeaveInProgress = false; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/app/component/zapdialog/zapdialog.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 43 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 75 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/app/component/create-community/create-community.component.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/app/component/community-card/community-card.component.html: -------------------------------------------------------------------------------- 1 |
4 |
5 |
6 | {{'Community image'|translate}} 9 | temp community avatar 12 |
13 |
14 | n/{{community.name}} of 15 | Profile picture 18 | temp avatar 21 | {{community.creatorProfile?.displayName ? community.creatorProfile?.displayName : 22 | (community.creatorProfile?.name ? community.creatorProfile?.name : (community.creatorHexKey | abbreviateId))}} 23 |
24 |
25 |
26 | 27 | {{'Active'|translate}} {{lastActive | formatTimestamp }} 28 | 29 |
30 |
31 | {{(community.followersHexKeys.length >0)? (community.followersHexKeys.length | shortNumber) : 0}} {{community.followersHexKeys.length===1 ? ('person'|translate):('people'|translate)}} {{(community.followersHexKeys.length >0)? ('including'|translate):''}} 32 | 33 | 34 | 35 |
36 |
37 | 38 |
39 |
40 | 46 |
47 |
48 |
49 |
50 |
51 |
52 | {{'n/'+id.split(':')[2]}} 53 |
54 |
55 | 56 | {{'Active'|translate}} {{lastActive | formatTimestamp }} 57 | 58 |
59 |
60 |
61 | Loading community... 62 |
63 |
64 | 65 |
66 | 67 | -------------------------------------------------------------------------------- /src/app/component/onboarding-wizard/onboarding-wizard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { NdkproviderService } from 'src/app/service/ndkprovider.service'; 3 | import { Constants } from 'src/app/util/Constants'; 4 | import { Clipboard } from '@angular/cdk/clipboard'; 5 | import { LoginUtil } from 'src/app/util/LoginUtil'; 6 | import { TopicService } from '../../service/topic.service'; 7 | 8 | @Component({ 9 | selector: 'app-onboarding-wizard', 10 | templateUrl: './onboarding-wizard.component.html', 11 | styleUrls: ['./onboarding-wizard.component.scss'] 12 | }) 13 | export class OnboardingWizardComponent { 14 | 15 | @Input() 16 | open:boolean = false; 17 | 18 | @Output() 19 | openChange:EventEmitter = new EventEmitter(); 20 | dailyDoseSeeker:boolean = false; 21 | newsReader:boolean = false; 22 | memeEnjoyer:boolean =false; 23 | questionAnswerer: boolean = false; 24 | btcMaxi:boolean = false; 25 | foodLover:boolean = false; 26 | plebLover:boolean = false; 27 | clientsFan:boolean = false; 28 | cryptoRepeller:boolean = false; 29 | weirdStuffRepeller:boolean = false; 30 | nonSporter:boolean = false; 31 | twitterHater:boolean = false; 32 | suggestedTopics: string[] = []; 33 | muteList: string[] = []; 34 | newUserDisplayName?:string; 35 | ndkProvider: NdkproviderService; 36 | 37 | constructor(ndkProvider:NdkproviderService, private clipboard:Clipboard, private topicService:TopicService){ 38 | this.ndkProvider = ndkProvider; 39 | } 40 | 41 | updateTopics(){ 42 | let newTopics = [] 43 | if(this.dailyDoseSeeker){ 44 | newTopics.push('coffeechain','chaichain','nostr','nostrich','nostriches'); 45 | } 46 | if(this.newsReader){ 47 | newTopics.push('news','worldnews'); 48 | } 49 | if(this.questionAnswerer){ 50 | newTopics.push('asknostr'); 51 | } 52 | if(this.memeEnjoyer){ 53 | newTopics.push('memechain','meme','memes') 54 | } 55 | if(this.btcMaxi){ 56 | newTopics.push('bitcoin','shitcoin') 57 | } 58 | if(this.foodLover){ 59 | newTopics.push('foodstr','foodchain') 60 | } 61 | if(this.plebLover){ 62 | newTopics.push('plebchain','zapathon') 63 | } 64 | if(this.clientsFan){ 65 | newTopics.push('amethyst','damus','iris','coracle','zapddit','client','nostrclient') 66 | } 67 | this.suggestedTopics = newTopics 68 | } 69 | 70 | updateMuteList(){ 71 | let mutedTopics = [] 72 | if(this.cryptoRepeller){ 73 | mutedTopics.push('bitcoin','shitcoin','crypto') 74 | } 75 | if(this.weirdStuffRepeller){ 76 | mutedTopics.push('footstr','feetstr','boobstr','nudestr') 77 | } 78 | if(this.nonSporter){ 79 | mutedTopics.push('baseball','sports','football','fifa','cricket','nfl','sport') 80 | } 81 | if(this.twitterHater){ 82 | mutedTopics.push('twitter','birdapp','elon','elonmusk') 83 | } 84 | this.muteList = mutedTopics; 85 | } 86 | 87 | async acceptChoices(){ 88 | if(this.ndkProvider.isTryingZapddit){ 89 | this.ndkProvider.appData.followedTopics = this.suggestedTopics.join(','); 90 | this.ndkProvider.followedTopicsEmitter.emit(this.ndkProvider.appData.followedTopics); 91 | this.ndkProvider.appData.mutedTopics = this.muteList.join(","); 92 | this.ndkProvider.mutedTopicsEmitter.emit(this.ndkProvider.appData.mutedTopics); 93 | localStorage.setItem(Constants.FOLLOWEDTOPICS,this.ndkProvider.appData.followedTopics); 94 | localStorage.setItem(Constants.MUTEDTOPICS,this.ndkProvider.appData.mutedTopics); 95 | this.ndkProvider.setNotNewToNostr(); 96 | return; 97 | } 98 | let alreadyFollowedTopics:string[] = [] 99 | let followedTopicsToBePublished = [] 100 | let alreadyFollowedTopicsString = this.ndkProvider.appData.followedTopics; 101 | if(alreadyFollowedTopicsString === ''){ 102 | alreadyFollowedTopics = []; 103 | } else { 104 | alreadyFollowedTopics = alreadyFollowedTopicsString.split(","); 105 | } 106 | followedTopicsToBePublished = [...alreadyFollowedTopics,...this.suggestedTopics]; 107 | followedTopicsToBePublished = [...new Set(followedTopicsToBePublished)]; 108 | 109 | let alreadyMutedTopics:string[] = [] 110 | let mutedTopicsToBePublished = [] 111 | let alreadyMutedTopicsString = this.ndkProvider.appData.mutedTopics; 112 | if(alreadyMutedTopicsString === ''){ 113 | alreadyMutedTopics = []; 114 | } else { 115 | alreadyMutedTopics = alreadyMutedTopicsString.split(","); 116 | } 117 | mutedTopicsToBePublished = [...alreadyMutedTopics,...this.muteList]; 118 | mutedTopicsToBePublished = [...new Set(mutedTopicsToBePublished)]; 119 | if(this.newUserDisplayName){ 120 | //create new profile event and send it across 121 | await this.ndkProvider.createNewUserOnNostr(this.newUserDisplayName); 122 | } 123 | localStorage.setItem(Constants.FOLLOWEDTOPICS,followedTopicsToBePublished.join(',')); 124 | localStorage.setItem(Constants.MUTEDTOPICS,mutedTopicsToBePublished.join(',')); 125 | 126 | this.ndkProvider.publishAppData(undefined, undefined, mutedTopicsToBePublished.join(',')); 127 | await this.topicService.followTopicsInteroperableList(followedTopicsToBePublished); 128 | this.ndkProvider.setNotNewToNostr(); 129 | } 130 | 131 | copyPrivateKey(){ 132 | const privateKeyHex = localStorage.getItem('privateKey') 133 | this.clipboard.copy(LoginUtil.hexToBech32("nsec",privateKeyHex!)) 134 | } 135 | 136 | markWizardClosed(){ 137 | this.open = false; 138 | this.openChange.emit(this.open) 139 | } 140 | 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { ClipboardModule } from '@angular/cdk/clipboard'; 2 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule, isDevMode } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { LayoutModule } from '@angular/cdk/layout'; 5 | import "@getalby/bitcoin-connect"; 6 | // import ngx-translate and the http loader 7 | import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; 8 | import {TranslateHttpLoader} from '@ngx-translate/http-loader'; 9 | import {HttpClient, HttpClientModule} from '@angular/common/http'; 10 | import { ToastService, AngularToastifyModule } from 'angular-toastify'; 11 | 12 | 13 | import { AppRoutingModule } from './app-routing.module'; 14 | import { AppComponent } from './app.component'; 15 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 16 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 17 | import { ClarityModule } from '@clr/angular'; 18 | import { MentionModule } from 'angular-mentions'; 19 | 20 | import { UserprofileComponent } from './component/userprofile/userprofile.component'; 21 | import { EventFeedComponent } from './component/event-feed/event-feed.component'; 22 | import { EventCardComponent } from './component/event-card/event-card.component'; 23 | import { SinglePostComponent } from './component/single-post/single-post.component'; 24 | import { ShortNumberPipe } from './pipe/short-number.pipe'; 25 | import { HashtagComponent } from './component/hashtag/hashtag.component'; 26 | import { DynamicHooksModule, HookParserEntry } from 'ngx-dynamic-hooks'; 27 | import { PreferencesPageComponent } from './component/preferences-page/preferences-page.component'; 28 | import { ServiceWorkerModule } from '@angular/service-worker'; 29 | import { LoginPageComponent } from './page/login-page/login-page.component'; 30 | import { PeopleIFollowComponent } from './component/peopleifollow/peopleifollow.component'; 31 | import { UserMentionComponent } from './component/user-mention/user-mention.component'; 32 | import { QuotedEventComponent } from './component/quoted-event/quoted-event.component'; 33 | import { NoteComposerComponent } from './component/note-composer/note-composer.component'; 34 | import { ContactCardComponent } from './component/contact-card/contact-card.component'; 35 | import { formatTimestampPipe } from './pipe/formatTimeStamp.pipe'; 36 | import { OnboardingWizardComponent } from './component/onboarding-wizard/onboarding-wizard.component'; 37 | import { ImageLoaderDirective } from './directive/ImageLoaderDirective'; 38 | import { ProfileComponent } from './component/profile/profile.component'; 39 | import { AbbreviateIdPipe } from './pipe/abbreviateId.pipe'; 40 | import { TopicComponent } from './component/topic/topic.component'; 41 | import { CommunityCardComponent } from './component/community-card/community-card.component'; 42 | import { ZapdialogComponent } from './component/zapdialog/zapdialog.component'; 43 | import { CommunityListComponent } from './page/community-list/community-list.component'; 44 | import { HashTagFilter } from './filter/HashTagFilter'; 45 | import { UserPicAndNameComponent } from './component/user-pic-and-name/user-pic-and-name.component'; 46 | import { CreateCommunityComponent } from './component/create-community/create-community.component'; 47 | import { NewLineToBrPipe } from './pipe/newLineToBr.pipe'; 48 | import { InViewportModule } from 'ng-in-viewport'; 49 | import { HomeFeedComponent } from './component/event-feed/home-feed.component'; 50 | import { ClickStopPropagation } from './directive/ClickStopPropagation'; 51 | const componentParsers: Array = [ 52 | {component: HashtagComponent}, 53 | {component: UserMentionComponent}, 54 | {component: QuotedEventComponent} 55 | // ... 56 | ]; 57 | 58 | @NgModule({ 59 | declarations: [ 60 | AppComponent, 61 | UserprofileComponent, 62 | EventFeedComponent, 63 | HomeFeedComponent, 64 | EventCardComponent, 65 | SinglePostComponent, 66 | ShortNumberPipe, 67 | formatTimestampPipe, 68 | AbbreviateIdPipe, 69 | NewLineToBrPipe, 70 | HashtagComponent, 71 | PreferencesPageComponent, 72 | LoginPageComponent, 73 | PeopleIFollowComponent, 74 | UserMentionComponent, 75 | QuotedEventComponent, 76 | NoteComposerComponent, 77 | ContactCardComponent, 78 | OnboardingWizardComponent, 79 | ImageLoaderDirective, 80 | ClickStopPropagation, 81 | ProfileComponent, 82 | TopicComponent, 83 | CommunityListComponent, 84 | CommunityCardComponent, 85 | ZapdialogComponent, 86 | UserPicAndNameComponent, 87 | CreateCommunityComponent 88 | ], 89 | imports: [DynamicHooksModule.forRoot({ 90 | globalParsers: componentParsers 91 | }), 92 | BrowserModule, 93 | // ngx-translate and the loader module 94 | HttpClientModule, 95 | TranslateModule.forRoot({ 96 | loader: { 97 | provide: TranslateLoader, 98 | useFactory: HttpLoaderFactory, 99 | deps: [HttpClient] 100 | } 101 | }), 102 | InViewportModule, LayoutModule, FormsModule, ReactiveFormsModule, MentionModule, AppRoutingModule, BrowserAnimationsModule, AngularToastifyModule, ClarityModule, ClipboardModule, ServiceWorkerModule.register('ngsw-worker.js', { 103 | enabled: !isDevMode(), 104 | // Register the ServiceWorker as soon as the application is stable 105 | // or after 30 seconds (whichever comes first). 106 | registrationStrategy: 'registerWhenStable:30000' 107 | })], 108 | providers: [ToastService], 109 | bootstrap: [AppComponent], 110 | schemas:[CUSTOM_ELEMENTS_SCHEMA] 111 | }) 112 | export class AppModule {} 113 | 114 | // required for AOT compilation 115 | export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { 116 | return new TranslateHttpLoader(http); 117 | } 118 | -------------------------------------------------------------------------------- /src/app/page/login-page/login-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Logging in... 6 |
7 |
8 |
9 |
    10 |
  • {{notice}}
  • 11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 |

{{'Secure Login (Recommended)'|translate}} 🔐

22 |
23 |

Nostr Extension

24 |
25 |

26 | {{'Secure login to zapddit requires a Nostr extension to work. Don\'t have a Nostr extension?'|translate}} {{'Refer'|translate}} 27 | {{'this page'|translate}} 28 |

29 |

32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 |

{{'Alternate Login'|translate}} 🗝️

40 |
41 |

Private/Public Key Login

42 |
43 |
44 | 45 | 46 | 47 | Key starting with nsec(for read+write) or npub(read-only) 48 | 49 | 61 |
62 |

64 |
65 |
66 |
67 |
68 |
69 |
70 | 71 |
72 |
73 |

{{'New to nostr?'|translate}}' 🙋‍♂️

74 |
75 |

Create a new user

76 |
77 |

78 | Zapddit runs on top of nostr. Click the button below to create a new user on the nostr ecosystem. 79 |

80 |

81 | You can use the same account in all the nostr clients. 82 |

83 |

86 |
87 |
88 |
89 |
90 | 91 |
92 |
93 |

{{'Try zapddit'|translate}}⚡

94 |
95 |

Not ready to use a nostr account yet?

96 |
97 |

98 | You can have a clean lurker experience using this option. 99 |

100 |

101 | All your settings are saved within browser's local storage and cleared when you logout. 102 |

103 |

106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |

We are opensource!

115 |

116 | {{'Find zapddit on'|translate}} {{'Github'|translate}}. {{'Found bugs? Raise them'|translate}} here. 117 |

118 |
119 |
120 |
121 |
122 |
-------------------------------------------------------------------------------- /src/app/page/community-list/community-list.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { Observable, liveQuery } from 'dexie'; 5 | import { Community } from 'src/app/model/community'; 6 | import { NdkproviderService } from 'src/app/service/ndkprovider.service'; 7 | import { CommunityService } from '../../service/community.service'; 8 | import { ObjectCacheService } from 'src/app/service/object-cache.service'; 9 | import { CommunityEventService } from 'src/app/observable-service/community-event.service'; 10 | 11 | const BUFFER_REFILL_PAGE_SIZE = 100; 12 | const BUFFER_READ_PAGE_SIZE = 20; 13 | 14 | @Component({ 15 | selector: 'app-community-list', 16 | templateUrl: './community-list.component.html', 17 | styleUrls: ['./community-list.component.scss'] 18 | }) 19 | export class CommunityListComponent { 20 | 21 | searchTerm:string=''; 22 | communities?:Community[]; 23 | allCommunities?:Observable; 24 | isCachingCommunities:boolean; 25 | until: number | undefined = Date.now(); 26 | limit: number | undefined = BUFFER_REFILL_PAGE_SIZE; 27 | loadingEvents: boolean = false; 28 | loadingNextEvents: boolean = false; 29 | reachedEndOfFeed : boolean = false; 30 | nextEvents: Community[] | undefined; 31 | isLoggedInUsingPubKey:boolean = false; 32 | showOnlyOwnedCommunities: boolean = false; 33 | showOnlyJoinedCommunities: boolean = false; 34 | showOnlyModeratingCommunities:boolean = false; 35 | showRecentlyActiveCommunities:boolean = false; 36 | searchResults?:Community[]; 37 | showCreateCommunity:boolean = false; 38 | communityEventService:CommunityEventService; 39 | searchInProgress:boolean = false; 40 | discoverCommunities:boolean = false; 41 | 42 | constructor(public ndkProvider:NdkproviderService, private router:Router, 43 | private communityService:CommunityService, private objectCache:ObjectCacheService, communityEventService:CommunityEventService){ 44 | this.communityEventService = communityEventService; 45 | } 46 | 47 | 48 | ngOnInit(){ 49 | this.ndkProvider.loadingCommunitiesEmitter$.subscribe((loading)=>{ 50 | if(loading){ 51 | console.log("Received event for true") 52 | this.isCachingCommunities = true 53 | } else { 54 | console.log("Received event for false") 55 | this.isCachingCommunities = false 56 | } 57 | }) 58 | 59 | 60 | const url = this.router.url; 61 | if(url.indexOf('/own')>-1){ 62 | this.showOnlyOwnedCommunities = true; 63 | } 64 | 65 | if(url.indexOf('/joined')>-1){ 66 | this.showOnlyJoinedCommunities = true; 67 | } 68 | 69 | if(url.indexOf('/moderating')>-1){ 70 | this.showOnlyModeratingCommunities = true; 71 | } 72 | 73 | if(url.indexOf('/discover')>-1){ 74 | this.discoverCommunities = true; 75 | } 76 | 77 | if(url.indexOf('/recently-active')>-1){ 78 | this.showRecentlyActiveCommunities = true; 79 | } 80 | 81 | this.ndkProvider.isLoggedInUsingPubKey$.subscribe(val => { 82 | this.isLoggedInUsingPubKey = val; 83 | }); 84 | 85 | if(!this.discoverCommunities && !this.showRecentlyActiveCommunities ){ 86 | this.fetchCommunities(); 87 | } 88 | } 89 | 90 | onLeave(community:any){ 91 | let cardToRemove = community as Community 92 | const listAfterDelete = this.communities?.filter((c)=> c.id !== cardToRemove.id); 93 | this.communities = listAfterDelete; 94 | } 95 | 96 | onSearchTermChange(){ 97 | let searchFor = this.searchTerm; 98 | if(searchFor.length>2){ 99 | if(searchFor.startsWith('n/')){ 100 | searchFor = searchFor.substring(2,searchFor.length); 101 | } 102 | this.populateSearchResults(searchFor); 103 | } 104 | 105 | } 106 | 107 | async populateSearchResults(searchFor:string){ 108 | if(searchFor.length>0){ 109 | this.searchInProgress = true; 110 | const filtered = await this.objectCache.communities?.filter((c) => { 111 | //this.communities?.filter((c) => { 112 | if(c.displayName && c.displayName.toLocaleLowerCase().indexOf(searchFor.toLocaleLowerCase())>-1){ 113 | return true; 114 | } 115 | if(c.name && c.name.toLocaleLowerCase().indexOf(searchFor.toLocaleLowerCase())>-1){ 116 | return true; 117 | } 118 | return false; 119 | }) 120 | this.searchResults = await filtered.toArray(); 121 | this.searchInProgress = false; 122 | } else { 123 | this.searchInProgress = false; 124 | this.searchResults = this.communities; 125 | } 126 | 127 | } 128 | 129 | async fetchCommunities(){ 130 | try{ 131 | this.loadingEvents = true; 132 | if(this.showOnlyJoinedCommunities){ 133 | this.communities = await this.fetchJoinedCommunities(); 134 | } else if (this.showOnlyModeratingCommunities){ 135 | this.communities = await this.ndkProvider.fetchCommunities(this.limit, undefined, this.until,undefined, this.showOnlyModeratingCommunities); 136 | } else if (this.showOnlyOwnedCommunities){ 137 | this.communities = await this.ndkProvider.fetchCommunities(this.limit, undefined, this.until, this.showOnlyOwnedCommunities); 138 | } 139 | else { 140 | const communitiesFromCache = await this.objectCache.communities.toArray(); 141 | if(communitiesFromCache && communitiesFromCache.length > 0){ 142 | this.communities = communitiesFromCache; 143 | this.loadingEvents = false; 144 | } 145 | await this.ndkProvider.fetchCommunities(); 146 | this.communities = await this.objectCache.communities.toArray(); 147 | } 148 | } catch (err){ 149 | console.error(err); 150 | } finally{ 151 | this.loadingEvents = false; 152 | } 153 | } 154 | 155 | async fetchJoinedCommunities():Promise{ 156 | var fromStandardSource = await this.communityService.fetchJoinedCommunities(); 157 | var fromAppSource = await this.ndkProvider.fetchJoinedCommunities(); 158 | var deDuplicated = this.communityService.deDuplicateCommunities([...fromStandardSource].concat(fromAppSource)); 159 | return deDuplicated; 160 | } 161 | 162 | 163 | onCloseCreateCommunity($event:any){ 164 | this.showCreateCommunity = false; 165 | if($event){ 166 | this.ngOnInit(); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/app/page/community-list/community-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{'Search Communities'|translate}} 7 |

8 |

9 | {{'Recently Active Communities'|translate}} 12 |

13 |

14 | {{'Communities you created'|translate}} 17 |

18 |

19 | {{'Communities you joined'|translate}} 22 |

23 |

24 | {{'Communities you moderate'|translate}} 27 |

28 |
29 |
30 |
31 |
32 | 43 |
44 |
45 | 46 | 56 | 57 |
58 |
59 |
60 | Loading communities... 61 |
62 |
63 | 64 |
65 |
66 | Searching communities... 67 |
68 |
69 | 70 |
71 |
72 |
73 | No communities at all. Fishy! 74 | You have not created any community so far. 75 |
76 |
77 |
78 | 79 |
80 |
81 |
82 | {{'Search to see communities'|translate}} 👆 83 |
84 |
85 |
86 | 87 | 88 |
89 | 90 |
91 |
92 |
93 | 94 | 95 |
96 | 97 |
98 |
99 | 100 |
101 | 102 |
103 |
104 | 105 |
106 | 107 | 108 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | Login 36 | 37 | Onboarding Wizard 38 | 39 | Profile 40 | Preferences 41 | 42 | Logout 43 | 44 | 45 | 46 |
47 |
48 |
49 | 50 | 51 | 52 | Login 53 | 54 | 55 | My Feed 56 | 57 | 58 | Profile 59 | 60 | 61 | Preferences 62 | 63 | 64 | {{ndkProvider.isTryingZapddit ? ('Reset Data'|translate) : ('Logout'|translate)}} 65 | 66 | 67 | 69 | 70 | Recently active 71 | 72 | 73 | Search 74 | 75 | 76 | Joined 77 | 78 | 79 | Created by me 80 | 81 | 82 | Moderating 83 | 84 | 85 | 86 | 87 | 88 |
89 |
90 | 91 |
92 | 93 |
94 |
95 | 96 | Loading... 97 | 98 |
99 |
100 |
101 |
    102 |
  • {{notice}}
  • 103 |
104 |
105 |
106 |
107 |
108 |
109 | -------------------------------------------------------------------------------- /src/app/service/community.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NdkproviderService } from './ndkprovider.service'; 3 | import { Community } from '../model/community'; 4 | import { Constants } from '../util/Constants'; 5 | import { NDKEvent, NDKTag } from '@nostr-dev-kit/ndk'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class CommunityService { 11 | 12 | readonly MODERATOR:string='moderator'; 13 | 14 | constructor(private ndkProviderService: NdkproviderService) { } 15 | 16 | async joinCommunity(community:Community){ 17 | if(this.ndkProviderService.isTryingZapddit){ 18 | let joinedCommunities = this.ndkProviderService.appData.followedCommunities; 19 | if(joinedCommunities === ""){ 20 | this.ndkProviderService.appData.followedCommunities = community.id! 21 | } else { 22 | var communities = [...joinedCommunities.split(",")] 23 | communities.push(community.id!) 24 | this.ndkProviderService.appData.followedCommunities = communities.join(','); 25 | } 26 | this.ndkProviderService.followedCommunitiesEmitter.emit(this.ndkProviderService.appData.followedCommunities) 27 | localStorage.setItem(Constants.FOLLOWEDCOMMUNITIES,this.ndkProviderService.appData.followedCommunities); 28 | return; 29 | } 30 | var joinedCommunities = await this.fetchJoinedCommunitiesMetadata() || []; 31 | joinedCommunities.push(community); 32 | await this.publishJoiningEvent(joinedCommunities); 33 | } 34 | 35 | async publishJoiningEvent(data:Community[]){ 36 | var followedCommunities = [...new Set(data)]; 37 | await this.publishCommunityListEvent(followedCommunities); 38 | var followedCommunitiesCsv = followedCommunities.map(i=>i.id!).join(','); 39 | localStorage.setItem(Constants.FOLLOWEDCOMMUNITIES,followedCommunitiesCsv); 40 | this.ndkProviderService.appData.followedCommunities = followedCommunitiesCsv; 41 | this.ndkProviderService.followedCommunitiesEmitter.emit(followedCommunitiesCsv); 42 | } 43 | 44 | async leaveCommunity(community:Community){ 45 | if(this.ndkProviderService.isTryingZapddit){ 46 | let joinedCommunities = this.ndkProviderService.appData.followedCommunities; 47 | if(joinedCommunities === ""){ 48 | return; 49 | } else { 50 | var communities = [...joinedCommunities.split(",")] 51 | communities = communities.filter((c) => c !== community.id) 52 | this.ndkProviderService.appData.followedCommunities = communities.join(','); 53 | this.ndkProviderService.followedCommunitiesEmitter.emit(this.ndkProviderService.appData.followedCommunities) 54 | localStorage.setItem(Constants.FOLLOWEDCOMMUNITIES,this.ndkProviderService.appData.followedCommunities); 55 | 56 | } 57 | return; 58 | } 59 | var existing = (await this.fetchJoinedCommunitiesMetadata() || []).filter(item=>item.id !== community.id!); 60 | await this.publishJoiningEvent(existing); 61 | } 62 | 63 | async createCommunity(newCommunity:Community){ 64 | if (this.ndkProviderService.canWriteToNostr) { 65 | const ndkEvent = this.ndkProviderService.createNDKEvent(); 66 | let tags: NDKTag[] = []; 67 | tags.push(['d', newCommunity.name!]); 68 | 69 | if(newCommunity.displayName) 70 | tags.push(['name', newCommunity.displayName]); 71 | 72 | if(newCommunity.description) 73 | tags.push(['description', newCommunity.description]) 74 | 75 | if(newCommunity.image){ 76 | tags.push(['image', newCommunity.image]) 77 | } 78 | 79 | if(newCommunity.rules) 80 | tags.push(['rules', newCommunity.rules]) 81 | 82 | if(newCommunity.moderatorHexKeys && newCommunity.moderatorHexKeys.length>0){ 83 | for(let mod of newCommunity.moderatorHexKeys) 84 | tags.push(['p', mod,'',this.MODERATOR]) 85 | } 86 | 87 | ndkEvent.tags = tags; 88 | ndkEvent.kind = 34550; 89 | await ndkEvent.publish(); 90 | } 91 | } 92 | 93 | async fetchJoinedCommunities():Promise{ 94 | var communitiesArr; 95 | if(this.ndkProviderService.isTryingZapddit){ 96 | communitiesArr = this.ndkProviderService.appData.followedCommunities.split(","); 97 | } else { 98 | communitiesArr = (await this.ndkProviderService.fetchLatestDataFromInteroperableList()).communities; 99 | } 100 | var communitiesDetails:Community[] = []; 101 | for(let tag of communitiesArr) { 102 | if(tag){ 103 | communitiesDetails.push((await this.ndkProviderService.getCommunityDetails(tag))!); 104 | } 105 | } 106 | return communitiesDetails; 107 | } 108 | 109 | async fetchJoinedCommunitiesMetadata():Promise{ 110 | var communitiesArr; 111 | 112 | if(this.ndkProviderService.isTryingZapddit){ 113 | communitiesArr = this.ndkProviderService.appData.followedCommunities.split(","); 114 | } else { 115 | communitiesArr = (await this.ndkProviderService.fetchLatestDataFromInteroperableList()).communities; 116 | } 117 | 118 | var communities:Community[] = []; 119 | 120 | for(let c of communitiesArr){ 121 | communities.push({id: c}); 122 | } 123 | 124 | return communities; 125 | } 126 | 127 | buildCommunityListEvent(existing:Community[]): NDKEvent { 128 | const deDupedCommunities = this.deDuplicateCommunities(existing); 129 | 130 | var event = this.ndkProviderService.createNDKEvent(); 131 | let tags: NDKTag[] = []; 132 | tags.push(['d', 'communities']); 133 | 134 | for(let item of deDupedCommunities){ 135 | if(item.id && !(item.creatorHexKey!) && !(item.name!)) 136 | tags.push(['a',`${item.id}`]) 137 | else 138 | tags.push(['a',`34550:${item.creatorHexKey}:${item.name!}`]) 139 | } 140 | 141 | event.tags = tags; 142 | event.kind = 30001; 143 | return event; 144 | } 145 | 146 | async publishCommunityListEvent(existing:Community[]){ 147 | var event = this.buildCommunityListEvent(existing); 148 | await event.sign(); 149 | await event.publish(); 150 | } 151 | 152 | async clearCommunitiesFromAppData(){ 153 | var communitiesCleared = localStorage.getItem(Constants.COMMUNITIES_CLEARED) || "false"; 154 | var data = await this.ndkProviderService.fetchAppData(); 155 | if(communitiesCleared === "false" || (data.communities.length>0 && data.communities[0]!=='')){ 156 | this.ndkProviderService.publishAppData(data.hashtags.join(','), data.downzapRecipients,data.mutedHashtags,''); 157 | localStorage.setItem(Constants.COMMUNITIES_CLEARED, "true") 158 | } 159 | } 160 | 161 | deDuplicateCommunities(communities:Community[]){ 162 | return communities.reduce((accumulator:Community[], current:Community) => { 163 | if (!accumulator.find((item) => item.id === current.id)) { 164 | accumulator.push(current); 165 | } 166 | return accumulator; 167 | }, []); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/app/util/LoginUtil.ts: -------------------------------------------------------------------------------- 1 | import * as secp from "@noble/secp256k1"; 2 | import * as utils from "@noble/curves/abstract/utils"; 3 | import * as bip39 from "@scure/bip39"; 4 | import { wordlist } from "@scure/bip39/wordlists/english"; 5 | import { HDKey } from "@scure/bip32"; 6 | import { bech32 } from "bech32"; 7 | import { NDKUser } from "@nostr-dev-kit/ndk"; 8 | import { Util } from "./Util"; 9 | 10 | 11 | 12 | /* 13 | Most of this logic is adapted from snort.social repository. Thanks to them :) 14 | */ 15 | 16 | export enum NostrPrefix { 17 | PublicKey = "npub", 18 | PrivateKey = "nsec", 19 | Note = "note", 20 | 21 | // TLV prefixes 22 | Profile = "nprofile", 23 | Event = "nevent", 24 | Relay = "nrelay", 25 | Address = "naddr", 26 | } 27 | 28 | export enum TLVEntryType { 29 | Special = 0, 30 | Relay = 1, 31 | Author = 2, 32 | Kind = 3, 33 | } 34 | 35 | export interface TLVEntry { 36 | type: TLVEntryType; 37 | length: number; 38 | value: string | number; 39 | } 40 | 41 | export interface NewCredential{ 42 | pubkey: string, 43 | privateKey:string 44 | } 45 | export class LoginUtil{ 46 | static getHexFromPrivateOrPubKey(key:string):string{ 47 | const hasSubtleCrypto = window.crypto.subtle !== undefined; 48 | const insecureMsg = "Can't login with private key on an insecure connection, please use a Nostr key manager (NIP-07) extension instead" 49 | 50 | if (key.startsWith("nsec")) { 51 | if (!hasSubtleCrypto) { 52 | throw new Error(insecureMsg); 53 | } 54 | const hexKey = LoginUtil.bech32ToHex(key); 55 | if (secp.utils.isValidPrivateKey(hexKey)) { 56 | return hexKey; 57 | } else { 58 | throw new Error("Invalid Private Key"); 59 | } 60 | } else if (key.startsWith("npub")) { 61 | const hexKey = LoginUtil.bech32ToHex(key); 62 | return hexKey; 63 | } else if (secp.utils.isValidPrivateKey(key)) { 64 | if (!hasSubtleCrypto) { 65 | throw new Error(insecureMsg); 66 | } 67 | return key; 68 | } else { 69 | throw new Error("Invalid Private Key"); 70 | } 71 | } 72 | 73 | static bech32ToHex(str: string) { 74 | try { 75 | const nKey = bech32.decode(str, 1_000); 76 | const buff = bech32.fromWords(nKey.words); 77 | return secp.utils.bytesToHex(Uint8Array.from(buff)); 78 | } catch { 79 | return str; 80 | } 81 | } 82 | 83 | /* Thanks to https://github.com/v0l/snort/blob/main/packages/app/src/Util.ts 84 | */ 85 | static hexToBech32(idType: string, hex?: string) { 86 | if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) { 87 | return ""; 88 | } 89 | 90 | try { 91 | if (idType === NostrPrefix.Note || idType === NostrPrefix.PrivateKey || idType === NostrPrefix.PublicKey) { 92 | const buf = utils.hexToBytes(hex); 93 | return bech32.encode(idType, bech32.toWords(buf)); 94 | } else { 95 | return LoginUtil.encodeTLV(idType as NostrPrefix, hex); 96 | } 97 | } catch (e) { 98 | console.warn("Invalid hex", hex, e); 99 | return ""; 100 | } 101 | } 102 | 103 | static encodeTLV(prefix: NostrPrefix, id: string, relays?: string[], kind?: number, author?: string) { 104 | const enc = new TextEncoder(); 105 | const buf = prefix === NostrPrefix.Address ? enc.encode(id) : utils.hexToBytes(id); 106 | 107 | const tl0 = [0, buf.length, ...buf]; 108 | const tl1 = 109 | relays 110 | ?.map((a) => { 111 | const data = enc.encode(a); 112 | return [1, data.length, ...data]; 113 | }) 114 | .flat() ?? []; 115 | 116 | const tl2 = author ? [2, 32, ...utils.hexToBytes(author)] : []; 117 | const tl3 = kind ? [3, 4, ...new Uint8Array(new Uint32Array([kind]).buffer).reverse()] : [] 118 | 119 | return bech32.encode(prefix, bech32.toWords([...tl0, ...tl1, ...tl2, ...tl3]), 1_000); 120 | } 121 | 122 | static generateBip39Entropy(mnemonic?: string): Uint8Array { 123 | try { 124 | const mn = mnemonic ?? bip39.generateMnemonic(wordlist, 256); 125 | return bip39.mnemonicToEntropy(mn, wordlist); 126 | } catch (e) { 127 | throw new Error("INVALID MNEMONIC PHRASE"); 128 | } 129 | } 130 | 131 | static generateNewCredential(): NewCredential { 132 | const ent = LoginUtil.generateBip39Entropy(); 133 | const privateKey = LoginUtil.entropyToPrivateKey(ent); 134 | const publicKey = utils.bytesToHex(secp.schnorr.getPublicKey(privateKey)); 135 | return { 136 | pubkey: this.hexToBech32("npub",publicKey), 137 | privateKey: this.hexToBech32("nsec",privateKey) 138 | } 139 | } 140 | 141 | /** 142 | * Derive NIP-06 private key from master key 143 | */ 144 | static entropyToPrivateKey(entropy: Uint8Array): string { 145 | const masterKey = HDKey.fromMasterSeed(entropy); 146 | const newKey = masterKey.derive("m/44'/1237'/0'/0/0"); // Thanks - https://github.com/v0l/snort/blob/main/packages/app/src/Const.ts 147 | 148 | if (!newKey.privateKey) { 149 | throw new Error("INVALID KEY DERIVATION"); 150 | } 151 | 152 | return utils.bytesToHex(newKey.privateKey); 153 | } 154 | 155 | static getPublicKey(privKey: string) { 156 | return secp.utils.bytesToHex(secp.schnorr.getPublicKey(privKey)); 157 | } 158 | 159 | static getNpubFromHex( hex?: string) { 160 | if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) { 161 | return ""; 162 | } 163 | try { 164 | const buf = secp.utils.hexToBytes(hex); 165 | return bech32.encode('npub', bech32.toWords(buf)); 166 | } catch (e) { 167 | console.warn("Invalid hex", hex, e); 168 | return ""; 169 | } 170 | } 171 | 172 | static decodeTLV(str: string) { 173 | const decoded = bech32.decode(str, 1_000); 174 | const data = bech32.fromWords(decoded.words); 175 | 176 | const entries: TLVEntry[] = []; 177 | let x = 0; 178 | while (x < data.length) { 179 | const t = data[x]; 180 | const l = data[x + 1]; 181 | const v = data.slice(x + 2, x + 2 + l); 182 | entries.push({ 183 | type: t, 184 | length: l, 185 | value: LoginUtil.decodeTLVEntry(t, decoded.prefix, new Uint8Array(v)), 186 | }); 187 | x += 2 + l; 188 | } 189 | return entries; 190 | } 191 | 192 | static decodeTLVEntry(type: TLVEntryType, prefix: string, data: Uint8Array) { 193 | switch (type) { 194 | case TLVEntryType.Special: { 195 | if (prefix === NostrPrefix.Address) { 196 | return new TextDecoder("ASCII").decode(data); 197 | } else { 198 | return utils.bytesToHex(data); 199 | } 200 | } 201 | case TLVEntryType.Author: { 202 | return utils.bytesToHex(data); 203 | } 204 | case TLVEntryType.Kind: { 205 | return new Uint32Array(new Uint8Array(data.reverse()).buffer)[0]; 206 | } 207 | case TLVEntryType.Relay: { 208 | return new TextDecoder("ASCII").decode(data); 209 | } 210 | } 211 | } 212 | } -------------------------------------------------------------------------------- /src/app/component/note-composer/note-composer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; 2 | import { Observable, Subject, debounceTime, filter, from, of, switchMap } from 'rxjs'; 3 | import { User } from 'src/app/model/user'; 4 | import { NdkproviderService } from 'src/app/service/ndkprovider.service'; 5 | import { LoginUtil } from 'src/app/util/LoginUtil'; 6 | import { ZappeditdbService } from '../../service/zappeditdb.service'; 7 | import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; 8 | import Uploader from 'src/app/util/Uploader'; 9 | import { Community } from 'src/app/model/community'; 10 | import { ObjectCacheService } from 'src/app/service/object-cache.service'; 11 | 12 | const HASHTAG_REGEX=/(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/gi; 13 | const NOSTR_NPUB_REGEX = /nostr:(npub[\S]*)/gi; 14 | 15 | const NOSTR_NOTE_REGEX = /nostr:(note1[\S]*)/gi; 16 | 17 | 18 | @Component({ 19 | selector: 'app-note-composer', 20 | templateUrl: './note-composer.component.html', 21 | styleUrls: ['./note-composer.component.scss'] 22 | }) 23 | 24 | export class NoteComposerComponent { 25 | 26 | static loggedInWithNpub:boolean = false; 27 | NoteComposerComponent = NoteComposerComponent; 28 | 29 | @Input() 30 | isActivated:boolean = false; 31 | 32 | @Input() 33 | isReply:boolean = false; 34 | 35 | @Input() 36 | parentEvent?:NDKEvent; 37 | 38 | @Input() 39 | currentTag?:string; 40 | 41 | @Input() 42 | currentCommunity?:Community; 43 | 44 | @Input() 45 | parentEventId?:string; 46 | 47 | @Output() 48 | postedEventEmitter: EventEmitter = new EventEmitter(); 49 | 50 | @Output() 51 | clickEventEmitter:EventEmitter = new EventEmitter(); 52 | 53 | @ViewChild('noteText') 54 | noteText?: ElementRef; 55 | isSendingNote:boolean = false; 56 | noteSent:boolean = false; 57 | searchResults$: Observable = of([]); 58 | uploadingNow: boolean = false; 59 | uploadError?: string; 60 | ndkProvider: NdkproviderService; 61 | private searchTermStream = new Subject(); 62 | 63 | constructor(ndkProvider: NdkproviderService, private objectCache:ObjectCacheService){ 64 | this.ndkProvider = ndkProvider; 65 | } 66 | 67 | ngOnInit() { 68 | if(this.isReply){ 69 | this.isActivated = true; 70 | } 71 | this.searchTermStream 72 | .subscribe((value:string) => { 73 | this.searchResults$ = this.getItems(value); 74 | },(err:any)=> console.error(err)); 75 | 76 | this.ndkProvider.isLoggedInUsingPubKey$.subscribe(val => { 77 | NoteComposerComponent.loggedInWithNpub = val; 78 | }) 79 | } 80 | 81 | activate(){ 82 | this.isActivated = true; 83 | } 84 | 85 | getItems(term:string): Observable { 86 | if (!term) { 87 | // if the search term is empty, return an empty array 88 | return of([]); 89 | } 90 | // return this.http.get('api/names') // get all names 91 | return from(this.findUsersFromFewLetters(term)); // get filtered names 92 | } 93 | 94 | doSearch(term: string) { 95 | this.searchTermStream.next(term); 96 | } 97 | 98 | formatSelectedUser(user: User){ 99 | return "nostr:"+user.npub+" "; 100 | } 101 | 102 | async findUsersFromFewLetters(term: string): Promise { 103 | if (!term) { 104 | return []; 105 | } 106 | let usersFromCache = await this.objectCache.users.toArray(); 107 | let filteredUsersFromCache = usersFromCache.filter((user:User)=>{ 108 | return user.displayName?.toLocaleLowerCase().startsWith(term) 109 | || user.name?.toLocaleLowerCase().startsWith(term) 110 | || user.npub?.toLocaleLowerCase().startsWith(term) 111 | }) 112 | console.log("Result for " +term + " " +filteredUsersFromCache ) 113 | return filteredUsersFromCache; 114 | } 115 | 116 | async sendNote(){ 117 | this.isSendingNote = true; 118 | let noteText =this.noteText?.nativeElement.value; 119 | if(noteText){ 120 | let hashTags = this.getHashTagsFromText(noteText); 121 | let userMentions = this.getUserMentionsFromText(noteText); 122 | let noteMentions = this.getNoteMentionsFromText(noteText); 123 | let postedEvent = await this.ndkProvider.sendNote(noteText,hashTags,userMentions,noteMentions,this.parentEvent,this.parentEventId,this.currentCommunity); 124 | this.isSendingNote = false; 125 | this.postedEventEmitter.emit(postedEvent); 126 | if(this.noteText){ 127 | this.noteText.nativeElement.value= this.currentTag? '#'+this.currentTag+' ' : ''; 128 | } 129 | this.noteSent =true; 130 | setTimeout(()=>{ 131 | this.noteSent = false; 132 | }, 3000) 133 | } 134 | } 135 | 136 | async attachFile() { 137 | try { 138 | this.uploadingNow = true; 139 | const file = await this.openFile(); 140 | if (file) { 141 | this.uploadFile(file); 142 | } 143 | } catch (error) { 144 | if (error instanceof Error) { 145 | console.error(error) 146 | this.uploadError = error.message; 147 | } 148 | } finally{ 149 | this.uploadingNow = false; 150 | } 151 | } 152 | 153 | async uploadFile(file: File | Blob) { 154 | try { 155 | if (file) { 156 | this.uploadingNow = true; 157 | const uploaderResponse = await Uploader.upload(file); 158 | if (uploaderResponse.url) { 159 | if(this.noteText){ 160 | this.noteText.nativeElement.value += '\n '+uploaderResponse.url; 161 | } 162 | } else if (uploaderResponse?.error) { 163 | this.uploadError = uploaderResponse?.error 164 | } 165 | } 166 | } catch (error) { 167 | if (error instanceof Error) { 168 | this.uploadError = error.message; 169 | } 170 | } finally{ 171 | this.uploadingNow = false; 172 | } 173 | } 174 | 175 | handlePaste(evt:ClipboardEvent) { 176 | if (evt.clipboardData) { 177 | const clipboardItems = evt.clipboardData.items; 178 | const items: DataTransferItem[] = Array.from(clipboardItems).filter(function (item: DataTransferItem) { 179 | // Filter the image items only 180 | return /^image\//.test(item.type); 181 | }); 182 | if (items.length === 0) { 183 | return; 184 | } 185 | 186 | const item = items[0]; 187 | const blob = item.getAsFile(); 188 | if (blob) { 189 | this.uploadFile(blob); 190 | } 191 | } 192 | }; 193 | 194 | 195 | async openFile(): Promise { 196 | return new Promise(resolve => { 197 | const newElement = document.createElement("input"); 198 | newElement.type = "file"; 199 | newElement.accept = "image/*"; 200 | newElement.onchange = (e: Event) => { 201 | const currentElement = e.target as HTMLInputElement; 202 | if (currentElement.files) { 203 | resolve(currentElement.files[0]); 204 | } else { 205 | resolve(undefined); 206 | } 207 | }; 208 | newElement.click(); 209 | }); 210 | } 211 | 212 | getHashTagsFromText(text:string){ 213 | const hashTagMatches= [...text.matchAll(HASHTAG_REGEX)]; 214 | return hashTagMatches.map(hashTag => { 215 | return hashTag[0].slice(1); 216 | }); 217 | } 218 | 219 | getUserMentionsFromText(text:string){ 220 | const userMentionMatches = [...text.matchAll(NOSTR_NPUB_REGEX)]; 221 | return userMentionMatches.map(userMention => { 222 | return LoginUtil.bech32ToHex(userMention[1]) 223 | }); 224 | } 225 | 226 | getNoteMentionsFromText(text:string){ 227 | const noteMentionMatches = [...text.matchAll(NOSTR_NOTE_REGEX)]; 228 | return noteMentionMatches.map(noteMention => { 229 | return LoginUtil.bech32ToHex(noteMention[1]) 230 | }); 231 | } 232 | 233 | onComposeClick(event:MouseEvent){ 234 | console.log('reply clicked'); 235 | event.stopImmediatePropagation(); 236 | event.stopPropagation(); 237 | this.clickEventEmitter.emit(event); 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /src/app/component/zapdialog/zapdialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, EventEmitter, Input, Output, ViewChild, Renderer2, OnInit, ChangeDetectorRef } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { NDKEvent } from '@nostr-dev-kit/ndk'; 4 | import { NdkproviderService } from 'src/app/service/ndkprovider.service'; 5 | import QRCodeStyling from 'qr-code-styling'; 6 | import { Clipboard } from '@angular/cdk/clipboard'; 7 | import { Constants } from 'src/app/util/Constants'; 8 | import { Community } from 'src/app/model/community'; 9 | import { ZapSplitUtil } from 'src/app/util/ZapSplitUtil'; 10 | import { HexKeyWithSplitPercentage } from 'src/app/model/ZapSplitConfig'; 11 | 12 | @Component({ 13 | selector: 'app-zapdialog', 14 | templateUrl: './zapdialog.component.html', 15 | styleUrls: ['./zapdialog.component.scss'] 16 | }) 17 | export class ZapdialogComponent implements OnInit { 18 | 19 | @Input() 20 | eventHexId?:string; 21 | 22 | @Input() 23 | authorName:string | undefined; 24 | 25 | @Input() 26 | imageUrl:string | undefined; 27 | 28 | @Input() 29 | show:boolean = false; 30 | 31 | @Input() 32 | type:string | undefined; 33 | 34 | @Input() 35 | authorNpub:string|undefined; 36 | 37 | @Input() 38 | community?:Community; 39 | 40 | @Output() 41 | onClose = new EventEmitter(); 42 | 43 | @Output() 44 | onUpzapDone = new EventEmitter(); 45 | 46 | zapValue:number = 1; 47 | disableZap :boolean =false; 48 | showQR:boolean = false; 49 | @ViewChild("canvas") 50 | canvas: ElementRef | undefined; 51 | invoice:string|null=null; 52 | errorMsg:string | null = null; 53 | zappingNow:boolean = false; 54 | 55 | @Input() 56 | event:NDKEvent|undefined; 57 | 58 | constructor(private renderer: Renderer2,private ndkProvider: NdkproviderService, 59 | private clipboard: Clipboard) { 60 | 61 | } 62 | 63 | ngOnInit(): void { 64 | this.showQR = false; 65 | const satsFromLocalStorage = localStorage.getItem(Constants.DEFAULTSATSFORZAPS); 66 | if (satsFromLocalStorage) { 67 | try { 68 | this.zapValue = Number.parseInt(satsFromLocalStorage); 69 | } catch (e) { 70 | console.error(e); 71 | } 72 | } 73 | } 74 | 75 | async initiateZap(){ 76 | if(this.event){ 77 | this.event.id = this.eventHexId!; 78 | } 79 | if(this.community){ 80 | await this.zapCommunityMods(); 81 | } else { 82 | if(this.type==='upzap'){ 83 | await this.upZap(); 84 | }else{ 85 | await this.downZap(); 86 | } 87 | } 88 | } 89 | 90 | async upZap() { 91 | this.zappingNow = true; 92 | try{ 93 | if (this.event) { 94 | if(this.canvas && this.canvas?.nativeElement) 95 | this.renderer.setProperty(this.canvas?.nativeElement, 'innerHTML', ''); 96 | 97 | if(window.hasOwnProperty('webln')){ 98 | //@ts-ignore 99 | if(window.webln.connected){ 100 | let zapForEvent = this.zapValue; 101 | zapForEvent = this.executeZapSplits(zapForEvent); 102 | const result = await this.sendWebLnZapForEvent(zapForEvent); 103 | if(!result?.preimage) { 104 | throw new Error('Payment failed. Please try again'); 105 | } else { 106 | this.zapDoneClicked(); 107 | } 108 | return; 109 | } 110 | } 111 | const invoice = await this.ndkProvider.zapRequest(this.zapValue, this.event!); 112 | this.invoice = invoice; 113 | this.openQRDialog(invoice); 114 | } 115 | }catch(e:any){ 116 | this.errorMsg = e.message; 117 | }finally{ 118 | this.zappingNow = false; 119 | } 120 | } 121 | 122 | private executeZapSplits(zapForEvent: number) { 123 | let zapSplitConfig = this.getZapSplitConfigFromLocalStorage(); 124 | zapSplitConfig = ZapSplitUtil.validateZapSplitConfig(zapSplitConfig); 125 | 126 | //zap devs 127 | zapSplitConfig.developers.forEach((d:HexKeyWithSplitPercentage) => { 128 | if(d.percentage > 0){ 129 | const zapForDev = Math.ceil((d.percentage / 100) * this.zapValue); 130 | zapForEvent = this.zapValue - zapForDev; 131 | this.zapUser(d.hexKey, zapForDev, d.percentage, false); 132 | } 133 | }) 134 | 135 | //zap translators 136 | zapSplitConfig.translators.forEach((t:HexKeyWithSplitPercentage) => { 137 | if(t.percentage > 0){ 138 | const zapForTranslator = Math.ceil((t.percentage / 100) * this.zapValue); 139 | zapForEvent = this.zapValue - zapForTranslator; 140 | this.zapUser(t.hexKey, zapForTranslator, t.percentage, false); 141 | } 142 | }) 143 | 144 | return zapForEvent; 145 | } 146 | 147 | private getZapSplitConfigFromLocalStorage(){ 148 | var zapSplitConfigText = localStorage.getItem(Constants.ZAP_SPLIT_CONFIG); 149 | try{ 150 | if (zapSplitConfigText !== null && zapSplitConfigText !== undefined && zapSplitConfigText !== '') { 151 | return JSON.parse(zapSplitConfigText!); 152 | } 153 | } 154 | catch(e){ 155 | console.error(e); 156 | } 157 | } 158 | 159 | private async sendWebLnZapForEvent(zapValue:number) { 160 | const invoice = await this.ndkProvider.zapRequest(zapValue, this.event!); 161 | //@ts-ignore 162 | const result = await window.webln.sendPayment(invoice); 163 | return result; 164 | } 165 | 166 | async downZap() { 167 | this.zappingNow = true; 168 | try{ 169 | if (this.event) { 170 | if(this.canvas && this.canvas?.nativeElement) 171 | this.renderer.setProperty(this.canvas?.nativeElement, 'innerHTML', ''); 172 | 173 | if(window.hasOwnProperty('webln')){ 174 | //@ts-ignore 175 | if(window.webln.connected){ 176 | let zapForEvent = this.zapValue; 177 | zapForEvent = this.executeZapSplits(zapForEvent); 178 | const invoice = await this.ndkProvider.downZapRequest(zapForEvent, 179 | this.event, 180 | await this.ndkProvider.getNdkUserFromNpub(this.ndkProvider.appData.downzapRecipients), 181 | '-' 182 | ); 183 | //@ts-ignore 184 | const result = await window.webln.sendPayment(invoice); 185 | if(!result?.preimage) { 186 | throw new Error('Payment failed. Please try again'); 187 | } else { 188 | this.zapDoneClicked(); 189 | } 190 | return; 191 | } 192 | } 193 | const invoice = await this.ndkProvider.downZapRequest(this.zapValue, 194 | this.event, 195 | await this.ndkProvider.getNdkUserFromNpub(this.ndkProvider.appData.downzapRecipients), 196 | '-' 197 | ); 198 | this.invoice = invoice; 199 | this.openQRDialog(this.invoice); 200 | } 201 | }catch(e:any){ 202 | this.errorMsg = e.message; 203 | }finally{ 204 | this.zappingNow = false; 205 | } 206 | } 207 | 208 | async zapCommunityMods(){ 209 | const zapValue = this.zapValue; 210 | const mods = this.community?.moderatorHexKeys; 211 | if(mods){ 212 | for (const mod of mods){ 213 | try{ 214 | await this.zapUser(mod, zapValue/mods.length,undefined,true); 215 | }catch(e){ 216 | console.error(e); 217 | } 218 | } 219 | this.zapDoneClicked(); 220 | } 221 | } 222 | 223 | private async zapUser(pubkey: string, zapValue: number, zapSplitPercentage:any, forCommunity:boolean) { 224 | const modUser = this.ndkProvider.ndk?.getUser({ pubkey: pubkey }); 225 | let message = forCommunity? ('Great job with n/' + this.community?.name) :`Zap split ${zapSplitPercentage}% for zapddit`; 226 | const invoice = await modUser?.zap((zapValue * 1000), message, [], this.ndkProvider.ndk?.signer); 227 | if (window.hasOwnProperty('webln')) { 228 | //@ts-ignore 229 | if (window.webln.connected) { 230 | //@ts-ignore 231 | const result = await window.webln.sendPayment(invoice); 232 | if (!result?.preimage) { 233 | throw new Error('Payment failed for ' + modUser?.profile?.name + '. Please try again'); 234 | } 235 | } 236 | } 237 | } 238 | 239 | private openQRDialog(invoice: any) { 240 | const qr = new QRCodeStyling({ 241 | width: 256, 242 | height: 256, 243 | data: invoice ? invoice : undefined, 244 | margin: 5, 245 | type: "canvas", 246 | dotsOptions: { 247 | type: "rounded", 248 | }, 249 | cornersSquareOptions: { 250 | type: "extra-rounded", 251 | } 252 | }); 253 | this.showQR = true; 254 | setTimeout(() => qr.append(this.canvas?.nativeElement), 1000); 255 | } 256 | 257 | zapValueChange(event:any){ 258 | if(event===null || event===undefined){ 259 | this.disableZap = true; 260 | }else{ 261 | this.disableZap = false; 262 | } 263 | } 264 | 265 | close(value: boolean) { 266 | this.onChange(value); 267 | } 268 | 269 | onChange(event:any){ 270 | this.show=false; 271 | this.onClose.emit(true); 272 | } 273 | 274 | setSats(sats:number){ 275 | this.zapValue = sats; 276 | this.zapValueChange(sats); 277 | } 278 | 279 | openWallet(){ 280 | window.open(`lightning:${this.invoice}`,"_blank"); 281 | } 282 | 283 | copyInvoiceToClipboard(){ 284 | this.clipboard.copy(this.invoice!); 285 | } 286 | 287 | zapDoneClicked(){ 288 | this.showQR = false; 289 | this.zapValue = 0; 290 | this.onClose.emit(true); 291 | this.onUpzapDone.emit(true); 292 | } 293 | } 294 | --------------------------------------------------------------------------------