├── .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 | {{ 'Badge.IssueBadge' | translate }}
10 | {{ 'Badge.Edit' | translate }}
11 |
12 |
13 |
14 |
15 | {{ 'Badge.PubKeyDescription' | translate }}
16 |
17 |
18 |
19 | {{'Badge.PublishBadgeReward' | translate }}
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 |
19 |
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 | I don't consent
22 | I consent
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 |
5 | cameraswitch
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 | close
18 |
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 | Import my latest contacts
8 |
9 |
10 | Import from file:
11 |
12 |
13 |
14 |
19 |
20 |
21 |
22 | Cancel
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 |
2 | Followers
3 | Following
4 |
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 |
3 | Followers
4 | Following
5 |
6 |
7 |
10 |
11 |
12 |
13 |
20 |
21 |
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 |
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 | {{'Notifications.ResetNotifications' | translate}}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
29 | {{'Notifications.ShowMore' | translate }}
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 | Import my latest contacts
8 |
9 |
10 | Import from file:
11 |
12 |
13 |
14 |
19 |
20 |
21 |
22 | Cancel
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 | Cancel
13 | Add
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 |
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 | {{'Relays.AppendFromExtension' | translate }} {{ 'Relays.AppendFromApp' | translate }}
14 |
15 | {{ 'Relays.PublishRelay(and following)list' | translate}} {{'Relays.PublishRelay(NIP-65)list' | translate}}
17 | {{'Relays.DeleteAllRelays' | translate}}
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 | {{ 'RelayDialog.Cancel' | translate }}
16 | {{ 'RelayDialog.Add' | translate }}
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 |
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 |
4 |
5 |
6 | search
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
21 | {{ chat.pubkey }} : {{ chat.content }}
22 |
23 |
26 |
27 |
30 |
31 | Add
32 |
33 | Reset
34 |
35 | Download
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 |
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 | Cancel
34 | Save
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 | Cancel
13 | Follow
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 |
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 |
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 |
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 |
9 | done
10 |
11 |
12 | close
13 |
14 |
15 |
16 | add
17 |
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 |
--------------------------------------------------------------------------------
/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 | 7"> {{ 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 |
19 |
20 |
21 | Cancel
22 | Approve
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 |
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 |
7 | cancel
8 |
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 | Write Message
7 |
8 |
{{ user.username }}
9 |
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 |
You must enable JavaScript to use Blockcore Notes.
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 |
--------------------------------------------------------------------------------