├── .editorconfig ├── .github ├── copilot-instructions.md └── workflows │ ├── build.yml │ └── desktop.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── angular.json ├── dependencies.txt ├── ngsw-config.json ├── package-lock.json ├── package.json ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── capabilities │ └── default.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ ├── lib.rs │ └── main.rs └── tauri.conf.json ├── src ├── .well-known │ └── assetlinks.json ├── app │ ├── app-routing.module.ts │ ├── app.config.ts │ ├── app.css │ ├── app.html │ ├── app.module.ts │ ├── app.routes.ts │ ├── app.spec.ts │ ├── app.ts │ ├── common │ │ └── NostrProtocolRequest.ts │ ├── components │ │ ├── feed-private │ │ │ ├── feed-private.css │ │ │ ├── feed-private.html │ │ │ └── feed-private.ts │ │ └── feed-public │ │ │ ├── feed-public.html │ │ │ └── feed-public.ts │ ├── development │ │ ├── development.css │ │ ├── development.html │ │ └── development.ts │ ├── example │ │ ├── example.css │ │ ├── example.html │ │ └── example.ts │ ├── pages │ │ ├── about │ │ │ ├── about.css │ │ │ ├── about.html │ │ │ ├── about.module.ts │ │ │ ├── about.spec.ts │ │ │ ├── about.ts │ │ │ └── licenses │ │ │ │ ├── licenses.css │ │ │ │ ├── licenses.html │ │ │ │ └── licenses.ts │ │ ├── article │ │ │ ├── article.css │ │ │ ├── article.html │ │ │ └── article.ts │ │ ├── articles │ │ │ └── articles.component.ts │ │ ├── badge │ │ │ ├── badge.css │ │ │ ├── badge.html │ │ │ └── badge.ts │ │ ├── badges │ │ │ ├── badges.css │ │ │ ├── badges.html │ │ │ └── badges.ts │ │ ├── chat │ │ │ ├── chat.html │ │ │ ├── chat.scss │ │ │ └── chat.ts │ │ ├── circles │ │ │ ├── circles.css │ │ │ ├── circles.html │ │ │ └── circles.ts │ │ ├── connect │ │ │ ├── connect.css │ │ │ ├── connect.html │ │ │ ├── connect.ts │ │ │ ├── consent-dialog │ │ │ │ ├── consent-dialog.css │ │ │ │ ├── consent-dialog.html │ │ │ │ └── consent-dialog.ts │ │ │ ├── create │ │ │ │ ├── create.css │ │ │ │ ├── create.html │ │ │ │ └── create.ts │ │ │ ├── key │ │ │ │ ├── key.css │ │ │ │ ├── key.html │ │ │ │ ├── key.ts │ │ │ │ └── qr-scan-dialog │ │ │ │ │ ├── qr-scan.css │ │ │ │ │ ├── qr-scan.html │ │ │ │ │ └── qr-scan.ts │ │ │ └── login │ │ │ │ ├── login.css │ │ │ │ ├── login.html │ │ │ │ └── login.ts │ │ ├── editor-badges │ │ │ ├── editor.css │ │ │ ├── editor.html │ │ │ └── editor.ts │ │ ├── editor │ │ │ ├── editor.css │ │ │ ├── editor.html │ │ │ └── editor.ts │ │ ├── feed │ │ │ ├── feed.html │ │ │ └── feed.ts │ │ ├── files │ │ │ ├── files.component.css │ │ │ ├── files.component.html │ │ │ ├── files.component.ts │ │ │ └── import-follow-dialog │ │ │ │ ├── import-follow-dialog.html │ │ │ │ ├── import-follow-dialog.scss │ │ │ │ └── import-follow-dialog.ts │ │ ├── followers │ │ │ ├── followers.css │ │ │ ├── followers.html │ │ │ └── followers.ts │ │ ├── following │ │ │ ├── following.css │ │ │ ├── following.html │ │ │ └── following.ts │ │ ├── home │ │ │ ├── home.css │ │ │ ├── home.html │ │ │ ├── home.ts │ │ │ └── relay.json │ │ ├── logout │ │ │ ├── logout.html │ │ │ └── logout.ts │ │ ├── message │ │ │ ├── message.css │ │ │ ├── message.html │ │ │ └── message.ts │ │ ├── messages │ │ │ ├── messages.css │ │ │ ├── messages.html │ │ │ └── messages.ts │ │ ├── note │ │ │ ├── note.css │ │ │ ├── note.html │ │ │ └── note.ts │ │ ├── notes │ │ │ ├── notes.css │ │ │ ├── notes.html │ │ │ └── notes.ts │ │ ├── notifications │ │ │ ├── notifications.css │ │ │ ├── notifications.html │ │ │ └── notifications.ts │ │ ├── people │ │ │ ├── import-follow-dialog │ │ │ │ ├── import-follow-dialog.html │ │ │ │ ├── import-follow-dialog.scss │ │ │ │ └── import-follow-dialog.ts │ │ │ ├── people.css │ │ │ ├── people.html │ │ │ └── people.ts │ │ ├── profile │ │ │ ├── profile.css │ │ │ ├── profile.html │ │ │ └── profile.ts │ │ ├── queue │ │ │ ├── add-media-dialog │ │ │ │ ├── add-media-dialog.css │ │ │ │ ├── add-media-dialog.html │ │ │ │ └── add-media-dialog.ts │ │ │ ├── queue.css │ │ │ ├── queue.html │ │ │ └── queue.ts │ │ ├── relays │ │ │ ├── relays.css │ │ │ ├── relays.html │ │ │ └── relays.ts │ │ ├── settings │ │ │ ├── settings.css │ │ │ ├── settings.html │ │ │ └── settings.ts │ │ └── user │ │ │ ├── user.css │ │ │ ├── user.html │ │ │ └── user.ts │ ├── schemas │ │ └── nostr-event.ts │ ├── services │ │ ├── app-update.ts │ │ ├── applicationstate.ts │ │ ├── article.ts │ │ ├── auth-guard.ts │ │ ├── authentication.ts │ │ ├── badge.ts │ │ ├── cache.ts │ │ ├── chat.service.ts │ │ ├── check-for-update.ts │ │ ├── circle.ts │ │ ├── content.ts │ │ ├── data-validation.ts │ │ ├── data.ts │ │ ├── database.ts │ │ ├── debug-log.ts │ │ ├── event.ts │ │ ├── interfaces.ts │ │ ├── label.ts │ │ ├── loading-resolver.ts │ │ ├── log-writer.ts │ │ ├── logger.ts │ │ ├── media.ts │ │ ├── message-control.service.ts │ │ ├── messages.ts │ │ ├── metric-service.ts │ │ ├── navigation.ts │ │ ├── nostr.ts │ │ ├── notes.ts │ │ ├── options.ts │ │ ├── profile.ts │ │ ├── queue.service.ts │ │ ├── queue.ts │ │ ├── relay.ts │ │ ├── search.ts │ │ ├── security.ts │ │ ├── spaces.ts │ │ ├── state.ts │ │ ├── storage.ts │ │ ├── theme.ts │ │ ├── thread.ts │ │ ├── ui.ts │ │ ├── upload.ts │ │ ├── user.service.ts │ │ ├── utilities.ts │ │ ├── zap-ui.ts │ │ └── zap.service.ts │ ├── shared │ │ ├── add-relay-dialog │ │ │ ├── add-relay-dialog.css │ │ │ ├── add-relay-dialog.html │ │ │ └── add-relay-dialog.ts │ │ ├── ago.pipe.ts │ │ ├── badge-card │ │ │ ├── badge-card.css │ │ │ ├── badge-card.html │ │ │ └── badge-card.ts │ │ ├── bech32.pipe.ts │ │ ├── chat-detail │ │ │ ├── chat-detail.component.html │ │ │ ├── chat-detail.component.scss │ │ │ ├── chat-detail.component.spec.ts │ │ │ └── chat-detail.component.ts │ │ ├── chat-item │ │ │ ├── chat-item.component.html │ │ │ ├── chat-item.component.scss │ │ │ ├── chat-item.component.spec.ts │ │ │ └── chat-item.component.ts │ │ ├── chat-list │ │ │ ├── chat-list.component.html │ │ │ ├── chat-list.component.scss │ │ │ ├── chat-list.component.spec.ts │ │ │ └── chat-list.component.ts │ │ ├── circle-style.ts │ │ ├── content-input-directive │ │ │ ├── content-input-height.directive.ts │ │ │ └── content-input.directive.ts │ │ ├── content-music │ │ │ ├── content-music.css │ │ │ ├── content-music.html │ │ │ └── content-music.ts │ │ ├── content-photos │ │ │ ├── content-photos.css │ │ │ ├── content-photos.html │ │ │ └── content-photos.ts │ │ ├── content-podcast │ │ │ ├── content-podcast.css │ │ │ ├── content-podcast.html │ │ │ └── content-podcast.ts │ │ ├── content │ │ │ ├── content.css │ │ │ ├── content.html │ │ │ └── content.ts │ │ ├── create-circle-dialog │ │ │ ├── create-circle-dialog.html │ │ │ ├── create-circle-dialog.scss │ │ │ └── create-circle-dialog.ts │ │ ├── create-follow-dialog │ │ │ ├── create-follow-dialog.html │ │ │ ├── create-follow-dialog.scss │ │ │ └── create-follow-dialog.ts │ │ ├── create-note-dialog │ │ │ ├── create-note-dialog.html │ │ │ ├── create-note-dialog.scss │ │ │ └── create-note-dialog.ts │ │ ├── date-time │ │ │ ├── date-time.component.html │ │ │ ├── date-time.component.scss │ │ │ ├── date-time.component.spec.ts │ │ │ └── date-time.component.ts │ │ ├── date │ │ │ ├── date.css │ │ │ ├── date.html │ │ │ └── date.ts │ │ ├── defaults.ts │ │ ├── directives │ │ │ └── drag-scroll.directive.ts │ │ ├── directory-icon │ │ │ ├── directory-icon.html │ │ │ └── directory-icon.ts │ │ ├── event-actions │ │ │ ├── event-actions.html │ │ │ └── event-actions.ts │ │ ├── event-buttons │ │ │ ├── event-buttons.css │ │ │ ├── event-buttons.html │ │ │ └── event-buttons.ts │ │ ├── event-header │ │ │ ├── event-header.css │ │ │ ├── event-header.html │ │ │ └── event-header.ts │ │ ├── event-reactions │ │ │ ├── event-reactions.css │ │ │ ├── event-reactions.html │ │ │ └── event-reactions.ts │ │ ├── event-thread │ │ │ ├── event-thread.css │ │ │ ├── event-thread.html │ │ │ └── event-thread.ts │ │ ├── event │ │ │ ├── event.css │ │ │ ├── event.html │ │ │ └── event.ts │ │ ├── import-sheet │ │ │ ├── import-sheet.html │ │ │ └── import-sheet.ts │ │ ├── label.pipe.ts │ │ ├── label │ │ │ ├── label.css │ │ │ ├── label.html │ │ │ └── label.ts │ │ ├── labels │ │ │ ├── labels.css │ │ │ ├── labels.html │ │ │ └── labels.ts │ │ ├── loading.pipe.ts │ │ ├── media-player │ │ │ ├── media-player.css │ │ │ ├── media-player.html │ │ │ └── media-player.ts │ │ ├── message-bubble │ │ │ ├── message-bubble.component.html │ │ │ ├── message-bubble.component.scss │ │ │ ├── message-bubble.component.spec.ts │ │ │ └── message-bubble.component.ts │ │ ├── mobile-menu │ │ │ ├── mobile-menu.css │ │ │ ├── mobile-menu.html │ │ │ └── mobile-menu.ts │ │ ├── notification-label │ │ │ ├── notification-label.html │ │ │ └── notification-label.ts │ │ ├── password-dialog │ │ │ ├── password-dialog.css │ │ │ ├── password-dialog.html │ │ │ └── password-dialog.ts │ │ ├── profile-actions │ │ │ ├── profile-actions.css │ │ │ ├── profile-actions.html │ │ │ └── profile-actions.ts │ │ ├── profile-header │ │ │ ├── profile-header.css │ │ │ ├── profile-header.html │ │ │ └── profile-header.ts │ │ ├── profile-image-dialog │ │ │ ├── profile-image-dialog.html │ │ │ ├── profile-image-dialog.scss │ │ │ └── profile-image-dialog.ts │ │ ├── profile-image │ │ │ ├── profile-image.html │ │ │ └── profile-image.ts │ │ ├── profile-name │ │ │ ├── profile-name.html │ │ │ └── profile-name.ts │ │ ├── profile-widget │ │ │ ├── profile-widget.css │ │ │ ├── profile-widget.html │ │ │ └── profile-widget.ts │ │ ├── relay-list │ │ │ ├── relay-list.html │ │ │ └── relay-list.ts │ │ ├── relay │ │ │ ├── relay.css │ │ │ ├── relay.html │ │ │ └── relay.ts │ │ ├── relays │ │ │ ├── relays.css │ │ │ ├── relays.html │ │ │ └── relays.ts │ │ ├── reply-list │ │ │ ├── reply-list.css │ │ │ ├── reply-list.html │ │ │ └── reply-list.ts │ │ ├── scroll.directive.ts │ │ ├── status │ │ │ ├── status.component.html │ │ │ ├── status.component.scss │ │ │ ├── status.component.spec.ts │ │ │ └── status.component.ts │ │ ├── tags │ │ │ ├── tags.css │ │ │ ├── tags.html │ │ │ └── tags.ts │ │ ├── time.pipe.ts │ │ ├── user-item │ │ │ ├── user-item.component.html │ │ │ ├── user-item.component.scss │ │ │ ├── user-item.component.spec.ts │ │ │ └── user-item.component.ts │ │ ├── user-list │ │ │ ├── user-list.component.html │ │ │ ├── user-list.component.scss │ │ │ ├── user-list.component.spec.ts │ │ │ └── user-list.component.ts │ │ ├── user-profile │ │ │ ├── user-profile.component.html │ │ │ ├── user-profile.component.scss │ │ │ ├── user-profile.component.spec.ts │ │ │ └── user-profile.component.ts │ │ ├── username.ts │ │ ├── utilities.ts │ │ ├── zap-dialog │ │ │ ├── zap-dialog.component.html │ │ │ ├── zap-dialog.component.scss │ │ │ ├── zap-dialog.component.spec.ts │ │ │ └── zap-dialog.component.ts │ │ ├── zap-qr-code │ │ │ ├── zap-qr-code.component.html │ │ │ ├── zap-qr-code.component.scss │ │ │ ├── zap-qr-code.component.spec.ts │ │ │ └── zap-qr-code.component.ts │ │ └── zappers-list-dialog │ │ │ ├── zappers-list-dialog.component.html │ │ │ ├── zappers-list-dialog.component.scss │ │ │ ├── zappers-list-dialog.component.spec.ts │ │ │ └── zappers-list-dialog.component.ts │ ├── types │ │ ├── database.ts │ │ ├── relay.ts │ │ ├── storage.ts │ │ └── table.ts │ ├── typings.d.ts │ └── workers │ │ ├── nip05.worker.ts │ │ ├── relay.ts │ │ └── relay.worker.ts ├── assets │ ├── .gitkeep │ ├── badge-1024x1024.png │ ├── badge-128x128.png │ ├── badge-512x512.png │ ├── bg.jpg │ ├── blank.png │ ├── blockcore-light-small.png │ ├── blockcore-notes-screenshot.png │ ├── blockcore-notes-social.png │ ├── chat-bg.png │ ├── gradient.jpg │ ├── i18n │ │ ├── de.json │ │ ├── en.json │ │ ├── no.json │ │ └── ru.json │ ├── icon.svg │ ├── icons │ │ ├── icon-120x120.png │ │ ├── icon-128x128.png │ │ ├── icon-128x128.webp │ │ ├── icon-16x16.png │ │ ├── icon-180x180.png │ │ ├── icon-192x192.png │ │ ├── icon-2048x2048.png │ │ ├── icon-256x256.png │ │ ├── icon-256x256.webp │ │ ├── icon-32x32.png │ │ ├── icon-48x48.png │ │ ├── icon-512x512.png │ │ ├── icon-96x96.png │ │ ├── icon-light-128x128.png │ │ └── icon.svg │ ├── logos │ │ ├── youtube.png │ │ └── youtube.svg │ ├── nostr.svg │ ├── nostritch.avif │ ├── nostritch.jpg │ ├── post.svg │ ├── profile-bg.png │ ├── profile.png │ └── shared.worker.js ├── favicon.ico ├── index.html ├── main.ts ├── manifest.webmanifest ├── modules.d.ts ├── styles-theme.scss └── styles.scss ├── store ├── blockcore-notes-feature.png ├── blockcore-notes-people-phone.png ├── blockcore-notes-profile-phone.png └── blockcore-notes-thread-phone.png ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tsconfig.worker.json /.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 | max_line_length = 240 11 | 12 | [*.ts] 13 | quote_type = single 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | This is an Angular 19 project, make sure to always use signals and effects. Also always use most modern TypeScript, with async/await. 2 | 3 | Make sure to use new flow syntax of latest Angular, which is @if instead of *ngIf, @for instead of *ngFor, and @let instead of *ngLet. 4 | 5 | The application uses Angular Material, so make sure to use Angular Material components when possible. 6 | 7 | Make sure to put most styles in the global styles.scss file, and only use component styles for component-specific styles. 8 | 9 | Always use "fetch" for http request instead of HttpClient. -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Web Build and Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: read 9 | pages: write 10 | id-token: write 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 22 23 | cache: 'npm' 24 | 25 | - name: Version 26 | run: | 27 | BASE_VERSION=$(npm run get-version --silent) 28 | # Remove last digit and dot, then append run number 29 | VERSION=$(echo $BASE_VERSION | sed 's/\.[0-9]*$/.'${{ github.run_number }}/) 30 | echo "VERSION=$VERSION" >> $GITHUB_ENV 31 | shell: bash 32 | 33 | - name: Update package.json version 34 | run: npm version $VERSION --no-git-tag-version 35 | shell: bash 36 | 37 | - name: Install dependencies 38 | run: npm install --force 39 | 40 | - name: Build 41 | run: npm run build 42 | 43 | - name: Copy index.html to 404.html 44 | run: cp dist/browser/index.html dist/browser/404.html 45 | 46 | - name: Setup Pages 47 | uses: actions/configure-pages@v3 48 | 49 | - name: Upload artifact 50 | uses: actions/upload-pages-artifact@v3 51 | with: 52 | path: './dist/browser' 53 | 54 | - name: Deploy to GitHub Pages 55 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "tauri-apps.tauri-vscode", 4 | "rust-lang.rust-analyzer", 5 | "angular.ng-template" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.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": "pwa-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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Blockcore 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. 22 | -------------------------------------------------------------------------------- /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-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blockcore-notes" 3 | version = "2.0.0" 4 | description = "Blockcore Notes" 5 | authors = ["you"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "notes_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2", features = [] } 19 | 20 | [dependencies] 21 | tauri = { version = "2", features = [] } 22 | tauri-plugin-opener = "2" 23 | serde = { version = "1", features = ["derive"] } 24 | serde_json = "1" 25 | 26 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": ["main"], 6 | "permissions": [ 7 | "core:default", 8 | "opener:default" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 2 | #[tauri::command] 3 | fn greet(name: &str) -> String { 4 | format!("Hello, {}! You've been greeted from Rust!", name) 5 | } 6 | 7 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 8 | pub fn run() { 9 | tauri::Builder::default() 10 | .plugin(tauri_plugin_opener::init()) 11 | .invoke_handler(tauri::generate_handler![greet]) 12 | .run(tauri::generate_context!()) 13 | .expect("error while running tauri application"); 14 | } 15 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | notes_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "Blockcore Notes", 4 | "version": "2.0.0", 5 | "identifier": "net.blockcore.notes", 6 | "build": { 7 | "beforeDevCommand": "npm run start", 8 | "devUrl": "http://localhost:4200", 9 | "beforeBuildCommand": "npm run build", 10 | "frontendDist": "../dist/browser" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "Blockcore Notes", 16 | "width": 800, 17 | "height": 600 18 | } 19 | ], 20 | "security": { 21 | "csp": null 22 | } 23 | }, 24 | "bundle": { 25 | "active": true, 26 | "targets": [ 27 | "msi", 28 | "appimage", 29 | "dmg" 30 | ], 31 | "icon": [ 32 | "icons/32x32.png", 33 | "icons/128x128.png", 34 | "icons/128x128@2x.png", 35 | "icons/icon.icns", 36 | "icons/icon.ico" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/.well-known/assetlinks.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "relation": ["delegate_permission/common.handle_all_urls"], 3 | "target" : { "namespace": "android_app", "package_name": "net.blockcore.notes", 4 | "sha256_cert_fingerprints": ["72:61:91:36:B4:A0:0A:64:20:4E:4E:83:46:BC:3F:54:0A:E0:FE:3A:11:1C:97:CA:1F:17:C8:A1:8C:53:AE:5F"] } 5 | } 6 | ] -------------------------------------------------------------------------------- /src/app/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { ServiceWorkerModule } from '@angular/service-worker'; 6 | import { AppComponent } from './app'; 7 | import { CheckForUpdateService } from './services/check-for-update'; 8 | 9 | describe('AppComponent', () => { 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [ 13 | BrowserModule, 14 | RouterTestingModule, 15 | CheckForUpdateService, 16 | MatSnackBarModule, 17 | ServiceWorkerModule.register('ngsw-worker.js', { 18 | enabled: false, 19 | // Register the ServiceWorker as soon as the application is stable 20 | // or after 30 seconds (whichever comes first). 21 | registrationStrategy: 'registerWhenStable:30000', 22 | }), 23 | ], 24 | declarations: [AppComponent], 25 | }).compileComponents(); 26 | }); 27 | 28 | it('should create the app', () => { 29 | const fixture = TestBed.createComponent(AppComponent); 30 | const app = fixture.componentInstance; 31 | expect(app).toBeTruthy(); 32 | }); 33 | 34 | // it(`should have as title 'notes'`, () => { 35 | // const fixture = TestBed.createComponent(AppComponent); 36 | // const app = fixture.componentInstance; 37 | // expect(app.title).toEqual('notes'); 38 | // }); 39 | 40 | it('should render title', () => { 41 | const fixture = TestBed.createComponent(AppComponent); 42 | fixture.detectChanges(); 43 | const compiled = fixture.nativeElement as HTMLElement; 44 | expect(compiled.querySelector('.content span')?.textContent).toContain('notes app is running!'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/components/feed-private/feed-private.css: -------------------------------------------------------------------------------- 1 | .loading-container { 2 | text-align: center; 3 | } 4 | 5 | .loading { 6 | margin: auto; 7 | } 8 | 9 | .circle-selection { 10 | margin-bottom: 1em; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/development/development.css: -------------------------------------------------------------------------------- 1 | button { 2 | margin-right: 0.2em; 3 | margin-top: 0.2em; 4 | } -------------------------------------------------------------------------------- /src/app/example/example.css: -------------------------------------------------------------------------------- 1 | .example-viewport { 2 | height: 100%; 3 | min-height: 500px; 4 | width: 100%; 5 | border: 1px solid black; 6 | } 7 | 8 | .example-item { 9 | box-sizing: border-box; 10 | height: 200px; 11 | background-color: purple; 12 | } 13 | 14 | .example-item-content { 15 | overflow: scroll; 16 | width: 100%; 17 | height: 100px; 18 | border: 1px solid green; 19 | } -------------------------------------------------------------------------------- /src/app/example/example.html: -------------------------------------------------------------------------------- 1 |
2 |

This example demonstrates how to do sorting, filtering, virtual scrolling, dynamic updates of complex data within Blockcore Notes.

3 | 4 | 5 |
6 | 7 | {{item.value.id}} 8 | 9 |
{{item.value.content}}
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/app/pages/about/about.css: -------------------------------------------------------------------------------- 1 | .mascot { 2 | width: 100%; 3 | max-width: 1024px; 4 | } 5 | 6 | .logo { 7 | float: left; 8 | margin-bottom: 1em; 9 | margin-right: 1em; 10 | max-width: 128px; 11 | margin-left: -8em; 12 | } 13 | 14 | .help { 15 | padding-left: 10em; 16 | } 17 | 18 | @media only screen and (max-width: 599px) { 19 | .help { 20 | font-size: 0.8em; 21 | padding-left: 8em; 22 | } 23 | 24 | .logo { 25 | max-width: 64px; 26 | margin-left: -6em; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/pages/about/about.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, isDevMode } from '@angular/core'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { MatExpansionModule } from '@angular/material/expansion'; 4 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 5 | import { AboutComponent } from './about'; 6 | import { LicensesComponent } from './licenses/licenses'; 7 | import {} from '@angular/common/http'; 8 | import { RouterModule, Routes } from '@angular/router'; 9 | import { AuthGuardService } from '../services/auth-guard'; 10 | import { LoadingResolverService } from '../services/loading-resolver'; 11 | import { CommonModule } from '@angular/common'; 12 | 13 | const routes: Routes = [ 14 | { 15 | path: '', 16 | component: AboutComponent, 17 | canActivate: [AuthGuardService], 18 | resolve: { 19 | data: LoadingResolverService, 20 | }, 21 | }, 22 | { 23 | path: 'licenses', 24 | component: LicensesComponent, 25 | canActivate: [AuthGuardService], 26 | resolve: { 27 | data: LoadingResolverService, 28 | }, 29 | }, 30 | ]; 31 | 32 | @NgModule({ 33 | imports: [RouterModule.forChild(routes), MatExpansionModule, MatSnackBarModule], 34 | exports: [], 35 | providers: [], 36 | }) 37 | export class AboutModule {} 38 | -------------------------------------------------------------------------------- /src/app/pages/about/about.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { ServiceWorkerModule } from '@angular/service-worker'; 6 | import { AboutComponent } from './about'; 7 | import { AboutModule } from './about.module'; 8 | import { LicensesComponent } from './licenses/licenses'; 9 | 10 | describe('AboutComponent', () => { 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | imports: [AboutModule], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the about component', () => { 18 | const fixture = TestBed.createComponent(AboutComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it('should create the licenses component', () => { 24 | const fixture = TestBed.createComponent(LicensesComponent); 25 | const app = fixture.componentInstance; 26 | expect(app).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/pages/about/about.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MatDialogModule } from '@angular/material/dialog'; 4 | import { MatExpansionModule } from '@angular/material/expansion'; 5 | import { ApplicationState } from 'src/app/services/applicationstate'; 6 | 7 | @Component({ 8 | selector: 'app-about', 9 | standalone: true, 10 | imports: [CommonModule, MatDialogModule, MatExpansionModule], 11 | templateUrl: './about.html', 12 | styleUrls: ['./about.css'], 13 | }) 14 | export class AboutComponent { 15 | constructor(private appState: ApplicationState) {} 16 | 17 | ngOnInit() { 18 | this.appState.showBackButton = true; 19 | this.appState.updateTitle('About'); 20 | this.appState.actions = []; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/pages/about/licenses/licenses.css: -------------------------------------------------------------------------------- 1 | .mascot { 2 | width: 100%; 3 | max-width: 1024px; 4 | } 5 | 6 | .logo { 7 | float: left; 8 | margin-bottom: 1em; 9 | margin-right: 1em; 10 | max-width: 128px; 11 | margin-left: -8em; 12 | } 13 | 14 | .help { 15 | padding-left: 10em; 16 | } 17 | 18 | @media only screen and (max-width: 599px) { 19 | .help { 20 | font-size: 0.8em; 21 | padding-left: 8em; 22 | } 23 | 24 | .logo { 25 | max-width: 64px; 26 | margin-left: -6em; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/pages/about/licenses/licenses.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Blockcore Note is an open source project published under the MIT license. You can view the source code on GitHub

5 | 6 |

Third Party Licenses

7 | 8 |
{{ licenses }}
9 |
10 | -------------------------------------------------------------------------------- /src/app/pages/about/licenses/licenses.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Component, ChangeDetectorRef } from '@angular/core'; 3 | import { tap } from 'rxjs'; 4 | import { ApplicationState } from 'src/app/services/applicationstate'; 5 | 6 | @Component({ 7 | selector: 'app-licenses', 8 | templateUrl: './licenses.html', 9 | styleUrls: ['./licenses.css'], 10 | }) 11 | export class LicensesComponent { 12 | licenses?: string; 13 | 14 | constructor(private cd: ChangeDetectorRef, private appState: ApplicationState, private readonly http: HttpClient) {} 15 | 16 | ngOnInit() { 17 | this.appState.showBackButton = true; 18 | this.appState.updateTitle('Licenses'); 19 | this.appState.actions = []; 20 | 21 | const dataFormatter = (data: string) => `
${data.replace(//g, '>')}
`; 22 | this.showContent('3rdpartylicenses.txt', dataFormatter); 23 | } 24 | 25 | private showContent(contentUrl: string, dataFormatter: (data: string) => string = (data) => data) { 26 | this.http 27 | .get(contentUrl, { responseType: 'text' }) 28 | .pipe( 29 | tap( 30 | (data) => { 31 | this.licenses = data; 32 | // const formattedData = dataFormatter(data); 33 | // this.licenses = this.sanitizer.bypassSecurityTrustHtml(formattedData); 34 | this.cd.markForCheck(); 35 | }, 36 | (error) => { 37 | this.licenses = `Unable to get content (${error.statusText})`; 38 | this.cd.markForCheck(); 39 | } 40 | ) 41 | ) 42 | .subscribe(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/pages/article/article.css: -------------------------------------------------------------------------------- 1 | .thread-event { 2 | margin-left: 27px; 3 | padding-top: 0em; 4 | padding-left: 1em; 5 | } 6 | 7 | .thread-content { 8 | margin-left: 27px; 9 | padding-left: 1em; 10 | display: block; 11 | } 12 | 13 | .events { 14 | padding: 0 !important; 15 | } 16 | 17 | .thread-actions { 18 | margin-left: 27px; 19 | padding-left: 1em; 20 | padding-bottom: 0.3em; 21 | } 22 | 23 | .button-card { 24 | margin-bottom: 0.4em; 25 | } 26 | 27 | .parent-event { 28 | } 29 | 30 | .has-root { 31 | margin-left: 27px; 32 | padding-left: 1em; 33 | } 34 | 35 | .is-root { 36 | margin-left: 0em; 37 | padding-left: 0em; 38 | border-left: 2px solid transparent !important; 39 | } 40 | 41 | .options-container { 42 | margin-bottom: 1em; 43 | } 44 | -------------------------------------------------------------------------------- /src/app/pages/badge/badge.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/app/pages/badge/badge.css -------------------------------------------------------------------------------- /src/app/pages/badge/badge.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | {{ 'Badge.CreatedBy' | translate }}:  5 |
6 | Last updated: 7 | 8 |

9 | 10 | 11 |

12 | 13 |
14 | 15 | {{ 'Badge.PubKeyDescription' | translate }} 16 | 17 | 18 |
19 | 20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /src/app/pages/badges/badges.css: -------------------------------------------------------------------------------- 1 | .badge-header { 2 | margin-bottom: 1em; 3 | } 4 | 5 | .badges { 6 | display: flex; 7 | flex-wrap: wrap; 8 | flex-direction: row; 9 | gap: 1em; 10 | padding-top: 1em; 11 | } 12 | 13 | .options-buttons button { 14 | margin-right: 1em; 15 | margin-bottom: 1em; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/pages/chat/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | arrow_back_ios 14 | {{sidebarTitles.chat}} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{sidebarTitles.user}} 23 | close 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/app/pages/chat/chat.scss: -------------------------------------------------------------------------------- 1 | .drawer-container { 2 | height: 100%; 3 | z-index: 9999; 4 | overflow: visible !important; 5 | } 6 | 7 | .sidebar { 8 | &--message { 9 | width: 100%; 10 | } 11 | 12 | &--user { 13 | max-width: 360px; 14 | width: 100%; 15 | } 16 | 17 | .mat-drawer-inner-container { 18 | display: flex; 19 | flex-direction: column; 20 | flex: 1; 21 | z-index: 9999; 22 | } 23 | } 24 | 25 | .zIndexTop { 26 | z-index: 10; 27 | } 28 | 29 | .spacer { 30 | flex: 1; 31 | } 32 | -------------------------------------------------------------------------------- /src/app/pages/circles/circles.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | margin: auto; 3 | } 4 | 5 | .input-full-width { 6 | width: 100% !important; 7 | } 8 | 9 | .search { 10 | margin-top: 1em; 11 | padding: 1em 1em 0em 1em; 12 | margin-bottom: 1em; 13 | border-radius: 10px; 14 | } 15 | 16 | .circle-button { 17 | } 18 | 19 | .circle-button-icon { 20 | } 21 | 22 | .circle-container { 23 | display: flex; 24 | flex-direction: row; 25 | flex-wrap: nowrap; 26 | justify-content: center; 27 | align-content: center; 28 | align-items: center; 29 | column-gap: 1em; 30 | 31 | margin-top: 1em; 32 | padding: 1em; 33 | margin-bottom: 1em; 34 | border-radius: 10px; 35 | } 36 | 37 | .circle-item:nth-child(1) { 38 | order: 0; 39 | flex: 0 1 auto; 40 | align-self: auto; 41 | } 42 | 43 | .circle-item:nth-child(2) { 44 | order: 0; 45 | flex: 1 1 auto; 46 | align-self: auto; 47 | } 48 | 49 | .circle-item:nth-child(3) { 50 | order: 0; 51 | flex: 0 1 auto; 52 | align-self: auto; 53 | } 54 | 55 | .circle-actions button { 56 | margin-right: 0.4em; 57 | margin-bottom: 0.4em; 58 | } 59 | -------------------------------------------------------------------------------- /src/app/pages/connect/consent-dialog/consent-dialog.css: -------------------------------------------------------------------------------- 1 | .input-full-width { 2 | width: 100% !important; 3 | } 4 | 5 | .dialog { 6 | max-width: 800px; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/pages/connect/consent-dialog/consent-dialog.html: -------------------------------------------------------------------------------- 1 |
2 |

Give consent

3 |
4 | 5 |

6 | Nostr is a decentralized and distributed network of relays that relays data by users. That means there is no centralized service where filtering or censoring is occurring. You may be exposed to content that will be disturbing 7 | and against your morality and world views. 8 |

9 |

Content you publish are your responsibility and you cannot undo/delete after you publish. Don't publish or share a note (post) you wouldn't say to your neighbour.

10 | 11 |

Blockcore has no involvement in the type of content being produced and shown to you in this app.

12 | 13 |

You accept that all usage of Blockcore Notes, Blockcore infrastructure and software must be the result of peaceful voluntary human interactions.

14 | 15 |

If you're young and still living with your parents, consider asking them for permission before you continue.

16 | 17 |

To continue you must agree with our Privacy Policy and Terms & Conditions.

18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /src/app/pages/connect/consent-dialog/consent-dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { MatButtonModule } from '@angular/material/button'; 3 | import { MatDialog, MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; 4 | 5 | @Component({ 6 | selector: 'add-consent-dialog', 7 | templateUrl: 'consent-dialog.html', 8 | styleUrls: ['consent-dialog.css'], 9 | imports: [MatDialogModule, MatButtonModule], 10 | }) 11 | export class ConsentDialog { 12 | constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public consent: boolean) {} 13 | 14 | onNoClick(): void { 15 | this.consent = false; 16 | this.dialogRef.close(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/pages/connect/create/create.css: -------------------------------------------------------------------------------- 1 | .public-key { 2 | margin-bottom: 1em; 3 | word-wrap: break-word; 4 | } 5 | 6 | .error { 7 | margin-bottom: 1em; 8 | color: red; 9 | } 10 | 11 | .full-button { 12 | width: 100%; 13 | border-radius: 10px; 14 | padding: 2em; 15 | } 16 | 17 | .description { 18 | margin-top: 0.4em; 19 | text-align: center; 20 | font-size: 0.85em; 21 | margin-bottom: 1.4em; 22 | } 23 | 24 | .start-button { 25 | margin-right: 0.62em; 26 | } 27 | 28 | .connect-action { 29 | text-align: right; 30 | } 31 | 32 | .recovery-phrase { 33 | font-size: 1.4em; 34 | word-spacing: 0.8em; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/pages/connect/key/key.css: -------------------------------------------------------------------------------- 1 | .public-key { 2 | margin-bottom: 1em; 3 | word-wrap: break-word; 4 | } 5 | 6 | .error { 7 | margin-bottom: 1em; 8 | color: red; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/pages/connect/key/qr-scan-dialog/qr-scan.css: -------------------------------------------------------------------------------- 1 | .qr-scan { 2 | position: absolute !important; 3 | left: 0; 4 | right: 0; 5 | top: 0; 6 | bottom: 0; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | .full-screen-modal .mat-dialog-container { 12 | } 13 | 14 | .camera-button { 15 | margin: 2em; 16 | left: 2em; 17 | position: absolute; 18 | } 19 | 20 | [dir=rtl] 21 | .camera-button { 22 | margin: 2em; 23 | right: 2em; 24 | position: absolute; 25 | left: auto; 26 | } 27 | 28 | .camera-close-button { 29 | margin: 2em; 30 | position: absolute; 31 | right: 2em; 32 | top: 2em; 33 | } 34 | 35 | [dir=rtl] 36 | .camera-close-button { 37 | margin: 2em; 38 | position: absolute; 39 | left: 2em; 40 | right: auto; 41 | top: 2em; 42 | } 43 | 44 | .camera-label { 45 | font-size: 2em; 46 | color: rgba(1, 1, 1, 0.5); 47 | margin-left: auto; 48 | margin-right: auto; 49 | } 50 | 51 | .camera-error { 52 | color: #a83222; 53 | } 54 | 55 | /* .mat-dialog-container { 56 | padding: 0 !important; 57 | } */ 58 | -------------------------------------------------------------------------------- /src/app/pages/connect/key/qr-scan-dialog/qr-scan.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 7 | 8 | 11 | 12 | 15 | 16 | 19 | 20 |
{{ error }}
21 |
22 | -------------------------------------------------------------------------------- /src/app/pages/connect/login/login.css: -------------------------------------------------------------------------------- 1 | .public-key { 2 | margin-bottom: 1em; 3 | word-wrap: break-word; 4 | } 5 | 6 | .error { 7 | margin-bottom: 1em; 8 | color: red; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/pages/editor-badges/editor.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin-top: 0; 3 | } 4 | 5 | .input-full-width { 6 | width: 100% !important; 7 | } 8 | 9 | .toolbar { 10 | display: flex; 11 | margin-bottom: 5px; 12 | margin-top: 3px; 13 | } 14 | 15 | .toolbar-icon { 16 | cursor: pointer; 17 | } 18 | 19 | .toolbar-icon:hover { 20 | color: #9c27b0; 21 | } 22 | 23 | .margin-right { 24 | margin-right: 5px; 25 | } 26 | 27 | .picker { 28 | display: block; 29 | position: fixed; 30 | z-index: 3; 31 | } 32 | 33 | .note-type { 34 | text-align: right; 35 | margin-bottom: 1em; 36 | } 37 | 38 | @media only screen and (max-width: 599px) { 39 | h1 { 40 | font-size: 1.8em; 41 | } 42 | } 43 | 44 | .text-editor { 45 | border: 1px lightgrey solid; 46 | min-height: 88px; 47 | padding: 1em; 48 | } 49 | -------------------------------------------------------------------------------- /src/app/pages/editor/editor.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin-top: 0; 3 | } 4 | 5 | .input-full-width { 6 | width: 100% !important; 7 | } 8 | 9 | .toolbar { 10 | display: flex; 11 | margin-bottom: 5px; 12 | margin-top: 3px; 13 | } 14 | 15 | .toolbar-icon { 16 | cursor: pointer; 17 | } 18 | 19 | .toolbar-icon:hover { 20 | color: #9c27b0; 21 | } 22 | 23 | .margin-right { 24 | margin-right: 5px; 25 | } 26 | 27 | .picker { 28 | display: block; 29 | position: fixed; 30 | z-index: 3; 31 | } 32 | 33 | .note-type { 34 | text-align: right; 35 | margin-bottom: 1em; 36 | } 37 | 38 | @media only screen and (max-width: 599px) { 39 | h1 { 40 | font-size: 1.8em; 41 | } 42 | } 43 | 44 | .text-editor { 45 | border: 1px lightgrey solid; 46 | min-height: 88px; 47 | padding: 1em; 48 | } 49 | 50 | .footer { 51 | display: flex; 52 | justify-content: space-between; 53 | } 54 | -------------------------------------------------------------------------------- /src/app/pages/feed/feed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | home 5 |  Home 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 22 | 23 | 35 | 36 | 45 | 46 | -------------------------------------------------------------------------------- /src/app/pages/files/files.component.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | margin: auto; 3 | margin-top: 2em; 4 | margin-bottom: 2em; 5 | } 6 | 7 | .input-full-width { 8 | width: 100% !important; 9 | } 10 | 11 | .search { 12 | margin-top: 1em; 13 | padding: 1em 1em 0em 1em; 14 | margin-bottom: 1em; 15 | border-radius: 10px; 16 | } 17 | 18 | .people-list { 19 | display: flex; 20 | flex-direction: row; 21 | flex-wrap: wrap; 22 | gap: 1em; 23 | margin-top: 1em; 24 | } 25 | 26 | .people-list-1 { 27 | display: block; 28 | } 29 | 30 | .people-list-2 { 31 | display: block; 32 | } 33 | 34 | /* .people-list > * { 35 | flex: 1 1 240px; 36 | } */ 37 | 38 | .people-card { 39 | flex: 1 1 240px; 40 | } 41 | 42 | .profile-banner { 43 | min-height: 240px; 44 | /* margin-bottom: 30em; 45 | position: fixed; */ 46 | 47 | background-repeat: no-repeat; 48 | background-position: center; 49 | background-size: cover; 50 | margin-top: -2em; 51 | } 52 | 53 | .people-actions button { 54 | margin-right: 0.4em; 55 | margin-bottom: 0.4em; 56 | } 57 | -------------------------------------------------------------------------------- /src/app/pages/files/import-follow-dialog/import-follow-dialog.html: -------------------------------------------------------------------------------- 1 |

Import complete following list

2 |
3 |

Choose if you want to import from your latest contacts event or from file (backup) on your device.

4 | 5 | 6 |

7 | 8 |

9 |

10 | Import from file: 11 | 12 |

13 | 14 | 19 |
20 | 21 |
22 | 23 | 24 |
25 | -------------------------------------------------------------------------------- /src/app/pages/files/import-follow-dialog/import-follow-dialog.scss: -------------------------------------------------------------------------------- 1 | .input-full-width { 2 | width: 100% !important; 3 | } 4 | 5 | // .mat-dialog-content { 6 | // padding: 20px 24px 0px 24px !important; 7 | // } 8 | 9 | // .mat-dialog-actions { 10 | // padding: 10px 24px 24px 24px; 11 | // } 12 | -------------------------------------------------------------------------------- /src/app/pages/files/import-follow-dialog/import-follow-dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatDialog, MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; 5 | import { MatInputModule } from '@angular/material/input'; 6 | import { EventService } from 'src/app/services/event'; 7 | import { NostrEventDocument } from 'src/app/services/interfaces'; 8 | 9 | export interface ImportFollowDialogData { 10 | pubkey: string; 11 | pubkeys: string[]; 12 | import: boolean; 13 | } 14 | 15 | @Component({ 16 | selector: 'import-follow-dialog', 17 | templateUrl: 'import-follow-dialog.html', 18 | styleUrls: ['import-follow-dialog.scss'], 19 | imports: [MatDialogModule, MatButtonModule, FormsModule, MatInputModule], 20 | }) 21 | export class ImportFollowDialog { 22 | constructor(private eventService: EventService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: ImportFollowDialogData) {} 23 | 24 | onNoClick(): void { 25 | this.data.pubkey = ''; 26 | this.dialogRef.close(); 27 | } 28 | 29 | import() { 30 | this.data.import = true; 31 | this.dialogRef.close(this.data); 32 | } 33 | 34 | onFileSelected(event: any) { 35 | var file = event.target.files[0]; 36 | var reader = new FileReader(); 37 | reader.onload = () => { 38 | const event = JSON.parse(reader.result as string) as NostrEventDocument; 39 | const tags = this.eventService.pTags(event).map((v) => v[1]); 40 | 41 | this.data.pubkeys = tags; 42 | this.dialogRef.close(this.data); 43 | }; 44 | reader.readAsText(file); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/pages/followers/followers.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/app/pages/followers/followers.css -------------------------------------------------------------------------------- /src/app/pages/followers/followers.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |
NOT IMPLEMENTED. Check "Following".
7 | 10 |
11 | -------------------------------------------------------------------------------- /src/app/pages/following/following.css: -------------------------------------------------------------------------------- 1 | .following-item { 2 | height: 64px; 3 | } 4 | 5 | .example-header, 6 | .example-footer { 7 | height: 100px; 8 | background: lightgray; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/pages/following/following.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 7 | 10 | 11 |
12 | 13 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 | 29 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /src/app/pages/home/relay.json: -------------------------------------------------------------------------------- 1 | { "enabled": true, 2 | "public": true, 3 | "url": "wss://nos.lol", 4 | "type": 1, 5 | "modified": 1743587934, 6 | "status": 1, 7 | 8 | "nip11": { 9 | "contact": "https://wikifreedia.xyz/nos.lol/", 10 | "description": "Generally accepts notes, except spammy ones.", 11 | "limitation": { "max_limit": 500, "max_message_length": 131072, "max_subscriptions": 20 }, 12 | "name": "nos.lol", 13 | "negentropy": 1, 14 | "pubkey": "c5fadeb5d90d68baffc631455a07ca340ccf1e31110955e16d45eb5f87147cd9", 15 | "software": "git+https://github.com/hoytech/strfry.git", 16 | "supported_nips": [ 1, 2, 4, 9, 11, 22, 28, 40, 70, 77 ], 17 | "version": "1.0.4" }, 18 | "eventcount": 102 19 | 20 | } -------------------------------------------------------------------------------- /src/app/pages/logout/logout.html: -------------------------------------------------------------------------------- 1 | Loading... -------------------------------------------------------------------------------- /src/app/pages/logout/logout.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { ApplicationState } from '../../services/applicationstate'; 4 | import { AuthenticationService } from '../../services/authentication'; 5 | import { RelayService } from '../../services/relay'; 6 | import { StorageService } from '../../services/storage'; 7 | import { UIService } from '../../services/ui'; 8 | 9 | @Component({ 10 | selector: 'app-logout', 11 | templateUrl: './logout.html', 12 | }) 13 | export class LogoutComponent { 14 | constructor(private relayService: RelayService, private ui: UIService, private appState: ApplicationState, private db: StorageService, private authService: AuthenticationService, private router: Router) {} 15 | 16 | ngOnInit() { 17 | // Make sure we terminate all the relays (and Web Workers). 18 | this.relayService.terminateAll(); 19 | 20 | this.ui.clearAll(); 21 | 22 | this.db.close(); 23 | this.authService.logout(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/pages/message/message.css: -------------------------------------------------------------------------------- 1 | .drawer-container { 2 | height: 100%; 3 | z-index: 9999; 4 | overflow: visible !important; 5 | } 6 | 7 | .sidebar--message { 8 | width: 100%; 9 | } 10 | 11 | .sidebar--user { 12 | max-width: 360px; 13 | width: 100%; 14 | } 15 | 16 | .mat-drawer-inner-container { 17 | /* display: flex; 18 | flex-direction: column; 19 | flex: 1; 20 | z-index: 9999; */ 21 | } 22 | 23 | .zIndexTop { 24 | z-index: 10; 25 | } 26 | 27 | .spacer { 28 | flex: 1; 29 | } 30 | -------------------------------------------------------------------------------- /src/app/pages/message/message.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31 | -------------------------------------------------------------------------------- /src/app/pages/message/message.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectorRef, ViewChild, ViewEncapsulation } from '@angular/core'; 2 | import { MatSidenav } from '@angular/material/sidenav'; 3 | import { ApplicationState } from '../../services/applicationstate'; 4 | import { ChatDetailComponent } from '../../shared/chat-detail/chat-detail.component'; 5 | @Component({ 6 | selector: 'app-message', 7 | templateUrl: './message.html', 8 | styleUrls: ['./message.css'], 9 | encapsulation: ViewEncapsulation.None, 10 | imports: [ChatDetailComponent], 11 | }) 12 | export class MessageComponent { 13 | @ViewChild('chatSidebar', { static: false }) chatSidebar!: MatSidenav; 14 | @ViewChild('userSidebar', { static: false }) userSidebar!: MatSidenav; 15 | 16 | constructor(private appState: ApplicationState) {} 17 | 18 | sidebarTitles = { 19 | user: '', 20 | chat: '', 21 | }; 22 | 23 | open = { 24 | me: this, 25 | userSideBar: function (title: string = '') { 26 | this.me.userSidebar.open(); 27 | this.me.sidebarTitles.user = title; 28 | }, 29 | chatSideBar: function (title: string = '') { 30 | this.me.chatSidebar.open(); 31 | this.me.userSidebar.close(); 32 | this.me.sidebarTitles.chat = title; 33 | }, 34 | }; 35 | 36 | async ngOnInit() { 37 | this.appState.updateTitle('@Milad'); 38 | this.appState.goBack = true; 39 | this.appState.showBackButton = true; 40 | this.appState.actions = []; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/pages/messages/messages.css: -------------------------------------------------------------------------------- 1 | .drawer-container { 2 | height: 100%; 3 | z-index: 9999; 4 | overflow: visible !important; 5 | } 6 | 7 | .sidebar--message { 8 | width: 100%; 9 | } 10 | 11 | .sidebar--user { 12 | max-width: 360px; 13 | width: 100%; 14 | } 15 | 16 | .mat-drawer-inner-container { 17 | /* display: flex; 18 | flex-direction: column; 19 | flex: 1; 20 | z-index: 9999; */ 21 | } 22 | 23 | .zIndexTop { 24 | z-index: 10; 25 | } 26 | 27 | .spacer { 28 | flex: 1; 29 | } 30 | -------------------------------------------------------------------------------- /src/app/pages/messages/messages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
{{event | json}}
4 | 5 | 33 | -------------------------------------------------------------------------------- /src/app/pages/note/note.css: -------------------------------------------------------------------------------- 1 | .thread-event { 2 | margin-left: 27px; 3 | padding-top: 0em; 4 | padding-left: 1em; 5 | } 6 | 7 | .thread-content { 8 | margin-left: 27px; 9 | padding-left: 1em; 10 | display: block; 11 | } 12 | 13 | .events { 14 | padding: 0 !important; 15 | } 16 | 17 | .thread-actions { 18 | margin-left: 27px; 19 | padding-left: 1em; 20 | padding-bottom: 0.3em; 21 | } 22 | 23 | .button-card { 24 | margin-bottom: 0.4em; 25 | } 26 | 27 | .expand-button { 28 | margin-bottom: 0.4em; 29 | } 30 | 31 | .parent-event { 32 | } 33 | 34 | .has-root { 35 | margin-left: 27px; 36 | padding-left: 1em; 37 | } 38 | 39 | .is-root { 40 | margin-left: 0em; 41 | padding-left: 0em; 42 | border-left: 2px solid transparent !important; 43 | } 44 | 45 | .options-container { 46 | margin-bottom: 1em; 47 | } 48 | -------------------------------------------------------------------------------- /src/app/pages/notes/notes.css: -------------------------------------------------------------------------------- 1 | .events { 2 | width: 480px; 3 | max-width: 480px; 4 | } 5 | 6 | .notes { 7 | display: flex; 8 | flex-direction: row; 9 | flex-wrap: wrap; 10 | gap: 1em; 11 | margin-top: 1em; 12 | align-items: flex-start; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/pages/notes/notes.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
{{ 'Notes.Title' | translate }}
4 | 5 |
6 | 7 |
8 | {{ 'Notes.Posted' | translate }}: {{ note.created_at | ago }}, {{ 'Notes.Saved' | translate }}: {{ note.saved | ago }} 9 | 10 |
11 | 12 | 13 | 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /src/app/pages/notifications/notifications.css: -------------------------------------------------------------------------------- 1 | .notification-card { 2 | margin-bottom: 0.2em; 3 | } 4 | 5 | .notification-entry { 6 | display: flex; 7 | flex-direction: row; 8 | } 9 | 10 | .notification-mesage { 11 | flex-grow: 2; 12 | } 13 | 14 | .notification-date { 15 | text-align: right; 16 | flex-grow: 1; 17 | /* color: rgba(255, 255, 255, 0.5); */ 18 | } 19 | 20 | .notification-message-seen { 21 | font-style: italic; 22 | /* color: rgba(255, 255, 255, 0.5); */ 23 | } 24 | -------------------------------------------------------------------------------- /src/app/pages/notifications/notifications.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | {{ 'Notifications.Options' | translate}} 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 | -------------------------------------------------------------------------------- /src/app/pages/people/import-follow-dialog/import-follow-dialog.html: -------------------------------------------------------------------------------- 1 |

Import complete following list

2 |
3 |

Choose if you want to import from your latest contacts event or from file (backup) on your device.

4 | 5 | 6 |

7 | 8 |

9 |

10 | Import from file: 11 | 12 |

13 | 14 | 19 |
20 | 21 |
22 | 23 | 24 |
25 | -------------------------------------------------------------------------------- /src/app/pages/people/import-follow-dialog/import-follow-dialog.scss: -------------------------------------------------------------------------------- 1 | .input-full-width { 2 | width: 100% !important; 3 | } 4 | 5 | // .mat-dialog-content { 6 | // padding: 20px 24px 0px 24px !important; 7 | // } 8 | 9 | // .mat-dialog-actions { 10 | // padding: 10px 24px 24px 24px; 11 | // } 12 | -------------------------------------------------------------------------------- /src/app/pages/people/import-follow-dialog/import-follow-dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatDialog, MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; 5 | import { MatInputModule } from '@angular/material/input'; 6 | import { EventService } from 'src/app/services/event'; 7 | import { NostrEventDocument } from 'src/app/services/interfaces'; 8 | 9 | export interface ImportFollowDialogData { 10 | pubkey: string; 11 | pubkeys: string[]; 12 | import: boolean; 13 | } 14 | 15 | @Component({ 16 | selector: 'import-follow-dialog', 17 | templateUrl: 'import-follow-dialog.html', 18 | styleUrls: ['import-follow-dialog.scss'], 19 | imports: [MatDialogModule, MatButtonModule, FormsModule, MatInputModule], 20 | }) 21 | export class ImportFollowDialog { 22 | constructor(private eventService: EventService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: ImportFollowDialogData) {} 23 | 24 | onNoClick(): void { 25 | this.data.pubkey = ''; 26 | this.dialogRef.close(); 27 | } 28 | 29 | import() { 30 | this.data.import = true; 31 | this.dialogRef.close(this.data); 32 | } 33 | 34 | onFileSelected(event: any) { 35 | var file = event.target.files[0]; 36 | var reader = new FileReader(); 37 | reader.onload = () => { 38 | const event = JSON.parse(reader.result as string) as NostrEventDocument; 39 | const tags = this.eventService.pTags(event).map((v) => v[1]); 40 | 41 | this.data.pubkeys = tags; 42 | this.dialogRef.close(this.data); 43 | }; 44 | reader.readAsText(file); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/pages/people/people.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | margin: auto; 3 | margin-top: 2em; 4 | margin-bottom: 2em; 5 | } 6 | 7 | .input-full-width { 8 | width: 100% !important; 9 | } 10 | 11 | .search { 12 | margin-top: 1em; 13 | padding: 1em 1em 0em 1em; 14 | margin-bottom: 1em; 15 | border-radius: 10px; 16 | } 17 | 18 | .people-list { 19 | display: flex; 20 | flex-direction: row; 21 | flex-wrap: wrap; 22 | gap: 1em; 23 | margin-top: 1em; 24 | } 25 | 26 | .people-list-1 { 27 | display: block; 28 | } 29 | 30 | .people-list-2 { 31 | display: block; 32 | } 33 | 34 | /* .people-list > * { 35 | flex: 1 1 240px; 36 | } */ 37 | 38 | .people-card { 39 | flex: 1 1 240px; 40 | } 41 | 42 | .profile-banner { 43 | min-height: 240px; 44 | /* margin-bottom: 30em; 45 | position: fixed; */ 46 | 47 | background-repeat: no-repeat; 48 | background-position: center; 49 | background-size: cover; 50 | margin-top: -2em; 51 | } 52 | 53 | .people-actions button { 54 | margin-right: 0.4em; 55 | margin-bottom: 0.4em; 56 | } 57 | -------------------------------------------------------------------------------- /src/app/pages/profile/profile.css: -------------------------------------------------------------------------------- 1 | .example-full-width { 2 | width: 100%; 3 | } 4 | 5 | td { 6 | padding-right: 2px; 7 | } 8 | 9 | .file-name { 10 | margin-left: 1rem; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/pages/queue/add-media-dialog/add-media-dialog.css: -------------------------------------------------------------------------------- 1 | .input-full-width { 2 | width: 100% !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/pages/queue/add-media-dialog/add-media-dialog.html: -------------------------------------------------------------------------------- 1 |
2 |

Add media to queue

3 |
4 | 5 | dns 6 | URL 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/app/pages/queue/add-media-dialog/add-media-dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { MatDialog, MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; 4 | import { MatFormFieldModule } from '@angular/material/form-field'; 5 | import { MatInputModule } from '@angular/material/input'; 6 | 7 | export interface AddMediaDialogData { 8 | url: string; 9 | } 10 | 11 | @Component({ 12 | selector: 'add-media-dialog', 13 | templateUrl: 'add-media-dialog.html', 14 | styleUrls: ['add-media-dialog.css'], 15 | imports: [MatFormFieldModule, MatInputModule, FormsModule, MatDialogModule], 16 | }) 17 | export class AddMediaDialog { 18 | constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AddMediaDialogData) {} 19 | 20 | onNoClick(): void { 21 | this.data.url = ''; 22 | this.dialogRef.close(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/pages/queue/queue.css: -------------------------------------------------------------------------------- 1 | .queue-artwork { 2 | border-radius: 0 !important; 3 | } -------------------------------------------------------------------------------- /src/app/pages/queue/queue.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | {{i+1}}. {{item.title}} 7 | {{item.artist}} 8 | 9 | 12 | 13 | 14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/app/pages/relays/relays.css: -------------------------------------------------------------------------------- 1 | .example-action-buttons { 2 | padding-bottom: 20px; 3 | } 4 | 5 | .example-headers-align .mat-expansion-panel-header-description { 6 | justify-content: space-between; 7 | align-items: center; 8 | } 9 | 10 | .example-headers-align .mat-mdc-form-field + .mat-mdc-form-field { 11 | margin-left: 8px; 12 | } 13 | 14 | .online { 15 | margin-left: 0.2em; 16 | margin-bottom: -0.2em; 17 | } 18 | 19 | .relay-status-0 { 20 | color: silver; 21 | } 22 | 23 | .relay-status-1 { 24 | color: green; 25 | } 26 | 27 | .relay-status-2 { 28 | color: orange; 29 | } 30 | 31 | .relay-status-3 { 32 | color: red; 33 | } 34 | 35 | .relay-status-4 { 36 | color: rgb(49, 49, 210); 37 | } 38 | 39 | .relay-read-disabled { 40 | color: rgb(49, 49, 210) !important; 41 | } 42 | 43 | .relay-disabled { 44 | color: rgb(234, 136, 9) !important; 45 | } 46 | 47 | .primary-relay { 48 | color: rgb(198, 3, 181); 49 | } 50 | 51 | .relay-options { 52 | margin-top: 0.4em; 53 | margin-bottom: 0.2em; 54 | } 55 | 56 | .settings-action-buttons { 57 | padding-top: 0.8em; 58 | padding-bottom: 1em; 59 | } 60 | 61 | .settings-action-buttons button { 62 | margin-bottom: 1em; 63 | margin-right: 1em; 64 | } 65 | 66 | /* When changing the sidenav-content to flex, the toolbar does not render properly, so a minor hack is needed. */ 67 | @media only screen and (max-width: 599px) { 68 | .settings-action-buttons button { 69 | width: 100%; 70 | margin-right: 0; 71 | } 72 | 73 | .mat-expansion-panel-header-title { 74 | flex-grow: 2 !important; 75 | } 76 | 77 | .mat-expansion-panel-header-description { 78 | flex-grow: 1 !important; 79 | } 80 | } 81 | 82 | .relay-button { 83 | margin-top: 0.8em; 84 | } 85 | .options-slider { 86 | margin-left: 1em; 87 | } 88 | -------------------------------------------------------------------------------- /src/app/pages/relays/relays.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | {{'Relays.Options' | translate}} 7 | 8 | 9 | 10 |
11 | 13 | 14 |
15 |

17 | 18 |
19 |
20 |
21 |
22 | 23 | 24 |
25 | -------------------------------------------------------------------------------- /src/app/pages/settings/settings.css: -------------------------------------------------------------------------------- 1 | .example-action-buttons { 2 | padding-bottom: 20px; 3 | } 4 | 5 | .example-headers-align .mat-expansion-panel-header-description { 6 | justify-content: space-between; 7 | align-items: center; 8 | } 9 | 10 | .example-headers-align .mat-mdc-form-field + .mat-mdc-form-field { 11 | margin-left: 8px; 12 | } 13 | 14 | .online { 15 | margin-left: 0.2em; 16 | margin-bottom: -0.2em; 17 | } 18 | 19 | .relay-status-0 { 20 | color: silver; 21 | } 22 | 23 | .relay-status-1 { 24 | color: green; 25 | } 26 | 27 | .relay-status-2 { 28 | color: orange; 29 | } 30 | 31 | .relay-status-3 { 32 | color: red; 33 | } 34 | 35 | .relay-status-4 { 36 | color: rgb(49, 49, 210); 37 | } 38 | 39 | .relay-read-disabled { 40 | color: rgb(49, 49, 210) !important; 41 | } 42 | 43 | .relay-disabled { 44 | color: rgb(234, 136, 9) !important; 45 | } 46 | 47 | .primary-relay { 48 | color: rgb(198, 3, 181); 49 | } 50 | 51 | .relay-options { 52 | margin-top: 0.4em; 53 | margin-bottom: 0.2em; 54 | } 55 | 56 | .settings-action-buttons { 57 | padding-top: 0.8em; 58 | padding-bottom: 1em; 59 | } 60 | 61 | .settings-action-buttons button { 62 | margin-bottom: 1em; 63 | margin-right: 1em; 64 | } 65 | 66 | /* When changing the sidenav-content to flex, the toolbar does not render properly, so a minor hack is needed. */ 67 | @media only screen and (max-width: 599px) { 68 | .settings-action-buttons button { 69 | width: 100%; 70 | margin-right: 0; 71 | } 72 | 73 | .mat-expansion-panel-header-title { 74 | flex-grow: 2 !important; 75 | } 76 | 77 | .mat-expansion-panel-header-description { 78 | flex-grow: 1 !important; 79 | } 80 | } 81 | 82 | .relay-button { 83 | margin-top: 0.8em; 84 | } 85 | .options-slider { 86 | margin-left: 1em; 87 | } 88 | -------------------------------------------------------------------------------- /src/app/pages/user/user.css: -------------------------------------------------------------------------------- 1 | .profile-name { 2 | display: inline-block; 3 | } 4 | 5 | .feed-page { 6 | min-height: 1200px; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/schemas/nostr-event.ts: -------------------------------------------------------------------------------- 1 | // import Schema from 'joi'; 2 | 3 | // TODO: Do something similar to nostream implementation of schema validation: https://github.com/Cameri/nostream/tree/main/src/schemas 4 | 5 | // TODO: Also make another consideration if we should use ajv instead of joi? 6 | -------------------------------------------------------------------------------- /src/app/services/app-update.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material/snack-bar'; 3 | import { SwUpdate } from '@angular/service-worker'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class AppUpdateService { 9 | constructor(private readonly updates: SwUpdate, private snackBar: MatSnackBar) { 10 | this.updateClient(); 11 | } 12 | 13 | updateClient() { 14 | this.updates.versionUpdates.subscribe((evt) => { 15 | switch (evt.type) { 16 | case 'VERSION_DETECTED': 17 | console.log(`Downloading new app version: ${evt.version.hash}`); 18 | break; 19 | case 'VERSION_READY': 20 | console.log(`Current app version: ${evt.currentVersion.hash}`); 21 | console.log(`New app version ready for use: ${evt.latestVersion.hash}`); 22 | // this.showAppUpdateAlert(); 23 | this.doAppUpdate(); 24 | break; 25 | case 'VERSION_INSTALLATION_FAILED': 26 | console.log(`Failed to install app version '${evt.version.hash}': ${evt.error}`); 27 | break; 28 | } 29 | }); 30 | } 31 | 32 | showAppUpdateAlert() { 33 | let sb = this.snackBar.open('App Update available!', 'Update', { 34 | horizontalPosition: 'center', 35 | verticalPosition: 'bottom', 36 | }); 37 | sb.onAction().subscribe(() => { 38 | this.doAppUpdate(); 39 | }); 40 | } 41 | 42 | doAppUpdate() { 43 | this.updates.activateUpdate().then(() => document.location.reload()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/services/auth-guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { ApplicationState } from './applicationstate'; 4 | import { AuthenticationService, UserInfo } from './authentication'; 5 | 6 | @Injectable() 7 | export class AuthGuardService { 8 | constructor(public appState: ApplicationState, private authService: AuthenticationService, public router: Router) {} 9 | canActivate() { 10 | if (this.authService.authInfo$.getValue().authenticated()) { 11 | return true; 12 | } 13 | 14 | return this.authService.getAuthInfo().then((authInfo: UserInfo) => { 15 | if (authInfo.authenticated()) { 16 | return true; 17 | } else { 18 | this.router.navigateByUrl('/connect'); 19 | return false; 20 | } 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/services/cache.ts: -------------------------------------------------------------------------------- 1 | interface CacheContent { 2 | accessed: number; 3 | value: any; 4 | } 5 | 6 | export class CacheService { 7 | constructor(private maxEntries: number) {} 8 | 9 | #cache: Map = new Map(); 10 | 11 | get(key: string) { 12 | const val = this.#cache.get(key); 13 | 14 | if (!val) { 15 | return undefined; 16 | } 17 | 18 | // Only when there are many we'll start deleting old entries. 19 | if (this.#cache.size > this.maxEntries) { 20 | let sortedList = Array.from(this.#cache.entries()).sort((e) => e[1].accessed); 21 | let deleteList = sortedList.slice(0, 20); 22 | 23 | deleteList.forEach((val) => { 24 | this.#cache.delete(val[0]); 25 | }); 26 | } 27 | 28 | val.accessed = Date.now(); 29 | 30 | return val; 31 | } 32 | 33 | set(key: string, val: any) { 34 | this.#cache.set(key, { accessed: Date.now(), value: val }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/services/check-for-update.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationRef, Injectable, isDevMode } from '@angular/core'; 2 | import { SwUpdate } from '@angular/service-worker'; 3 | import { concat, interval } from 'rxjs'; 4 | import { first } from 'rxjs/operators'; 5 | 6 | @Injectable() 7 | export class CheckForUpdateService { 8 | constructor(appRef: ApplicationRef, updates: SwUpdate) { 9 | if (!isDevMode()) { 10 | // Allow the app to stabilize first, before starting 11 | // polling for updates with `interval()`. 12 | const appIsStable$ = appRef.isStable.pipe(first((isStable) => isStable === true)); 13 | const everySixHours$ = interval(6 * 60 * 60 * 1000); 14 | const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$); 15 | 16 | everySixHoursOnceAppIsStable$.subscribe(async () => { 17 | try { 18 | const updateFound = await updates.checkForUpdate(); 19 | console.log(updateFound ? 'A new version is available.' : 'Already on the latest version.'); 20 | } catch (err) { 21 | console.error('Failed to check for updates:', err); 22 | } 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/services/database.ts: -------------------------------------------------------------------------------- 1 | // import Dexie, { Table } from 'dexie'; 2 | // import { Circle, NostrEventDocument, NostrNoteDocument, NostrProfileDocument, NostrRelayDocument } from './interfaces'; 3 | 4 | // export class DatabaseService extends Dexie { 5 | // relays!: Table; 6 | // events!: Table; 7 | // notes!: Table; 8 | // profiles!: Table; 9 | // circles!: Table; 10 | 11 | // constructor(name: string) { 12 | // super(name); 13 | 14 | // this.version(3).stores({ 15 | // relays: 'url', 16 | // events: 'id,pubkey,created_at', 17 | // notes: 'id', 18 | // profiles: 'pubkey,status', 19 | // circles: '++id', 20 | // }); 21 | // this.on('populate', () => this.populate()); 22 | // } 23 | 24 | // async populate() { 25 | // // TODO: Add the default Circles here instead of within Circle service. 26 | // // const todoListId = await this.relays.add({ 27 | // // url: 'To Do Today', 28 | // // }); 29 | // // await this.events.bulkAdd([ 30 | // // { 31 | // // todoListId, 32 | // // title: 'Feed the birds', 33 | // // }, 34 | // // { 35 | // // todoListId, 36 | // // title: 'Watch a movie', 37 | // // }, 38 | // // { 39 | // // todoListId, 40 | // // title: 'Have some sleep', 41 | // // }, 42 | // // ]); 43 | // } 44 | // } 45 | -------------------------------------------------------------------------------- /src/app/services/debug-log.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { INGXLoggerMetadata } from '@blockcore/ngx-logger'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class DebugLogService { 8 | logs: INGXLoggerMetadata[] = []; 9 | errors: INGXLoggerMetadata[] = []; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/services/loading-resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; 3 | import { Observable, filter } from 'rxjs'; 4 | import { ApplicationState } from './applicationstate'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class LoadingResolverService { 10 | constructor(public appState: ApplicationState, private router: Router) {} 11 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 12 | return this.appState.initialized$.pipe(filter((value) => value)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/services/log-writer.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; 2 | import { NGXLoggerWriterService, INGXLoggerMetadata, INGXLoggerConfig, NgxLoggerLevel } from '@blockcore/ngx-logger'; 3 | import { DebugLogService } from './debug-log'; 4 | 5 | @Injectable() 6 | export class LogWriterService extends NGXLoggerWriterService { 7 | constructor(@Inject(PLATFORM_ID) protected override platformId: any, private debugLog: DebugLogService) { 8 | super(platformId); 9 | } 10 | 11 | /** Write the content sent to the log function to the sessionStorage */ 12 | public override writeMessage(metadata: INGXLoggerMetadata, config: INGXLoggerConfig): void { 13 | super.writeMessage(metadata, config); 14 | 15 | // If the log has 100 items or more, clear 50 oldest. 16 | if (this.debugLog.logs.length > 100) { 17 | this.resize(this.debugLog.logs, 80, null); 18 | } 19 | 20 | if (this.debugLog.errors.length > 100) { 21 | this.resize(this.debugLog.errors, 80, null); 22 | } 23 | 24 | this.debugLog.logs.push(metadata); 25 | 26 | // Keep a list of ERROR and FATAL messages. 27 | if (metadata.level > NgxLoggerLevel.WARN) { 28 | this.debugLog.errors.push(metadata); 29 | } 30 | } 31 | 32 | resize(arr: any[], size: number, defval: any) { 33 | while (arr.length > size) { 34 | arr.shift(); 35 | } 36 | while (arr.length < size) { 37 | arr.push(defval); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/services/logger.ts: -------------------------------------------------------------------------------- 1 | import { NGXLogger, NgxLoggerLevel } from '@blockcore/ngx-logger'; 2 | import { Injectable } from '@angular/core'; 3 | import { Logger } from './interfaces'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class LoggerService implements Logger { 9 | constructor(private logger: NGXLogger) {} 10 | 11 | /** Change the logging level to TRACE. This is not persisted and reset on app reloads. */ 12 | enableDebug() { 13 | this.logger.updateConfig({ level: NgxLoggerLevel.TRACE }); 14 | } 15 | 16 | disableDebug() { 17 | this.logger.updateConfig({ level: NgxLoggerLevel.INFO }); 18 | } 19 | 20 | trace(message?: any | (() => any), ...additional: any[]): void { 21 | this.logger.trace(message, ...additional); 22 | } 23 | 24 | debug(message?: any | (() => any), ...additional: any[]): void { 25 | this.logger.debug(message, ...additional); 26 | } 27 | 28 | info(message?: any | (() => any), ...additional: any[]): void { 29 | this.logger.info(message, ...additional); 30 | } 31 | 32 | log(message?: any | (() => any), ...additional: any[]): void { 33 | this.logger.log(message, ...additional); 34 | } 35 | 36 | warn(message?: any | (() => any), ...additional: any[]): void { 37 | this.logger.warn(message, ...additional); 38 | } 39 | 40 | error(message?: any | (() => any), ...additional: any[]): void { 41 | this.logger.error(message, ...additional); 42 | } 43 | 44 | fatal(message?: any | (() => any), ...additional: any[]): void { 45 | this.logger.fatal(message, ...additional); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/services/message-control.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class MessageControlService { 7 | isSendable(message: string) { 8 | return message !== "" && message.trim() !== ""; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/services/messages.ts: -------------------------------------------------------------------------------- 1 | export interface WorkerRequest {} 2 | 3 | export interface WorkerResponse {} 4 | 5 | export interface RelayRequest extends WorkerRequest { 6 | type: string; 7 | data?: any; 8 | } 9 | 10 | export interface RelayResponse extends WorkerResponse { 11 | type: string; 12 | url: string; 13 | data?: any; 14 | subscription: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/services/metric-service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { StorageService } from './storage'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class MetricService { 8 | constructor(private storage: StorageService) {} 9 | 10 | get users(): { [pubKey: string]: number } { 11 | if (!this.storage.state?.metrics?.users) { 12 | this.storage.state.metrics = { users: {} }; 13 | } 14 | 15 | return this.storage.state.metrics.users; 16 | } 17 | 18 | increase(value: number, pubKey: string) { 19 | let existingMetric = this.users[pubKey]; 20 | 21 | if (!existingMetric) { 22 | existingMetric = 0; 23 | } 24 | 25 | this.users[pubKey] = existingMetric + value; 26 | } 27 | 28 | decrease(value: number, pubKey: string) { 29 | let existingMetric = this.users[pubKey]; 30 | 31 | if (!existingMetric) { 32 | existingMetric = 0; 33 | } 34 | 35 | this.users[pubKey] = existingMetric - value; 36 | } 37 | 38 | get(pubKey: string) { 39 | let value = this.users[pubKey]; 40 | 41 | if (!value) { 42 | return 0; 43 | } 44 | 45 | return value; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/services/notes.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NostrEventDocument, NostrNoteDocument, NostrProfile, NostrProfileDocument } from './interfaces'; 3 | import { StorageService } from './storage'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class NotesService { 9 | items: NostrNoteDocument[] = []; 10 | filtered: NostrNoteDocument[] = []; 11 | 12 | constructor(private db: StorageService) {} 13 | 14 | /** Notes are upserts, we replace the existing note and only keep latest. */ 15 | async putNote(document: NostrNoteDocument | any) { 16 | await this.db.storage.putNote(document); 17 | } 18 | 19 | async deleteNote(id: string) { 20 | await this.db.storage.deleteNote(id); 21 | } 22 | 23 | async load() { 24 | this.items = await this.db.storage.getNotes(); 25 | this.filterByLabels([]); 26 | } 27 | 28 | filterByLabels(labels: string[]) { 29 | if (labels.length == 0) { 30 | // this.filtered = this.items.slice(0, 5); 31 | this.filtered = this.items; 32 | } else { 33 | this.filtered = this.items.filter((n) => n.labels != null && n.labels.some((l) => labels.includes(l))); 34 | } 35 | } 36 | 37 | /** Wipes all notes. */ 38 | async wipe() { 39 | await this.db.storage.deleteNotes(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/services/queue.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject, Observable } from 'rxjs'; 3 | import { QueryJob } from './interfaces'; 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class QueueService { 8 | #queuesChanged: BehaviorSubject = new BehaviorSubject(undefined); 9 | 10 | get queues$(): Observable { 11 | return this.#queuesChanged.asObservable(); 12 | } 13 | 14 | constructor() {} 15 | 16 | enqueProfile(identifier: string) { 17 | this.#queuesChanged.next({ identifier: identifier, type: 'Profile' }); 18 | } 19 | 20 | enqueEvent(identifier: string) { 21 | this.#queuesChanged.next({ identifier: identifier, type: 'Event' }); 22 | } 23 | 24 | enqueArticle(identifier: string) { 25 | this.#queuesChanged.next({ identifier: identifier, type: 'Article' }); 26 | } 27 | 28 | enqueBadgeDefinition(identifier: string) { 29 | this.#queuesChanged.next({ identifier: identifier, type: 'BadgeDefinition' }); 30 | } 31 | 32 | enqueContacts(identifier: string) { 33 | this.#queuesChanged.next({ identifier: identifier, type: 'Contacts' }); 34 | } 35 | 36 | enque(identifier: string, type: 'Profile' | 'Event' | 'Contacts' | 'Article' | 'BadgeDefinition') { 37 | if (type === 'Profile') { 38 | this.enqueProfile(identifier); 39 | } else if (type === 'Event') { 40 | this.enqueEvent(identifier); 41 | } else if (type === 'Contacts') { 42 | this.enqueContacts(identifier); 43 | } else if (type === 'Article') { 44 | this.enqueArticle(identifier); 45 | } else if (type === 'BadgeDefinition') { 46 | this.enqueBadgeDefinition(identifier); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/services/queue.ts: -------------------------------------------------------------------------------- 1 | import { NostrRelaySubscription, QueryJob } from './interfaces'; 2 | 3 | export class Queue { 4 | enqueProfile(identifier: string) { 5 | this.queues.profile.jobs.push({ identifier: identifier, type: 'Profile' }); 6 | } 7 | 8 | enqueEvent(identifier: string) { 9 | this.queues.event.jobs.push({ identifier: identifier, type: 'Event' }); 10 | } 11 | 12 | enqueContacts(identifier: string) { 13 | this.queues.contacts.jobs.push({ identifier: identifier, type: 'Contacts' }); 14 | } 15 | 16 | enque(identifier: string, type: 'Profile' | 'Event' | 'Contacts') { 17 | if (type === 'Profile') { 18 | this.enqueProfile(identifier); 19 | } else if (type === 'Event') { 20 | this.enqueEvent(identifier); 21 | } else if (type === 'Contacts') { 22 | this.enqueContacts(identifier); 23 | } 24 | } 25 | 26 | queues = { 27 | profile: { 28 | active: false, 29 | jobs: [] as QueryJob[], 30 | }, 31 | event: { 32 | active: false, 33 | jobs: [] as QueryJob[], 34 | }, 35 | article: { 36 | active: false, 37 | jobs: [] as QueryJob[], 38 | }, 39 | badgedefinition: { 40 | active: false, 41 | jobs: [] as QueryJob[], 42 | }, 43 | contacts: { 44 | active: false, 45 | jobs: [] as QueryJob[], 46 | }, 47 | subscriptions: { 48 | active: false, 49 | jobs: [] as NostrRelaySubscription[], 50 | }, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/app/services/theme.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Renderer2, RendererFactory2 } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class ThemeService { 7 | public renderer: Renderer2; 8 | 9 | constructor(private _renderer: RendererFactory2) { 10 | this.renderer = _renderer.createRenderer(null, null); 11 | } 12 | 13 | get darkMode(): boolean { 14 | if (localStorage.getItem('theme')) { 15 | if (localStorage.getItem('theme') === 'dark') { 16 | return true; 17 | } 18 | } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) { 19 | return true; 20 | } 21 | 22 | return false; 23 | } 24 | 25 | set darkMode(value: boolean) { 26 | if (value) { 27 | localStorage.setItem('theme', 'dark'); 28 | } else { 29 | localStorage.setItem('theme', 'light'); 30 | } 31 | 32 | this.updateMode(); 33 | } 34 | 35 | public init() { 36 | this.updateMode(); 37 | } 38 | 39 | private updateMode() { 40 | if (this.darkMode) { 41 | this.renderer.addClass(document.body, 'dark'); 42 | // document.documentElement.classList.add('dark'); 43 | } else { 44 | this.renderer.removeClass(document.body, 'dark'); 45 | // document.documentElement.classList.remove('dark'); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { ChatService } from './chat.service'; 4 | import { UserModel } from './interfaces'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class UserService { 10 | users = new BehaviorSubject>(null as any); 11 | active = new BehaviorSubject(null as any); 12 | 13 | constructor(private chatService: ChatService) {} 14 | 15 | getUsers() { 16 | return this.chatService.data.users; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/add-relay-dialog/add-relay-dialog.css: -------------------------------------------------------------------------------- 1 | .input-full-width { 2 | width: 100% !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/shared/add-relay-dialog/add-relay-dialog.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ 'RelayDialog.AddARelay' | translate }}

3 |
4 | 5 | dns 6 | URL 7 | 8 | 9 | 10 | {{ 'RelayDialog.Read' | translate }}  11 | {{ 'RelayDialog.Write' | translate }} 12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/app/shared/add-relay-dialog/add-relay-dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatDialog, MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; 5 | import { MatFormFieldModule } from '@angular/material/form-field'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatInputModule } from '@angular/material/input'; 8 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 9 | import { TranslateModule } from '@ngx-translate/core'; 10 | 11 | export interface AddRelayDialogData { 12 | url: string; 13 | read: boolean; 14 | write: boolean; 15 | } 16 | 17 | @Component({ 18 | selector: 'add-relay-dialog', 19 | templateUrl: 'add-relay-dialog.html', 20 | styleUrls: ['add-relay-dialog.css'], 21 | imports: [MatFormFieldModule, MatIconModule, MatButtonModule, MatInputModule, MatSlideToggleModule, FormsModule, TranslateModule, MatDialogModule], 22 | }) 23 | export class AddRelayDialog { 24 | constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AddRelayDialogData) {} 25 | 26 | onNoClick(): void { 27 | this.data.url = ''; 28 | this.dialogRef.close(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/ago.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ name: 'ago' }) 4 | export class AgoPipe implements PipeTransform { 5 | transform(value?: number): string { 6 | if (!value) { 7 | return ''; 8 | } 9 | 10 | const date = new Date(value * 1000); // Convert seconds to milliseconds 11 | const now = new Date(); 12 | const diff = now.getTime() - date.getTime(); 13 | const seconds = Math.floor(diff / 1000); 14 | const minutes = Math.floor(seconds / 60); 15 | const hours = Math.floor(minutes / 60); 16 | const days = Math.floor(hours / 24); 17 | 18 | if (days > 0) { 19 | return `${days} day${days > 1 ? 's' : ''} ago`; 20 | } else if (hours > 0) { 21 | return `${hours} hour${hours > 1 ? 's' : ''} ago`; 22 | } else if (minutes > 0) { 23 | return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; 24 | } else { 25 | return 'just now'; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/shared/badge-card/badge-card.css: -------------------------------------------------------------------------------- 1 | .badge-card { 2 | max-width: 256px; 3 | } 4 | 5 | .badge-card img { 6 | width: 100%; 7 | height: auto; 8 | } 9 | 10 | @media only screen and (max-width: 599px) { 11 | .badge-card { 12 | width: 100%; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/shared/badge-card/badge-card.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ badge.name }} 4 | {{ badge.name }} 5 | {{badge.slug}} 6 | 7 | 8 | 9 |

{{ badge.description }}

10 | 11 | #{{ tag }} 12 | 13 | 14 |

15 | 16 |

17 |
18 | 21 |
22 | -------------------------------------------------------------------------------- /src/app/shared/bech32.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { Utilities } from '../services/utilities'; 3 | 4 | @Pipe({ name: 'bech32' }) 5 | export class Bech32Pipe implements PipeTransform { 6 | constructor(private utilities: Utilities) {} 7 | 8 | transform(value: string): string { 9 | if (!value) { 10 | return value; 11 | } 12 | 13 | if (!value.startsWith('npub')) { 14 | return this.utilities.getNostrIdentifier(value); 15 | } 16 | 17 | return value; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shared/chat-detail/chat-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | // import {ChatDetailComponent} from './chat-detail.component'; 4 | 5 | // describe('ChatDetailComponent', () => { 6 | // let component: ChatDetailComponent; 7 | // let fixture: ComponentFixture; 8 | 9 | // beforeEach(async(() => { 10 | // TestBed.configureTestingModule({ 11 | // declarations: [ChatDetailComponent] 12 | // }) 13 | // .compileComponents(); 14 | // })); 15 | 16 | // beforeEach(() => { 17 | // fixture = TestBed.createComponent(ChatDetailComponent); 18 | // component = fixture.componentInstance; 19 | // fixture.detectChanges(); 20 | // }); 21 | 22 | // it('should create', () => { 23 | // expect(component).toBeTruthy(); 24 | // }); 25 | // }); 26 | -------------------------------------------------------------------------------- /src/app/shared/chat-item/chat-item.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

{{ chat.username }}

4 |

{{ chat.lastMessage }}

5 | 6 |
7 | -------------------------------------------------------------------------------- /src/app/shared/chat-item/chat-item.component.scss: -------------------------------------------------------------------------------- 1 | .last-message { 2 | text-overflow: ellipsis; 3 | overflow: hidden; 4 | width: 90%; 5 | white-space: nowrap; 6 | } 7 | .user-avatar{ 8 | height: 50px; 9 | width: 50px; 10 | } 11 | mat-list-item{ 12 | height: 90px !important; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/chat-item/chat-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | // import {ChatItemComponent} from './chat-item.component'; 4 | 5 | // describe('ChatItemComponent', () => { 6 | // let component: ChatItemComponent; 7 | // let fixture: ComponentFixture; 8 | 9 | // beforeEach(async(() => { 10 | // TestBed.configureTestingModule({ 11 | // declarations: [ChatItemComponent] 12 | // }) 13 | // .compileComponents(); 14 | // })); 15 | 16 | // beforeEach(() => { 17 | // fixture = TestBed.createComponent(ChatItemComponent); 18 | // component = fixture.componentInstance; 19 | // fixture.detectChanges(); 20 | // }); 21 | 22 | // it('should create', () => { 23 | // expect(component).toBeTruthy(); 24 | // }); 25 | // }); 26 | -------------------------------------------------------------------------------- /src/app/shared/chat-item/chat-item.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 3 | import { MatBadgeModule } from '@angular/material/badge'; 4 | import { MatListModule } from '@angular/material/list'; 5 | import { ChatService } from 'src/app/services/chat.service'; 6 | import { ChatModel, NostrEventDocument } from 'src/app/services/interfaces'; 7 | 8 | @Component({ 9 | selector: 'app-chat-item', 10 | templateUrl: './chat-item.component.html', 11 | styleUrls: ['./chat-item.component.scss'], 12 | imports: [MatListModule, MatBadgeModule, CommonModule], 13 | }) 14 | export class ChatItemComponent { 15 | @Output() openChatSidebar: EventEmitter = new EventEmitter(); 16 | @Input() chat!: ChatModel; 17 | @Input() event!: NostrEventDocument; 18 | 19 | constructor(private service: ChatService) {} 20 | 21 | showMessageDetail() { 22 | this.openChatSidebar.emit(this.chat.username); 23 | // this.service.chat.next(this.chat); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shared/chat-list/chat-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 |
{{ chat.pubkey }} : {{ chat.content }}
22 | 23 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/app/shared/chat-list/chat-list.component.scss: -------------------------------------------------------------------------------- 1 | .form { 2 | padding: 16px 16px 0 16px; 3 | } 4 | 5 | .input-full-width { 6 | position: relative; 7 | margin: auto; 8 | } 9 | 10 | .search { 11 | position: sticky; 12 | top: 0; 13 | padding: 10px; 14 | z-index: 999; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/chat-list/chat-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | // import {ChatListComponent} from './chat-list.component'; 4 | 5 | // describe('ChatListComponent', () => { 6 | // let component: ChatListComponent; 7 | // let fixture: ComponentFixture; 8 | 9 | // beforeEach(async(() => { 10 | // TestBed.configureTestingModule({ 11 | // declarations: [ChatListComponent] 12 | // }) 13 | // .compileComponents(); 14 | // })); 15 | 16 | // beforeEach(() => { 17 | // fixture = TestBed.createComponent(ChatListComponent); 18 | // component = fixture.componentInstance; 19 | // fixture.detectChanges(); 20 | // }); 21 | 22 | // it('should create', () => { 23 | // expect(component).toBeTruthy(); 24 | // }); 25 | // }); 26 | -------------------------------------------------------------------------------- /src/app/shared/chat-list/chat-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; 2 | import { from, Observable, of } from 'rxjs'; 3 | import { ChatService } from 'src/app/services/chat.service'; 4 | import { ChatItemComponent } from '../chat-item/chat-item.component'; 5 | import { MatIconModule } from '@angular/material/icon'; 6 | import { MatFormFieldModule } from '@angular/material/form-field'; 7 | import { MatListModule } from '@angular/material/list'; 8 | import { RouterModule } from '@angular/router'; 9 | import { CommonModule } from '@angular/common'; 10 | import { MatInputModule } from '@angular/material/input'; 11 | 12 | interface ChatModel { 13 | id: string; 14 | name: string; 15 | } 16 | 17 | @Component({ 18 | selector: 'app-chat-list', 19 | templateUrl: './chat-list.component.html', 20 | styleUrls: ['./chat-list.component.scss'], 21 | imports: [ChatItemComponent, MatIconModule, MatFormFieldModule, MatInputModule, MatListModule, RouterModule, CommonModule], 22 | }) 23 | export class ChatListComponent implements OnInit { 24 | @Output() openChatSidebar: EventEmitter = new EventEmitter(); 25 | 26 | constructor(public chatService: ChatService) {} 27 | 28 | ngOnInit() { 29 | this.chatService.download(); 30 | 31 | // this.chatService.uniqueChats$.subscribe((data) => { 32 | // console.log('YEEH!', data); 33 | // }); 34 | } 35 | 36 | add() { 37 | // this.#chats.unshift({ id: '123', name: 'Yes!' }); 38 | } 39 | 40 | reset() { 41 | // this.#chats = []; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/shared/circle-style.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { circleStyles } from './defaults'; 3 | 4 | @Pipe({ name: 'circlestyle' }) 5 | export class CircleStylePipe implements PipeTransform { 6 | constructor() {} 7 | 8 | transform(value: number): string | undefined { 9 | if (!value) { 10 | return 'Pipes'; 11 | } 12 | 13 | return circleStyles.find((c) => c.id == value)?.name; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/content-input-directive/content-input-height.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, HostListener, Input } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appAutoInputHeight]', 5 | }) 6 | export class ContentInputHeightDirective { 7 | @HostListener('input') 8 | onInput(): void { 9 | const e = this.element.nativeElement; 10 | 11 | if (e instanceof HTMLTextAreaElement) { 12 | const element: HTMLTextAreaElement = this.element.nativeElement; 13 | 14 | if (element.scrollHeight > element.clientHeight) { 15 | element.style.height = `${element.scrollHeight}px`; 16 | } 17 | } 18 | } 19 | 20 | constructor(private element: ElementRef) {} 21 | } 22 | -------------------------------------------------------------------------------- /src/app/shared/content-input-directive/content-input.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, HostListener, Input } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appContentEditor]', 5 | }) 6 | export class ContentEditorDirective { 7 | @HostListener('input') 8 | onInput(): void { 9 | const e = this.element.nativeElement; 10 | 11 | if (e instanceof HTMLDivElement) { 12 | // TODO: Add future support for content editable div. 13 | console.warn('Unsupported HTML element type (HTMLDivElement) for content editor.'); 14 | return; 15 | } else if (e instanceof HTMLTextAreaElement) { 16 | // Supported input type! 17 | } else { 18 | console.warn('Unsupported HTML element type for content editor.'); 19 | return; 20 | } 21 | 22 | const element: HTMLTextAreaElement = this.element.nativeElement; 23 | 24 | const selectionStart = element.selectionStart; 25 | const selectionEnd = element.selectionEnd; 26 | const textInput = element.value; 27 | 28 | // console.log('selectionStart: ' + selectionStart); 29 | // console.log('selectionEnd: ' + selectionEnd); 30 | // console.log('textInput: ' + textInput); 31 | 32 | let token: string | undefined; 33 | 34 | // If we are at the end of text, it means user is typing at the end of the text and 35 | // we'll select the last character. 36 | if (textInput.length == selectionEnd) { 37 | token = textInput.at(-1); 38 | } else { 39 | token = textInput.substring(selectionStart - 1, selectionEnd); 40 | } 41 | 42 | // console.log('token: ' + token); 43 | 44 | if (token == '@') { 45 | // console.log('Mention'); 46 | } else if (token == '#') { 47 | // console.log('Hashtag'); 48 | } 49 | } 50 | 51 | constructor(private element: ElementRef) {} 52 | } 53 | -------------------------------------------------------------------------------- /src/app/shared/content-music/content-music.css: -------------------------------------------------------------------------------- 1 | .music-list { 2 | } 3 | 4 | .music-item { 5 | display: flex; 6 | flex-direction: row; 7 | gap: 1em; 8 | margin-bottom: 1em; 9 | } 10 | 11 | .music-album { 12 | max-height: 128px; 13 | } 14 | 15 | .music-title { 16 | font-weight: 700; 17 | font-size: 1.2em; 18 | margin-bottom: 0.4em; 19 | } 20 | 21 | .music-artist { 22 | color: rgba(255, 255, 255, 0.5); 23 | margin-bottom: 0.4em; 24 | } 25 | 26 | .music-controls { 27 | width: 100%; 28 | } 29 | 30 | .music-details { 31 | flex: 1 2 auto; 32 | } 33 | 34 | audio { 35 | width: 100%; 36 | } 37 | 38 | @media only screen and (max-width: 599px) { 39 | } 40 | -------------------------------------------------------------------------------- /src/app/shared/content-photos/content-photos.css: -------------------------------------------------------------------------------- 1 | .event-image { 2 | width: 100%; 3 | max-width: 480px; 4 | display: inline-block; 5 | margin-top: 0.2em; 6 | } 7 | 8 | .event-images { 9 | text-align: left; 10 | } 11 | 12 | .event-video { 13 | width: 100%; 14 | display: inline-block; 15 | margin-top: 0.2em; 16 | min-height: 320px; 17 | height: 480px; 18 | } 19 | 20 | .events-videos { 21 | text-align: left; 22 | } 23 | 24 | .reply-to { 25 | margin-top: 0.2em; 26 | } 27 | 28 | @media only screen and (max-width: 599px) { 29 | .event-video { 30 | width: 100vw !important; 31 | height: 50vh !important; 32 | margin-left: -1.6em; 33 | } 34 | 35 | .event-image { 36 | width: 100vw !important; 37 | margin-left: -1.6em; 38 | max-width: max-content; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/shared/content-photos/content-photos.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /src/app/shared/content-podcast/content-podcast.css: -------------------------------------------------------------------------------- 1 | .music-list { 2 | } 3 | 4 | .music-item { 5 | display: flex; 6 | flex-direction: row; 7 | gap: 1em; 8 | margin-bottom: 1em; 9 | } 10 | 11 | .music-album { 12 | max-height: 128px; 13 | } 14 | 15 | .music-title { 16 | font-weight: 700; 17 | margin-bottom: 0.4em; 18 | } 19 | 20 | .music-artist { 21 | color: rgba(255, 255, 255, 0.5); 22 | margin-bottom: 0.4em; 23 | } 24 | 25 | .music-controls { 26 | width: 100%; 27 | } 28 | 29 | .music-details { 30 | flex: 1 2 auto; 31 | } 32 | 33 | audio { 34 | width: 100%; 35 | } 36 | 37 | @media only screen and (max-width: 599px) { 38 | .music-album { 39 | max-height: 32px; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/shared/content/content.css: -------------------------------------------------------------------------------- 1 | img { 2 | touch-action: pan-y pinch-zoom; 3 | -webkit-user-select: none; 4 | user-select: none; 5 | } 6 | 7 | .event-image { 8 | width: 100%; 9 | max-width: 480px; 10 | display: inline-block; 11 | margin-top: 0.2em; 12 | 13 | touch-action: pan-y pinch-zoom; 14 | -webkit-user-select: none; 15 | user-select: none; 16 | } 17 | 18 | .event-images { 19 | text-align: left; 20 | } 21 | 22 | .event-video { 23 | width: 100%; 24 | display: inline-block; 25 | margin-top: 0.2em; 26 | aspect-ratio: 16 / 9; 27 | } 28 | 29 | /* .animate { 30 | animation: scale 1.5s both; 31 | } 32 | 33 | @keyframes scale { 34 | 50% { 35 | transform: scale(0.5); 36 | } 37 | 100% { 38 | transform: scale(1); 39 | } 40 | } */ 41 | 42 | .embed-iframe { 43 | min-width: 300px; 44 | min-height: 300px; 45 | } 46 | 47 | .events-videos { 48 | text-align: left; 49 | } 50 | 51 | .reply-to { 52 | margin-top: 0.2em; 53 | } 54 | 55 | .event-content { 56 | overflow-wrap: break-word; 57 | /* word-break: break-all; */ 58 | /* inline-size: min-content; */ 59 | /* overflow: hidden; */ 60 | } 61 | 62 | .event-content-big { 63 | font-size: 2.2em; 64 | } 65 | 66 | @media only screen and (max-width: 599px) { 67 | .event-video { 68 | width: 100vw !important; 69 | height: 50vh !important; 70 | margin-left: -1.6em; 71 | } 72 | 73 | .event-image { 74 | width: 100vw !important; 75 | margin-left: -1.6em; 76 | max-width: max-content; 77 | } 78 | } 79 | 80 | .meme { 81 | max-width: 128px; 82 | } 83 | -------------------------------------------------------------------------------- /src/app/shared/create-circle-dialog/create-circle-dialog.html: -------------------------------------------------------------------------------- 1 |
2 |

Create Circle

3 |
4 | 5 | supervised_user_circle 6 | Circle Name 7 | 8 | 9 | 10 | 11 |
12 | Color 13 | 14 |
15 | 16 | 17 | Type 18 | 19 | {{style.name}} 20 | 21 | 22 | 23 | 24 | Visibility 25 | 26 | Public 27 | Private 28 | 29 | 30 |
31 | 32 |
33 | 34 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /src/app/shared/create-circle-dialog/create-circle-dialog.scss: -------------------------------------------------------------------------------- 1 | .input-full-width { 2 | width: 100% !important; 3 | } 4 | 5 | .circle { 6 | cursor: pointer; 7 | } 8 | 9 | .circle:hover { 10 | color: #fff; 11 | } 12 | 13 | .color { 14 | width: 24px; 15 | height: 24px; 16 | border: 2px solid #fff; 17 | margin: 12px; 18 | border-radius: 50%; 19 | } 20 | 21 | .circle-input { 22 | margin-right: 1em; 23 | } 24 | 25 | @media only screen and (max-width: 540px) { 26 | .circle-input { 27 | margin-right: 0; 28 | width: 100%; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/create-circle-dialog/create-circle-dialog.ts: -------------------------------------------------------------------------------- 1 | import { BreakpointObserver } from '@angular/cdk/layout'; 2 | import { Component, Inject } from '@angular/core'; 3 | import { MatDialog, MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; 4 | import { ApplicationState } from 'src/app/services/applicationstate'; 5 | import { Circle } from 'src/app/services/interfaces'; 6 | import { circleStyles } from '../defaults'; 7 | import { MatFormFieldModule } from '@angular/material/form-field'; 8 | import { FormsModule } from '@angular/forms'; 9 | import { MatSelectModule } from '@angular/material/select'; 10 | import { MatInputModule } from '@angular/material/input'; 11 | 12 | @Component({ 13 | selector: 'create-circle-dialog', 14 | templateUrl: 'create-circle-dialog.html', 15 | styleUrls: ['create-circle-dialog.scss'], 16 | imports: [MatFormFieldModule, MatInputModule, FormsModule, MatSelectModule, MatDialogModule], 17 | }) 18 | export class CircleDialog { 19 | styles = circleStyles; 20 | 21 | constructor(private appState: ApplicationState, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: Circle) { 22 | this.data.name = ''; 23 | this.data.color = '#673ab7'; 24 | } 25 | 26 | onNoClick(): void { 27 | this.data.name = ''; 28 | this.data.color = '#673ab7'; 29 | this.dialogRef.close(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/shared/create-follow-dialog/create-follow-dialog.html: -------------------------------------------------------------------------------- 1 |
2 |

Enter Public Key or Alias (NIP-05) to Follow

3 |
4 | 5 | person_add 6 | Public Key / Alias 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/app/shared/create-follow-dialog/create-follow-dialog.scss: -------------------------------------------------------------------------------- 1 | .input-full-width { 2 | width: 100% !important; 3 | } -------------------------------------------------------------------------------- /src/app/shared/create-follow-dialog/create-follow-dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatDialog, MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; 5 | import { MatFormFieldModule } from '@angular/material/form-field'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatInputModule } from '@angular/material/input'; 8 | 9 | export interface FollowDialogData { 10 | pubkey: string; 11 | } 12 | 13 | @Component({ 14 | selector: 'create-follow-dialog', 15 | templateUrl: 'create-follow-dialog.html', 16 | styleUrls: ['create-follow-dialog.scss'], 17 | imports: [MatFormFieldModule, MatIconModule, MatButtonModule, MatInputModule, FormsModule, MatDialogModule], 18 | }) 19 | export class FollowDialog { 20 | constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: FollowDialogData) {} 21 | 22 | onNoClick(): void { 23 | this.data.pubkey = ''; 24 | this.dialogRef.close(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/create-note-dialog/create-note-dialog.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Write Your Note

4 |
5 |
6 | sentiment_satisfied 7 | 8 |
9 | 10 | 11 | Note 12 | 13 | 14 | 17 |
18 |
19 | zoom_out_map 20 | 21 | 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /src/app/shared/create-note-dialog/create-note-dialog.scss: -------------------------------------------------------------------------------- 1 | .input-full-width { 2 | width: 100% !important; 3 | } 4 | 5 | .toolbar { 6 | display: flex; 7 | margin-bottom: 5px; 8 | margin-top: 3px; 9 | } 10 | 11 | .toolbar-icon { 12 | cursor: pointer; 13 | } 14 | 15 | .toolbar-icon:hover { 16 | color: #9c27b0; 17 | } 18 | 19 | .maximize-button { 20 | cursor: pointer; 21 | // float: left !important; 22 | margin-right: auto; 23 | color: #d87fe7; 24 | } 25 | 26 | .maximize-button:hover { 27 | color: #9c27b0; 28 | } 29 | 30 | .note-input { 31 | // height: 40vh !important; 32 | } 33 | 34 | .margin-right { 35 | margin-right: 5px; 36 | } 37 | 38 | .picker { 39 | display: block; 40 | position: fixed; 41 | z-index: 3; 42 | } 43 | 44 | .footer { 45 | display: flex; 46 | justify-content: space-between; 47 | } 48 | -------------------------------------------------------------------------------- /src/app/shared/date-time/date-time.component.html: -------------------------------------------------------------------------------- 1 | {{ CustomLabel }} 2 | 3 |
4 | 5 | {{ CustomDateLabel }} 6 | 7 | {{ CustomDateHint }} 8 | 9 | 10 | 11 | 12 | 13 | {{ CustomTimeLabel }} 14 | 15 | {{ CustomHoursHint }} 16 | 17 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /src/app/shared/date-time/date-time.component.scss: -------------------------------------------------------------------------------- 1 | :host, form { 2 | display: flex; 3 | align-items: baseline; 4 | justify-content: center; 5 | gap: 5px; 6 | } -------------------------------------------------------------------------------- /src/app/shared/date-time/date-time.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DateTimeComponent } from './date-time.component'; 4 | 5 | describe('DateTimeComponent', () => { 6 | let component: DateTimeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DateTimeComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DateTimeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/date/date.css: -------------------------------------------------------------------------------- 1 | .event-date { 2 | margin-right: 0.1em; 3 | cursor: pointer; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/shared/date/date.html: -------------------------------------------------------------------------------- 1 | {{ date | ago }} {{ date*1000 | date: 'yyyy-MM-dd HH:mm:ss' }} 2 | -------------------------------------------------------------------------------- /src/app/shared/date/date.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input } from '@angular/core'; 3 | import { AgoPipe } from '../ago.pipe'; 4 | 5 | @Component({ 6 | selector: 'app-date', 7 | templateUrl: 'date.html', 8 | styleUrls: ['date.css'], 9 | imports: [CommonModule, AgoPipe], 10 | }) 11 | export class DateComponent { 12 | @Input() date!: number; 13 | isoDate?: boolean; 14 | toggle() { 15 | this.isoDate = !this.isoDate; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/shared/defaults.ts: -------------------------------------------------------------------------------- 1 | import { CircleStyle } from '../services/interfaces'; 2 | 3 | export const circleStyles: CircleStyle[] = [ 4 | { 5 | id: 1, 6 | name: 'Pipe', 7 | }, 8 | { 9 | id: 2, 10 | name: 'Text', 11 | }, 12 | { 13 | id: 3, 14 | name: 'Photo', 15 | }, 16 | { 17 | id: 4, 18 | name: 'Film', 19 | }, 20 | { 21 | id: 5, 22 | name: 'Music', 23 | }, 24 | { 25 | id: 6, 26 | name: 'Podcast', 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /src/app/shared/directory-icon/directory-icon.html: -------------------------------------------------------------------------------- 1 | @for (verification of verifications; track verification) { 2 | public 3 | } -------------------------------------------------------------------------------- /src/app/shared/directory-icon/directory-icon.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { ProfileService } from 'src/app/services/profile'; 3 | import { NostrProfileDocument } from '../../services/interfaces'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { MatTooltipModule } from '@angular/material/tooltip'; 6 | 7 | @Component({ 8 | selector: 'app-directory-icon', 9 | templateUrl: './directory-icon.html', 10 | imports: [MatIconModule, MatTooltipModule], 11 | }) 12 | export class DirectoryIconComponent { 13 | @Input() pubkey: string = ''; 14 | @Input() profile?: NostrProfileDocument; 15 | 16 | verifications?: string[]; 17 | 18 | constructor(private profiles: ProfileService) {} 19 | 20 | async ngOnInit() { 21 | if (!this.profile) { 22 | this.profile = await this.profiles.getLocalProfile(this.pubkey); 23 | } 24 | 25 | if (!this.profile) { 26 | return; 27 | } 28 | 29 | this.verifications = this.profile.verifications; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/shared/event-buttons/event-buttons.css: -------------------------------------------------------------------------------- 1 | .picker { 2 | display: block; 3 | z-index: 3; 4 | } 5 | 6 | .thread-buttons { 7 | padding-left: 1em; 8 | margin-left: 27px; 9 | padding-bottom: 0.2em; 10 | padding-top: 0.4em; 11 | } 12 | 13 | .toolbar-icon { 14 | cursor: pointer; 15 | } 16 | 17 | .toolbar-icon:hover { 18 | color: #9c27b0; 19 | } 20 | 21 | .reaction-icon { 22 | font-size: 1.4em; 23 | margin-right: 0.4em; 24 | } 25 | 26 | .reply-widget { 27 | padding-top: 1em; 28 | padding-bottom: 1em; 29 | } 30 | 31 | .reply-widget-buttons { 32 | display: flex; 33 | flex-direction: row; 34 | } 35 | 36 | .reply-widget-middle { 37 | flex: 1 2 auto; 38 | } 39 | 40 | .thread-buttons button { 41 | margin-left: 0.4em; 42 | } 43 | 44 | .right-side { 45 | float: right; 46 | } 47 | -------------------------------------------------------------------------------- /src/app/shared/event-header/event-header.css: -------------------------------------------------------------------------------- 1 | .icon-large .profile-image { 2 | width: 256px; 3 | height: 256px; 4 | } 5 | 6 | .icon-medium .profile-image { 7 | object-fit: cover; 8 | width: 128px; 9 | height: 128px; 10 | border-radius: 50%; 11 | } 12 | 13 | .icon-thumbnail .profile-image { 14 | object-fit: cover; 15 | width: 64px; 16 | height: 64px; 17 | border-radius: 50%; 18 | border-width: 2px; 19 | margin-left: 2px; 20 | margin-right: 2px; 21 | margin-bottom: 2px; 22 | } 23 | 24 | .icon-small .profile-image { 25 | object-fit: cover; 26 | width: 48px; 27 | height: 48px; 28 | border-radius: 50%; 29 | border-width: 2px; 30 | margin-left: 4px; 31 | margin-right: 4px; 32 | margin-bottom: 2px; 33 | } 34 | 35 | .content-items div { 36 | word-wrap: break-word; 37 | line-break: anywhere; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/app/shared/event-reactions/event-reactions.css: -------------------------------------------------------------------------------- 1 | .thread-reactions { 2 | margin-left: 27px; 3 | padding-left: 1em; 4 | /* border-left: 2px solid rgba(255, 255, 255, 0.15) !important; */ 5 | padding-bottom: 0.4em; 6 | } 7 | .thread-reaction { 8 | margin-right: 0.75em; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/shared/event-reactions/event-reactions.html: -------------------------------------------------------------------------------- 1 |
2 | ⚡️{{ amountZapped }} 3 | 🔁 {{ threadEntry?.boosts }} 4 | {{ item.key }} {{ item.value }} 5 |
6 | -------------------------------------------------------------------------------- /src/app/shared/event-thread/event-thread.css: -------------------------------------------------------------------------------- 1 | .thread-event { 2 | margin-left: 27px; 3 | /* border-left: 2px solid rgba(255, 255, 255, 0.15); */ 4 | padding-top: 0em; 5 | padding-left: 1em; 6 | } 7 | 8 | .thread-content { 9 | margin-left: 27px; 10 | padding-left: 1em; 11 | /* border-left: 2px solid rgba(255, 255, 255, 0.15) !important; */ 12 | display: block; 13 | } 14 | 15 | @media only screen and (max-width: 1150px) { 16 | .thread-event { 17 | margin-top: 1em; 18 | margin-left: 0; 19 | padding-left: 4px; 20 | } 21 | 22 | .thread-content { 23 | margin-left: 27px; 24 | } 25 | 26 | .events-header { 27 | padding-left: 28px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/shared/event-thread/event-thread.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /src/app/shared/event-thread/event-thread.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { CircleService } from 'src/app/services/circle'; 3 | import { OptionsService } from 'src/app/services/options'; 4 | import { ProfileService } from 'src/app/services/profile'; 5 | import { ThreadService } from 'src/app/services/thread'; 6 | import { UIService } from 'src/app/services/ui'; 7 | import { Utilities } from 'src/app/services/utilities'; 8 | import { Circle, NostrEventDocument, ThreadEntry } from '../../services/interfaces'; 9 | import { EventHeaderComponent } from '../event-header/event-header'; 10 | import { EventActionsComponent } from '../event-actions/event-actions'; 11 | import { ContentComponent } from '../content/content'; 12 | import { EventReactionsComponent } from '../event-reactions/event-reactions'; 13 | import { EventButtonsComponent } from '../event-buttons/event-buttons'; 14 | import { DateComponent } from '../date/date'; 15 | import { CommonModule } from '@angular/common'; 16 | import { DirectoryIconComponent } from '../directory-icon/directory-icon'; 17 | 18 | @Component({ 19 | selector: 'app-event-thread', 20 | templateUrl: './event-thread.html', 21 | styleUrls: ['./event-thread.css'], 22 | imports: [EventHeaderComponent, EventActionsComponent, ContentComponent, 23 | EventReactionsComponent, EventButtonsComponent, DateComponent, CommonModule, DirectoryIconComponent 24 | ], 25 | }) 26 | export class EventThreadComponent { 27 | @Input() event?: NostrEventDocument | undefined; 28 | 29 | imagePath = '/assets/profile.png'; 30 | tooltip = ''; 31 | tooltipName = ''; 32 | profileName = ''; 33 | circle?: Circle; 34 | 35 | constructor(public ui: UIService, public thread: ThreadService, public optionsService: OptionsService) {} 36 | 37 | ngAfterViewInit() {} 38 | 39 | async ngOnInit() {} 40 | } 41 | -------------------------------------------------------------------------------- /src/app/shared/event/event.css: -------------------------------------------------------------------------------- 1 | .thread-event { 2 | margin-left: 27px; 3 | padding-top: 0em; 4 | padding-left: 1em; 5 | } 6 | 7 | .thread-content { 8 | margin-left: 27px; 9 | padding-left: 1em; 10 | display: block; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/event/event.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | {{ event.created_at | ago }} 5 | 6 |
7 | 8 | 9 | 10 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /src/app/shared/import-sheet/import-sheet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Found {{data.pubkeys.length}} profiles and {{data.relaysCount}} relays 4 | Import your existing relays (replaces default relays) and public following list 5 | 6 | 7 | 21 | 22 | -------------------------------------------------------------------------------- /src/app/shared/import-sheet/import-sheet.ts: -------------------------------------------------------------------------------- 1 | // import { Component, Inject } from '@angular/core'; 2 | // import { MatBottomSheetRef, MAT_BOTTOM_SHEET_DATA } from '@angular/material/bottom-sheet'; 3 | // import { Router } from '@angular/router'; 4 | // import { ProfileService } from 'src/app/services/profile'; 5 | // import { RelayService } from 'src/app/services/relay'; 6 | // import { sleep, Utilities } from 'src/app/services/utilities'; 7 | 8 | // @Component({ 9 | // selector: 'app-import-sheet', 10 | // templateUrl: 'import-sheet.html', 11 | // }) 12 | // export class ImportSheet { 13 | // constructor( 14 | // private utilities: Utilities, 15 | // private relayService: RelayService, 16 | // private router: Router, 17 | // private profileService: ProfileService, 18 | // @Inject(MAT_BOTTOM_SHEET_DATA) public data: any, 19 | // private bottomSheetRef: MatBottomSheetRef 20 | // ) {} 21 | 22 | // async import(event: MouseEvent) { 23 | // this.bottomSheetRef.dismiss(); 24 | // event.preventDefault(); 25 | 26 | // if (this.data.relaysCount > 0) { 27 | // const relayUrls = this.utilities.getRelayUrls(this.data.relays); 28 | 29 | // await this.relayService.deleteRelays(relayUrls); 30 | 31 | // await this.relayService.appendRelays(this.data.relays); 32 | // } 33 | 34 | // const following = this.data.pubkeys; 35 | 36 | // for (let i = 0; i < following.length; i++) { 37 | // const publicKey = following[i]; 38 | // this.profileService.follow(publicKey); 39 | // } 40 | 41 | // setTimeout(() => { 42 | // this.router.navigateByUrl('/people'); 43 | // }, 100); 44 | // } 45 | // } 46 | -------------------------------------------------------------------------------- /src/app/shared/label.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { LabelService } from '../services/label'; 3 | 4 | @Pipe({ name: 'label' }) 5 | export class LabelPipe implements PipeTransform { 6 | constructor(private labelService: LabelService) {} 7 | 8 | transform(value?: string): string { 9 | if (!value) { 10 | return ''; 11 | } 12 | 13 | return this.labelService.getName(value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/label/label.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/app/shared/label/label.css -------------------------------------------------------------------------------- /src/app/shared/label/label.html: -------------------------------------------------------------------------------- 1 | Labels: {{ label | label }}, 2 | -------------------------------------------------------------------------------- /src/app/shared/label/label.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input } from '@angular/core'; 3 | import { LabelService } from 'src/app/services/label'; 4 | import { LabelPipe } from '../label.pipe'; 5 | 6 | @Component({ 7 | selector: 'app-label', 8 | templateUrl: 'label.html', 9 | imports: [CommonModule, LabelPipe], 10 | }) 11 | export class LabelComponent { 12 | @Input() labels: string[] = []; 13 | 14 | constructor(private labelService: LabelService) {} 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/labels/labels.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/app/shared/labels/labels.css -------------------------------------------------------------------------------- /src/app/shared/labels/labels.html: -------------------------------------------------------------------------------- 1 | 2 | {{label.name}} 3 | 4 |
5 | 6 | 7 | 8 | 11 | 14 |
15 | 18 |
19 | -------------------------------------------------------------------------------- /src/app/shared/labels/labels.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Output } from '@angular/core'; 2 | import { MatChipListboxChange } from '@angular/material/chips'; 3 | import { LabelService } from 'src/app/services/label'; 4 | import {MatChipsModule} from '@angular/material/chips'; 5 | import { MatFormFieldModule } from '@angular/material/form-field'; 6 | import { MatIcon } from '@angular/material/icon'; 7 | import { FormsModule } from '@angular/forms'; 8 | import { CommonModule } from '@angular/common'; 9 | import { MatInputModule } from '@angular/material/input'; 10 | import { MatButtonModule } from '@angular/material/button'; 11 | 12 | @Component({ 13 | selector: 'app-labels', 14 | templateUrl: 'labels.html', 15 | styleUrls: ['labels.css'], 16 | imports: [MatChipsModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatIcon, FormsModule, CommonModule], 17 | }) 18 | export class LabelsComponent { 19 | showNewLabel?: boolean; 20 | label?: string; 21 | @Output() selectionChanged = new EventEmitter(); 22 | 23 | constructor(public labelService: LabelService) {} 24 | 25 | addNewLabel() { 26 | this.showNewLabel = true; 27 | } 28 | 29 | hideNewLabel() { 30 | this.showNewLabel = false; 31 | this.label = ''; 32 | } 33 | 34 | onChange(event: MatChipListboxChange) { 35 | console.log(event); 36 | this.selectionChanged.emit(event.value); 37 | } 38 | 39 | async saveLabel() { 40 | if (!this.label) { 41 | return; 42 | } 43 | 44 | this.labelService.saveLabel(this.label); 45 | 46 | this.label = ''; 47 | this.showNewLabel = false; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/shared/loading.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { Observable, of } from 'rxjs'; 3 | import { catchError, map, startWith } from 'rxjs/operators'; 4 | 5 | export interface WithStatusResult { 6 | loading?: boolean; 7 | value?: T; 8 | error?: string; 9 | } 10 | 11 | const defaultError = 'Whopsidaisy, something unexpected happened.'; 12 | 13 | @Pipe({ 14 | name: 'withStatus', 15 | }) 16 | export class WithStatusPipe implements PipeTransform { 17 | transform(val: Observable): Observable> { 18 | return val.pipe( 19 | map((value: any) => { 20 | return { 21 | loading: value.type === 'start', 22 | error: value.type === 'error' ? defaultError : '', 23 | value: value.type ? value.value : value, 24 | }; 25 | }), 26 | startWith({ loading: true }), 27 | catchError((error) => of({ loading: false, error: typeof error === 'string' ? error : defaultError })) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/message-bubble/message-bubble.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 | {{message.message}} 8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /src/app/shared/message-bubble/message-bubble.component.scss: -------------------------------------------------------------------------------- 1 | .bubble { 2 | $this: &; 3 | display: flex; 4 | flex-direction: row; 5 | margin-bottom: 12px; 6 | 7 | &.cover { 8 | width: 36px; 9 | height: 36px; 10 | margin-right: 16px; 11 | border-radius: 18px; 12 | overflow: hidden; 13 | } 14 | 15 | &.picture { 16 | display: block; 17 | max-width: 100%; 18 | margin-bottom: 0; 19 | } 20 | 21 | &.container { 22 | flex: 1; 23 | } 24 | 25 | &.card { 26 | display: inline-block !important; 27 | padding: 12px; 28 | } 29 | 30 | &.me { 31 | flex-direction: row-reverse; 32 | 33 | #{$this}.picture { 34 | display: block; 35 | max-width: 100%; 36 | } 37 | 38 | #{$this}.container { 39 | display: flex; 40 | justify-content: flex-end; 41 | 42 | .mat-card { 43 | background-color: rgba(255, 255, 255, 0.55);; 44 | } 45 | } 46 | 47 | #{$this}.cover { 48 | margin-right: 0; 49 | margin-left: 16px; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/shared/message-bubble/message-bubble.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | // import {MessageBubbleComponent} from './message-bubble.component'; 4 | 5 | // describe('MessageBubbleComponent', () => { 6 | // let component: MessageBubbleComponent; 7 | // let fixture: ComponentFixture; 8 | 9 | // beforeEach(async(() => { 10 | // TestBed.configureTestingModule({ 11 | // declarations: [MessageBubbleComponent] 12 | // }) 13 | // .compileComponents(); 14 | // })); 15 | 16 | // beforeEach(() => { 17 | // fixture = TestBed.createComponent(MessageBubbleComponent); 18 | // component = fixture.componentInstance; 19 | // fixture.detectChanges(); 20 | // }); 21 | 22 | // it('should create', () => { 23 | // expect(component).toBeTruthy(); 24 | // }); 25 | // }); 26 | -------------------------------------------------------------------------------- /src/app/shared/message-bubble/message-bubble.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | import { MatCardModule } from '@angular/material/card'; 3 | import { MessageModel } from 'src/app/services/interfaces'; 4 | 5 | 6 | @Component({ 7 | selector: 'app-message-bubble', 8 | templateUrl: './message-bubble.component.html', 9 | styleUrls: ['./message-bubble.component.scss'], 10 | imports: [MatCardModule], 11 | }) 12 | export class MessageBubbleComponent { 13 | @Input() message!: MessageModel; 14 | @Input() cover!: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/mobile-menu/mobile-menu.css: -------------------------------------------------------------------------------- 1 | .mobile-menu { 2 | display: flex; 3 | flex-direction: row; 4 | height: 56px; 5 | background-color: var(--mat-sidenav-container-background-color, var(--mat-sys-surface)); 6 | } 7 | 8 | .mobile-nav-button { 9 | padding: 4px; 10 | line-height: 1; 11 | font-size: 12px; 12 | } 13 | 14 | .mobile-nav-button { 15 | flex: 1; 16 | min-width: 0; 17 | } 18 | 19 | .mobile-nav-button-content { 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | } 24 | 25 | .mobile-footer { 26 | justify-content: space-around; 27 | padding: 0; 28 | height: 56px; 29 | } -------------------------------------------------------------------------------- /src/app/shared/mobile-menu/mobile-menu.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 9 | 15 | 22 | 28 | 29 |
-------------------------------------------------------------------------------- /src/app/shared/mobile-menu/mobile-menu.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, Input } from '@angular/core'; 2 | import { MatIconModule } from '@angular/material/icon'; 3 | import { CommonModule } from '@angular/common'; 4 | import { MatSliderModule } from '@angular/material/slider'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { MatButtonModule } from '@angular/material/button'; 7 | import { RouterModule } from '@angular/router'; 8 | import { MatToolbarModule } from '@angular/material/toolbar'; 9 | import { TranslateModule } from '@ngx-translate/core'; 10 | import { ApplicationState } from 'src/app/services/applicationstate'; 11 | import { MatBadgeModule } from '@angular/material/badge'; 12 | import { UIService } from 'src/app/services/ui'; 13 | 14 | @Component({ 15 | selector: 'app-mobile-menu', 16 | templateUrl: './mobile-menu.html', 17 | styleUrls: ['./mobile-menu.css'], 18 | imports: [MatToolbarModule, MatBadgeModule, TranslateModule, RouterModule, MatButtonModule, MatIconModule, CommonModule, MatSliderModule, FormsModule], 19 | }) 20 | export class MobileMenuComponent { 21 | @Input() miniplayer = false; 22 | 23 | appState = inject(ApplicationState); 24 | ui = inject(UIService); 25 | 26 | constructor() {} 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/app/shared/notification-label/notification-label.html: -------------------------------------------------------------------------------- 1 | 2 | {{ notification.message }} 3 | replied to your note. 4 | started following you. 5 | boosted your note 6 | reacted with {{notification.message}} to your note. 7 | -------------------------------------------------------------------------------- /src/app/shared/notification-label/notification-label.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { NotificationModel } from '../../services/interfaces'; 3 | import { ProfileNameComponent } from '../profile-name/profile-name'; 4 | import { CommonModule } from '@angular/common'; 5 | import { RouterModule } from '@angular/router'; 6 | 7 | @Component({ 8 | selector: 'app-notification-label', 9 | templateUrl: './notification-label.html', 10 | imports: [ProfileNameComponent, CommonModule, RouterModule], 11 | }) 12 | export class NotificationLabelComponent { 13 | @Input() notification: NotificationModel | any; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/shared/password-dialog/password-dialog.css: -------------------------------------------------------------------------------- 1 | .input-full-width { 2 | width: 100% !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/shared/password-dialog/password-dialog.html: -------------------------------------------------------------------------------- 1 |
2 |

Approve Action: {{ data.action }}

3 |
4 |

Do you want to approve action "{{ data.action }}"?

5 |
6 | 7 | person_add 8 | Public Key 9 | 10 | 11 | 12 | 13 | password 14 | Password 15 | 16 | 17 |
18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /src/app/shared/password-dialog/password-dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatDialog, MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; 5 | import { MatFormFieldModule } from '@angular/material/form-field'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatInputModule } from '@angular/material/input'; 8 | import { ApplicationState } from 'src/app/services/applicationstate'; 9 | 10 | export interface PasswordDialogData { 11 | password: string; 12 | action: string; 13 | } 14 | 15 | @Component({ 16 | selector: 'password-dialog', 17 | templateUrl: 'password-dialog.html', 18 | styleUrls: ['password-dialog.css'], 19 | imports: [FormsModule, MatIconModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatDialogModule], 20 | }) 21 | export class PasswordDialog { 22 | publicKey?: string; 23 | 24 | constructor(private appState: ApplicationState, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: PasswordDialogData) { 25 | this.publicKey = this.appState.getPublicKeyDisplay(); 26 | } 27 | 28 | onNoClick(): void { 29 | this.data.password = ''; 30 | this.dialogRef.close(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/shared/profile-actions/profile-actions.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/app/shared/profile-actions/profile-actions.css -------------------------------------------------------------------------------- /src/app/shared/profile-image-dialog/profile-image-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/shared/profile-image-dialog/profile-image-dialog.scss: -------------------------------------------------------------------------------- 1 | .profile-image-original { 2 | max-width: 100%; 3 | cursor: pointer; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/shared/profile-image-dialog/profile-image-dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | 4 | export interface ProfileImageDialogData { 5 | picture: string; 6 | } 7 | 8 | @Component({ 9 | selector: 'profile-image-dialog', 10 | templateUrl: 'profile-image-dialog.html', 11 | styleUrls: ['profile-image-dialog.scss'], 12 | }) 13 | export class ProfileImageDialog { 14 | constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: ProfileImageDialogData) {} 15 | 16 | onNoClick(): void { 17 | this.data.picture = ''; 18 | this.dialogRef.close(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/shared/profile-image/profile-image.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/shared/profile-image/profile-image.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { ProfileService } from 'src/app/services/profile'; 3 | import { NostrProfile } from '../../services/interfaces'; 4 | import { MatTooltipModule } from '@angular/material/tooltip'; 5 | 6 | @Component({ 7 | selector: 'app-profile-image', 8 | templateUrl: './profile-image.html', 9 | imports: [MatTooltipModule], 10 | }) 11 | export class ProfileImageComponent { 12 | @Input() publicKey: string = ''; 13 | 14 | imagePath = '/assets/profile.png'; 15 | tooltip = ''; 16 | 17 | constructor(private profiles: ProfileService) {} 18 | 19 | ngOnInit() { 20 | // const profile = this.profiles.profiles[this.publicKey] as NostrProfile; 21 | 22 | // if (!profile || !profile.picture) { 23 | // return; 24 | // } 25 | 26 | // // TODO: Just a basic protection of long urls, temporary. 27 | // if (profile.picture.length > 255) { 28 | // return; 29 | // } 30 | 31 | // this.imagePath = profile.picture; 32 | // this.tooltip = profile.about; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/shared/profile-name/profile-name.html: -------------------------------------------------------------------------------- 1 | 2 | {{ profileName }} 3 | {{ profileName }} 4 | 5 | -------------------------------------------------------------------------------- /src/app/shared/profile-name/profile-name.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { ProfileService } from 'src/app/services/profile'; 3 | import { StorageService } from 'src/app/services/storage'; 4 | import { Utilities } from 'src/app/services/utilities'; 5 | import { NostrProfile } from '../../services/interfaces'; 6 | import { RouterModule } from '@angular/router'; 7 | import { MatTooltipModule } from '@angular/material/tooltip'; 8 | import { CommonModule } from '@angular/common'; 9 | 10 | @Component({ 11 | selector: 'app-profile-name', 12 | templateUrl: './profile-name.html', 13 | imports: [RouterModule, MatTooltipModule, CommonModule], 14 | }) 15 | export class ProfileNameComponent { 16 | @Input() pubkey: string = ''; 17 | 18 | profileName? = ''; 19 | tooltip = ''; 20 | 21 | constructor(private db: StorageService, private profiles: ProfileService, private utilities: Utilities) {} 22 | 23 | async ngOnInit() { 24 | const profile = await this.db.storage.getProfile(this.pubkey); 25 | 26 | if (profile) { 27 | if (profile.display_name) { 28 | this.profileName = profile.display_name; 29 | } else { 30 | this.profileName = profile.name; 31 | } 32 | } else { 33 | this.profileName = this.utilities.getShortenedIdentifier(this.pubkey); 34 | } 35 | 36 | // this.profileName = this.utilities.getNostrIdentifier(this.publicKey); 37 | // const profile = this.profiles.profiles[this.publicKey] as NostrProfile; 38 | // if (!profile || !profile.name) { 39 | // return; 40 | // } 41 | // // TODO: Just a basic protection of long urls, temporary. 42 | // if (profile.name.length > 255) { 43 | // return; 44 | // } 45 | // this.tooltip = this.profileName; // Only set tooltip if we replace the publicKey with it. 46 | // this.profileName = profile.name; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/shared/profile-widget/profile-widget.css: -------------------------------------------------------------------------------- 1 | .profile-widget { 2 | gap: 0.4em; 3 | display: flex; 4 | flex-direction: row; 5 | height: 200px; 6 | } 7 | 8 | .profile-widget app-event-header { 9 | flex-grow: 2; 10 | } 11 | 12 | .profile-widget app-profile-actions { 13 | width: 48px; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/shared/profile-widget/profile-widget.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /src/app/shared/profile-widget/profile-widget.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { ProfileService } from 'src/app/services/profile'; 3 | import { Utilities } from 'src/app/services/utilities'; 4 | import { NostrProfile, NostrProfileDocument } from '../../services/interfaces'; 5 | import { EventHeaderComponent } from '../event-header/event-header'; 6 | import { ProfileActionsComponent } from '../profile-actions/profile-actions'; 7 | 8 | @Component({ 9 | selector: 'app-profile-widget', 10 | templateUrl: './profile-widget.html', 11 | styleUrls: ['./profile-widget.css'], 12 | imports: [EventHeaderComponent, ProfileActionsComponent], 13 | }) 14 | export class ProfileWidgetComponent { 15 | @Input() pubkey: string = ''; 16 | 17 | profileName = ''; 18 | tooltip = ''; 19 | profile!: NostrProfileDocument; 20 | 21 | constructor(private profiles: ProfileService, private utilities: Utilities) {} 22 | 23 | async ngOnInit() { 24 | if(this.pubkey){ 25 | this.profile = await this.profiles.getProfile(this.pubkey) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/shared/relay-list/relay-list.html: -------------------------------------------------------------------------------- 1 | {{ relay }}, 2 | -------------------------------------------------------------------------------- /src/app/shared/relay-list/relay-list.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input } from '@angular/core'; 3 | import { Utilities } from 'src/app/services/utilities'; 4 | 5 | @Component({ 6 | selector: 'app-relay-list', 7 | templateUrl: './relay-list.html', 8 | imports: [CommonModule], 9 | }) 10 | export class RelayListComponent { 11 | @Input() relays: any; 12 | relayNames: string[] = []; 13 | 14 | constructor(private utilities: Utilities) {} 15 | 16 | async ngOnInit() { 17 | this.relayNames = []; 18 | 19 | if (this.relays == null) { 20 | return; 21 | } 22 | 23 | const relayJson = JSON.parse(this.relays); 24 | this.relayNames = this.utilities.getRelayUrls(relayJson); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/relay/relay.css: -------------------------------------------------------------------------------- 1 | .mat-expansion-panel-header-description { 2 | justify-content: space-between; 3 | align-items: center; 4 | } 5 | 6 | .mat-expansion-panel { 7 | margin-bottom: 0.4em; 8 | } 9 | 10 | .relay-status--1 { 11 | color: rgb(49, 49, 210); 12 | } 13 | 14 | .relay-status-0 { 15 | color: gray; 16 | } 17 | 18 | .relay-status-1 { 19 | color: rgb(34, 179, 34); 20 | } 21 | 22 | .relay-status-2 { 23 | color: orange; 24 | } 25 | 26 | .relay-status-3 { 27 | color: red; 28 | } 29 | 30 | .relay-status-4 { 31 | color: rgb(49, 49, 210); 32 | } 33 | 34 | .relay-status-icon { 35 | margin-left: 0.2em; 36 | } 37 | 38 | /* .relay-type-0 { 39 | color: rgb(234, 136, 9) !important; 40 | } 41 | 42 | .relay-type-1 { 43 | } 44 | 45 | .relay-type-2 { 46 | color: rgb(49, 49, 210) !important; 47 | } */ 48 | 49 | .primary-relay { 50 | color: rgb(198, 3, 181); 51 | } 52 | 53 | .relay-options { 54 | margin-top: 1em; 55 | margin-bottom: 0.6em; 56 | background-color: rgba(255, 255, 255, 0.1); 57 | } 58 | 59 | .settings-action-buttons { 60 | padding-top: 0.8em; 61 | padding-bottom: 1em; 62 | } 63 | 64 | .settings-action-buttons button { 65 | margin-bottom: 1em; 66 | margin-right: 1em; 67 | } 68 | 69 | /* When changing the sidenav-content to flex, the toolbar does not render properly, so a minor hack is needed. */ 70 | @media only screen and (max-width: 599px) { 71 | .settings-action-buttons button { 72 | width: 100%; 73 | margin-right: 0; 74 | } 75 | 76 | .mat-expansion-panel-header-title { 77 | flex-grow: 3 !important; 78 | } 79 | 80 | .mat-expansion-panel-header-description { 81 | flex-grow: 1 !important; 82 | } 83 | } 84 | 85 | .relay-button { 86 | margin-top: 0.8em; 87 | } 88 | .options-slider { 89 | margin-left: 1em; 90 | } 91 | 92 | .relay-favicon { 93 | margin-right: 1em; 94 | } 95 | -------------------------------------------------------------------------------- /src/app/shared/relays/relays.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/shared/relays/relays.html: -------------------------------------------------------------------------------- 1 | 2 | @for (relay of relays; track trackByFn($index, relay)) { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /src/app/shared/relays/relays.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { MatExpansionModule } from "@angular/material/expansion"; 3 | import { NostrRelayDocument } from "src/app/services/interfaces"; 4 | import { RelayComponent } from "../relay/relay"; 5 | import { MatButtonModule } from "@angular/material/button"; 6 | 7 | @Component({ 8 | selector: 'app-relays', 9 | templateUrl: './relays.html', 10 | styleUrls: ['./relays.css'], 11 | inputs: ['relays'], 12 | imports: [MatExpansionModule, MatButtonModule, RelayComponent], 13 | }) 14 | export class RelaysComponent { 15 | relays?: NostrRelayDocument[]; 16 | 17 | constructor() { } 18 | 19 | ngOnInit() { } 20 | 21 | public trackByFn(index: number, item: NostrRelayDocument) { 22 | return `${item.url}${item.modified}`; 23 | } 24 | } -------------------------------------------------------------------------------- /src/app/shared/reply-list/reply-list.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/app/shared/reply-list/reply-list.css -------------------------------------------------------------------------------- /src/app/shared/reply-list/reply-list.html: -------------------------------------------------------------------------------- 1 | 2 | {{ profile.name }}, 4 | 5 | -------------------------------------------------------------------------------- /src/app/shared/reply-list/reply-list.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { ProfileService } from 'src/app/services/profile'; 5 | import { Utilities } from 'src/app/services/utilities'; 6 | 7 | export interface ReplyEntry { 8 | pubkey: string; 9 | name: string; 10 | } 11 | 12 | @Component({ 13 | selector: 'app-reply-list', 14 | templateUrl: 'reply-list.html', 15 | styleUrls: ['reply-list.css'], 16 | imports: [RouterModule, CommonModule], 17 | }) 18 | export class ReplyListComponent { 19 | @Input() keys: string[] = []; 20 | profiles: ReplyEntry[] = []; 21 | 22 | constructor(private profileService: ProfileService, private utilities: Utilities) {} 23 | 24 | async ngOnInit() { 25 | this.profiles = []; 26 | 27 | for (let i = 0; i < this.keys.length; i++) { 28 | const key = this.keys[i]; 29 | 30 | // Key was at one time "0" so ensure we skip empties. 31 | if (key == null || key === '0') { 32 | continue; 33 | } 34 | 35 | const profile = await this.profileService.getProfile(key); 36 | 37 | if (profile) { 38 | this.profiles.push({ pubkey: profile.pubkey, name: profile.name }); 39 | } else { 40 | this.profiles.push({ pubkey: key, name: key }); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/shared/status/status.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{text}} 3 | 4 | -------------------------------------------------------------------------------- /src/app/shared/status/status.component.scss: -------------------------------------------------------------------------------- 1 | .status { 2 | &:before { 3 | content: ''; 4 | display: inline-block; 5 | width: 4px; 6 | height: 4px; 7 | border-radius: 4px; 8 | position: relative; 9 | top: -4px; 10 | margin-right: 3px; 11 | } 12 | 13 | &--online { 14 | $color: #27f32f; 15 | 16 | &:before { 17 | background-color: $color; 18 | } 19 | 20 | color: $color; 21 | } 22 | 23 | &--offline { 24 | $color: #acacac; 25 | 26 | &:before { 27 | background-color: $color; 28 | } 29 | 30 | color: $color; 31 | } 32 | 33 | &--busy { 34 | $color: #ff451e; 35 | 36 | &:before { 37 | background-color: $color; 38 | } 39 | 40 | color: $color; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/shared/status/status.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | // import {StatusComponent} from './status.component'; 4 | 5 | // describe('StatusComponent', () => { 6 | // let component: StatusComponent; 7 | // let fixture: ComponentFixture; 8 | 9 | // beforeEach(async(() => { 10 | // TestBed.configureTestingModule({ 11 | // declarations: [StatusComponent] 12 | // }) 13 | // .compileComponents(); 14 | // })); 15 | 16 | // beforeEach(() => { 17 | // fixture = TestBed.createComponent(StatusComponent); 18 | // component = fixture.componentInstance; 19 | // fixture.detectChanges(); 20 | // }); 21 | 22 | // it('should create', () => { 23 | // expect(component).toBeTruthy(); 24 | // }); 25 | // }); 26 | -------------------------------------------------------------------------------- /src/app/shared/status/status.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-status', 5 | templateUrl: './status.component.html', 6 | styleUrls: ['./status.component.scss'] 7 | }) 8 | export class StatusComponent { 9 | @Input() type: "online" | "busy" | "offline" = "online"; 10 | @Input() text!: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/tags/tags.css: -------------------------------------------------------------------------------- 1 | .tags-chip-list { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/shared/tags/tags.html: -------------------------------------------------------------------------------- 1 | 2 | Tags 3 | 4 | 5 | {{tag}} 6 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/shared/tags/tags.ts: -------------------------------------------------------------------------------- 1 | import { COMMA, ENTER } from '@angular/cdk/keycodes'; 2 | import { Component, Input } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { MatChipEditedEvent, MatChipInputEvent, MatChipsModule } from '@angular/material/chips'; 5 | import { MatFormFieldModule } from '@angular/material/form-field'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatInputModule } from '@angular/material/input'; 8 | 9 | @Component({ 10 | selector: 'app-tags', 11 | templateUrl: 'tags.html', 12 | styleUrls: ['tags.css'], 13 | imports: [FormsModule, MatFormFieldModule, MatInputModule, MatChipsModule, MatIconModule], 14 | }) 15 | export class TagsComponent { 16 | addOnBlur = true; 17 | readonly separatorKeysCodes = [ENTER, COMMA] as const; 18 | @Input() tags: string[] = []; 19 | 20 | add(event: MatChipInputEvent): void { 21 | const value = (event.value || '').trim(); 22 | 23 | // Add our tag 24 | if (value) { 25 | this.tags.push(value); 26 | } 27 | 28 | // Clear the input value 29 | event.chipInput!.clear(); 30 | } 31 | 32 | remove(tag: string): void { 33 | const index = this.tags.indexOf(tag); 34 | 35 | if (index >= 0) { 36 | this.tags.splice(index, 1); 37 | } 38 | } 39 | 40 | edit(tag: string, event: MatChipEditedEvent) { 41 | const value = event.value.trim(); 42 | 43 | // Remove tag if it no longer has a name 44 | if (!value) { 45 | this.remove(tag); 46 | return; 47 | } 48 | 49 | // Edit existing tag 50 | const index = this.tags.indexOf(tag); 51 | if (index >= 0) { 52 | this.tags[index] = value; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/shared/time.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { Utilities } from '../services/utilities'; 3 | 4 | @Pipe({ name: 'time' }) 5 | export class TimePipe implements PipeTransform { 6 | constructor(private utilities: Utilities) {} 7 | 8 | transform(value?: number): string { 9 | if (!value) { 10 | return '00:00:00'; 11 | } 12 | 13 | return TimePipe.time(value); 14 | } 15 | 16 | static time(value: number) { 17 | var hours = Math.floor(value / 60 / 60); 18 | var minutes = Math.floor(value / 60) - hours * 60; 19 | var seconds = value % 60; 20 | var formatted = hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0'); 21 | return formatted; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/shared/user-item/user-item.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

{{ user.name }}

4 |

5 | 6 |

7 |
8 | -------------------------------------------------------------------------------- /src/app/shared/user-item/user-item.component.scss: -------------------------------------------------------------------------------- 1 | mat-list-item{ 2 | height: 90px !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/shared/user-item/user-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | // import {UserItemComponent} from './user-item.component'; 4 | 5 | // describe('UserItemComponent', () => { 6 | // let component: UserItemComponent; 7 | // let fixture: ComponentFixture; 8 | 9 | // beforeEach(async(() => { 10 | // TestBed.configureTestingModule({ 11 | // declarations: [UserItemComponent] 12 | // }) 13 | // .compileComponents(); 14 | // })); 15 | 16 | // beforeEach(() => { 17 | // fixture = TestBed.createComponent(UserItemComponent); 18 | // component = fixture.componentInstance; 19 | // fixture.detectChanges(); 20 | // }); 21 | 22 | // it('should create', () => { 23 | // expect(component).toBeTruthy(); 24 | // }); 25 | // }); 26 | -------------------------------------------------------------------------------- /src/app/shared/user-item/user-item.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, Output} from '@angular/core'; 2 | import { MatListModule } from '@angular/material/list'; 3 | import { UserModel } from 'src/app/services/interfaces'; 4 | import { UserService } from 'src/app/services/user.service'; 5 | import { StatusComponent } from '../status/status.component'; 6 | 7 | 8 | @Component({ 9 | selector: 'app-user-item', 10 | templateUrl: './user-item.component.html', 11 | styleUrls: ['./user-item.component.scss'], 12 | imports: [MatListModule, StatusComponent], 13 | }) 14 | export class UserItemComponent { 15 | @Output() openSidebar: EventEmitter = new EventEmitter(); 16 | @Input()user!: UserModel; 17 | 18 | 19 | constructor(private service: UserService) { 20 | } 21 | 22 | showProfile() { 23 | this.service.active.next(this.user); 24 | this.openSidebar.emit(this.user.name); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/user-list/user-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/shared/user-list/user-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/app/shared/user-list/user-list.component.scss -------------------------------------------------------------------------------- /src/app/shared/user-list/user-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | // import {UserListComponent} from './user-list.component'; 4 | 5 | // describe('UserListComponent', () => { 6 | // let component: UserListComponent; 7 | // let fixture: ComponentFixture; 8 | 9 | // beforeEach(async(() => { 10 | // TestBed.configureTestingModule({ 11 | // declarations: [UserListComponent] 12 | // }) 13 | // .compileComponents(); 14 | // })); 15 | 16 | // beforeEach(() => { 17 | // fixture = TestBed.createComponent(UserListComponent); 18 | // component = fixture.componentInstance; 19 | // fixture.detectChanges(); 20 | // }); 21 | 22 | // it('should create', () => { 23 | // expect(component).toBeTruthy(); 24 | // }); 25 | // }); 26 | -------------------------------------------------------------------------------- /src/app/shared/user-list/user-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, OnInit, Output } from '@angular/core'; 2 | import { MatListModule } from '@angular/material/list'; 3 | import { UserModel } from 'src/app/services/interfaces'; 4 | import { UserService } from 'src/app/services/user.service'; 5 | import { UserItemComponent } from '../user-item/user-item.component'; 6 | import { CommonModule } from '@angular/common'; 7 | 8 | @Component({ 9 | selector: 'app-user-list', 10 | templateUrl: './user-list.component.html', 11 | styleUrls: ['./user-list.component.scss'], 12 | imports: [MatListModule, UserItemComponent, CommonModule], 13 | }) 14 | export class UserListComponent implements OnInit { 15 | @Output('openUserSidebar') openSidebar: EventEmitter = new EventEmitter(); 16 | data: Array = []; 17 | 18 | constructor(private service: UserService) { 19 | this.service.getUsers(); 20 | } 21 | 22 | ngOnInit() { 23 | this.service.users.subscribe((users) => (this.data = users)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shared/user-profile/user-profile.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 |

{{ user.username }}

9 |
10 | 11 |
12 |

Biography

13 |

{{ user.bio }}

14 |
15 | -------------------------------------------------------------------------------- /src/app/shared/user-profile/user-profile.component.scss: -------------------------------------------------------------------------------- 1 | .profile { 2 | padding: 16px; 3 | 4 | &__picture-container { 5 | width: 180px; 6 | height: 180px; 7 | border-radius: 120px; 8 | overflow: hidden; 9 | margin: 0 auto 12px; 10 | } 11 | 12 | &__picture { 13 | width: 100%; 14 | height: auto; 15 | } 16 | 17 | &__username { 18 | margin: 0; 19 | } 20 | 21 | &__status { 22 | margin-bottom: 16px; 23 | } 24 | 25 | &__button { 26 | margin: 8px auto 16px; 27 | display: flex; 28 | justify-content: center; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/user-profile/user-profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | // import {UserProfileComponent} from './user-profile.component'; 4 | 5 | // describe('UserProfileComponent', () => { 6 | // let component: UserProfileComponent; 7 | // let fixture: ComponentFixture; 8 | 9 | // beforeEach(async(() => { 10 | // TestBed.configureTestingModule({ 11 | // declarations: [UserProfileComponent] 12 | // }) 13 | // .compileComponents(); 14 | // })); 15 | 16 | // beforeEach(() => { 17 | // fixture = TestBed.createComponent(UserProfileComponent); 18 | // component = fixture.componentInstance; 19 | // fixture.detectChanges(); 20 | // }); 21 | 22 | // it('should create', () => { 23 | // expect(component).toBeTruthy(); 24 | // }); 25 | // }); 26 | -------------------------------------------------------------------------------- /src/app/shared/user-profile/user-profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; 2 | import { Subscription } from 'rxjs'; 3 | import { ChatService } from 'src/app/services/chat.service'; 4 | import { UserModel } from 'src/app/services/interfaces'; 5 | import { UserService } from 'src/app/services/user.service'; 6 | import { StatusComponent } from '../status/status.component'; 7 | import { CommonModule } from '@angular/common'; 8 | 9 | @Component({ 10 | selector: 'app-user-profile', 11 | templateUrl: './user-profile.component.html', 12 | styleUrls: ['./user-profile.component.scss'], 13 | imports: [StatusComponent, CommonModule], 14 | }) 15 | export class UserProfileComponent implements OnInit, OnDestroy { 16 | @Output() openChatWindow: EventEmitter = new EventEmitter(); 17 | user!: UserModel; 18 | subscription!: Subscription; 19 | 20 | constructor(private service: UserService, private messageService: ChatService) {} 21 | 22 | openNewChat() { 23 | this.messageService.createChat(this.user); 24 | this.openChatWindow.emit(this.user.username); 25 | } 26 | 27 | ngOnInit() { 28 | this.subscription = this.service.active.subscribe((user) => (this.user = user)); 29 | } 30 | 31 | ngOnDestroy(): void { 32 | this.subscription.unsubscribe(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/shared/username.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { ProfileService } from '../services/profile'; 3 | import { Utilities } from '../services/utilities'; 4 | 5 | @Pipe({ name: 'username' }) 6 | export class UsernamePipe implements PipeTransform { 7 | constructor(private profileService: ProfileService, private utilities: Utilities) {} 8 | 9 | transform(value: string) { 10 | if (!value) { 11 | return value; 12 | } 13 | 14 | const profile = this.profileService.getCachedProfile(value); 15 | 16 | if (!profile) { 17 | return this.utilities.getShortenedIdentifier(value); 18 | } else { 19 | return this.utilities.getProfileDisplayName(profile); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/shared/utilities.ts: -------------------------------------------------------------------------------- 1 | import * as Rx from "rxjs"; 2 | // import Dexie, { liveQuery, Observable } from "dexie"; 3 | 4 | // export function dexieToRx(o: Observable): Rx.Observable { 5 | // return new Rx.Observable(observer => { 6 | // const subscription = o.subscribe({ 7 | // next: value => observer.next(value), 8 | // error: error => observer.error(error) 9 | // }); 10 | // return () => subscription.unsubscribe(); 11 | // }); 12 | // } 13 | 14 | export function copyToClipboard(content: string) { 15 | var textArea = document.createElement('textarea') as any; 16 | 17 | // Place in the top-left corner of screen regardless of scroll position. 18 | textArea.style.position = 'fixed'; 19 | textArea.style.top = 0; 20 | textArea.style.left = 0; 21 | 22 | // Ensure it has a small width and height. Setting to 1px / 1em 23 | // doesn't work as this gives a negative w/h on some browsers. 24 | textArea.style.width = '2em'; 25 | textArea.style.height = '2em'; 26 | 27 | // We don't need padding, reducing the size if it does flash render. 28 | textArea.style.padding = 0; 29 | 30 | // Clean up any borders. 31 | textArea.style.border = 'none'; 32 | textArea.style.outline = 'none'; 33 | textArea.style.boxShadow = 'none'; 34 | 35 | // Avoid flash of the white box if rendered for any reason. 36 | textArea.style.background = 'transparent'; 37 | textArea.value = content; 38 | 39 | document.body.appendChild(textArea); 40 | textArea.focus(); 41 | textArea.select(); 42 | 43 | try { 44 | var successful = document.execCommand('copy'); 45 | var msg = successful ? 'successful' : 'unsuccessful'; 46 | } catch (err) { 47 | console.error('Oops, unable to copy'); 48 | } 49 | 50 | document.body.removeChild(textArea); 51 | } 52 | -------------------------------------------------------------------------------- /src/app/shared/zap-dialog/zap-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | // import { ZapDialogComponent } from './zap-dialog.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 | -------------------------------------------------------------------------------- /src/app/shared/zap-qr-code/zap-qr-code.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 12 | 13 |
14 |
15 | Send Zaps to {{ profileName }} 16 |
17 |
18 |
19 | 20 |
21 | 24 |
25 | content_copy 26 |
27 |
28 |
29 |
-------------------------------------------------------------------------------- /src/app/shared/zap-qr-code/zap-qr-code.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .profile-container { 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | margin-bottom: 30px; 7 | } 8 | 9 | .icon { 10 | width: 40px; 11 | height: 40px; 12 | border-radius: 50%; 13 | overflow: hidden; 14 | margin-right: 10px; 15 | } 16 | 17 | .profile-image { 18 | width: 100%; 19 | height: 100%; 20 | object-fit: cover; 21 | border: unset; 22 | } 23 | 24 | .name { 25 | font-size: 18px; 26 | font-weight: bold; 27 | text-align: center; 28 | } 29 | 30 | .mdc-dialog__actions { 31 | padding: 0px; 32 | } -------------------------------------------------------------------------------- /src/app/shared/zap-qr-code/zap-qr-code.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | // import { ZapQrCodeComponent } from './zap-qr-code.component'; 4 | 5 | // describe('ZapQrCodeComponent', () => { 6 | // let component: ZapQrCodeComponent; 7 | // let fixture: ComponentFixture; 8 | 9 | // beforeEach(async () => { 10 | // await TestBed.configureTestingModule({ 11 | // declarations: [ ZapQrCodeComponent ] 12 | // }) 13 | // .compileComponents(); 14 | 15 | // fixture = TestBed.createComponent(ZapQrCodeComponent); 16 | // component = fixture.componentInstance; 17 | // fixture.detectChanges(); 18 | // }); 19 | 20 | // it('should create', () => { 21 | // expect(component).toBeTruthy(); 22 | // }); 23 | // }); 24 | -------------------------------------------------------------------------------- /src/app/shared/zappers-list-dialog/zappers-list-dialog.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Zappers

3 |
4 |
5 |
6 | 14 | 15 |
16 |
17 |
18 | {{ zapper.profile.display_name }} 19 |
20 |
21 |
22 |
23 | ⚡️ {{ zapper.zap.amount }} 24 |
25 | 26 |
27 |
28 | 29 | -------------------------------------------------------------------------------- /src/app/shared/zappers-list-dialog/zappers-list-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ZappersListDialogComponent } from './zappers-list-dialog.component'; 4 | 5 | describe('ZappersListDialogComponent', () => { 6 | let component: ZappersListDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ZappersListDialogComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ZappersListDialogComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/types/table.ts: -------------------------------------------------------------------------------- 1 | // import { Database } from './database'; 2 | 3 | // export class Table { 4 | // constructor(private db: Database, private name: string) {} 5 | 6 | // async get(key: any) { 7 | // return this.db.db.get(this.name, key); 8 | // } 9 | 10 | // async put(key: any, value: any) { 11 | // return this.db.db.put(this.name, value, key); 12 | // } 13 | // } 14 | -------------------------------------------------------------------------------- /src/app/workers/nip05.worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | addEventListener('message', ({ data }) => { 4 | const response = `worker response to ${data}`; 5 | 6 | postMessage(response); 7 | }); 8 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/badge-1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/badge-1024x1024.png -------------------------------------------------------------------------------- /src/assets/badge-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/badge-128x128.png -------------------------------------------------------------------------------- /src/assets/badge-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/badge-512x512.png -------------------------------------------------------------------------------- /src/assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/bg.jpg -------------------------------------------------------------------------------- /src/assets/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/blank.png -------------------------------------------------------------------------------- /src/assets/blockcore-light-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/blockcore-light-small.png -------------------------------------------------------------------------------- /src/assets/blockcore-notes-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/blockcore-notes-screenshot.png -------------------------------------------------------------------------------- /src/assets/blockcore-notes-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/blockcore-notes-social.png -------------------------------------------------------------------------------- /src/assets/chat-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/chat-bg.png -------------------------------------------------------------------------------- /src/assets/gradient.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/gradient.jpg -------------------------------------------------------------------------------- /src/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/assets/icons/icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-120x120.png -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-128x128.webp -------------------------------------------------------------------------------- /src/assets/icons/icon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-16x16.png -------------------------------------------------------------------------------- /src/assets/icons/icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-180x180.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-2048x2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-2048x2048.png -------------------------------------------------------------------------------- /src/assets/icons/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-256x256.png -------------------------------------------------------------------------------- /src/assets/icons/icon-256x256.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-256x256.webp -------------------------------------------------------------------------------- /src/assets/icons/icon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-32x32.png -------------------------------------------------------------------------------- /src/assets/icons/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-48x48.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/assets/icons/icon-light-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/icons/icon-light-128x128.png -------------------------------------------------------------------------------- /src/assets/logos/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/logos/youtube.png -------------------------------------------------------------------------------- /src/assets/logos/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/nostritch.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/nostritch.avif -------------------------------------------------------------------------------- /src/assets/nostritch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/nostritch.jpg -------------------------------------------------------------------------------- /src/assets/post.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/profile-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/profile-bg.png -------------------------------------------------------------------------------- /src/assets/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/assets/profile.png -------------------------------------------------------------------------------- /src/assets/shared.worker.js: -------------------------------------------------------------------------------- 1 | //shared.worker.js 2 | 3 | connections = []; 4 | 5 | self.onconnect = (connectEvent) => { 6 | const port = connectEvent.ports[0]; 7 | 8 | port.start(); 9 | connections.push(port); 10 | 11 | port.onmessage = (messageEvent) => { 12 | connections.forEach((connection) => { 13 | connection.postMessage(messageEvent.data); 14 | }); 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Blockcore Notes - Share and keep notes on decentralized platforms 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | 30 |
31 |
Blockcore Notes loading...
32 | 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | // import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | // import { AppModule } from './app/app.module'; 3 | 4 | // platformBrowserDynamic() 5 | // .bootstrapModule(AppModule) 6 | // .catch((err) => console.error(err)); 7 | 8 | import { bootstrapApplication } from '@angular/platform-browser'; 9 | import { appConfig } from './app/app.config'; 10 | import { AppComponent } from './app/app'; 11 | 12 | bootstrapApplication(AppComponent, appConfig) 13 | .catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /src/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'light-bolt11-decoder'; 2 | -------------------------------------------------------------------------------- /store/blockcore-notes-feature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/store/blockcore-notes-feature.png -------------------------------------------------------------------------------- /store/blockcore-notes-people-phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/store/blockcore-notes-people-phone.png -------------------------------------------------------------------------------- /store/blockcore-notes-profile-phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/store/blockcore-notes-profile-phone.png -------------------------------------------------------------------------------- /store/blockcore-notes-thread-phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/block-core/blockcore-notes/fe3a8e33c5016c1c832db91ab95757096f75ffae/store/blockcore-notes-thread-phone.png -------------------------------------------------------------------------------- /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": ["src/main.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": false, 14 | "sourceMap": true, 15 | "declaration": false, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "bundler", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | }, 23 | "angularCompilerOptions": { 24 | "enableI18nLegacyMessageIdFormat": false, 25 | "strictInjectionParameters": true, 26 | "strictInputAccessModifiers": true, 27 | "strictTemplates": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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 | "files": [], 9 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.worker.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/worker", 6 | "lib": ["es2018", "webworker"], 7 | "types": [] 8 | }, 9 | "include": ["src/**/*.worker.ts"] 10 | } 11 | --------------------------------------------------------------------------------