├── src
├── boot
│ ├── .gitkeep
│ └── components.js
├── css
│ ├── app.scss
│ ├── app.sass
│ ├── quasar.variables.scss
│ └── quasar.variables.sass
├── assets
│ ├── nostr.png
│ ├── nostr-logo.png
│ ├── nostr-layout.png
│ ├── 1280px-Markdown-mark.svg.png
│ └── quasar-logo-full.svg
├── global.js
├── App.vue
├── pages
│ ├── PageNotifications.vue
│ ├── PageHome.vue
│ ├── Error404.vue
│ ├── PageMessages.vue
│ ├── PageHelp.vue
│ ├── PageProfile.vue
│ ├── PageChat.vue
│ └── PageSettings.vue
├── store
│ ├── store-flag.d.ts
│ ├── state.js
│ ├── storage.js
│ ├── index.js
│ ├── getters.js
│ ├── mutations.js
│ └── actions.js
├── utils
│ ├── mixin.js
│ ├── emojis.js
│ └── nip04.js
├── router
│ ├── index.js
│ └── routes.js
├── index.template.html
├── components
│ ├── Post.vue
│ ├── Publish.vue
│ ├── Reply.vue
│ └── Generate.vue
└── layouts
│ └── MainLayout.vue
├── public
├── favicon.ico
└── icons
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon-96x96.png
│ ├── icon-128x128.png
│ ├── icon-192x192.png
│ ├── icon-256x256.png
│ ├── icon-384x384.png
│ ├── icon-512x512.png
│ ├── favicon-128x128.png
│ ├── ms-icon-144x144.png
│ ├── apple-icon-120x120.png
│ ├── apple-icon-152x152.png
│ ├── apple-icon-167x167.png
│ ├── apple-icon-180x180.png
│ ├── apple-launch-1125x2436.png
│ ├── apple-launch-1242x2208.png
│ ├── apple-launch-1242x2688.png
│ ├── apple-launch-1536x2048.png
│ ├── apple-launch-1668x2224.png
│ ├── apple-launch-1668x2388.png
│ ├── apple-launch-2048x2732.png
│ ├── apple-launch-640x1136.png
│ ├── apple-launch-750x1334.png
│ ├── apple-launch-828x1792.png
│ └── safari-pinned-tab.svg
├── babel.config.js
├── .vscode
├── settings.json
└── extensions.json
├── .editorconfig
├── src-pwa
├── custom-service-worker.js
├── pwa-flag.d.ts
└── register-service-worker.js
├── .prettierrc.yaml
├── .postcssrc.js
├── jsconfig.json
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── .eslintrc.json
└── quasar.conf.js
/src/boot/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/css/app.scss:
--------------------------------------------------------------------------------
1 | // app global css in SCSS form
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/nostr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/src/assets/nostr.png
--------------------------------------------------------------------------------
/src/assets/nostr-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/src/assets/nostr-logo.png
--------------------------------------------------------------------------------
/src/global.js:
--------------------------------------------------------------------------------
1 | import {relayPool} from 'nostr-tools'
2 |
3 | export const pool = relayPool()
4 |
--------------------------------------------------------------------------------
/src/assets/nostr-layout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/src/assets/nostr-layout.png
--------------------------------------------------------------------------------
/public/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/icons/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/favicon-96x96.png
--------------------------------------------------------------------------------
/public/icons/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/icon-128x128.png
--------------------------------------------------------------------------------
/public/icons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/icon-192x192.png
--------------------------------------------------------------------------------
/public/icons/icon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/icon-256x256.png
--------------------------------------------------------------------------------
/public/icons/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/icon-384x384.png
--------------------------------------------------------------------------------
/public/icons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/icon-512x512.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | presets: [
4 | '@quasar/babel-preset-app'
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/public/icons/favicon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/favicon-128x128.png
--------------------------------------------------------------------------------
/public/icons/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/ms-icon-144x144.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-icon-120x120.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-icon-152x152.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-167x167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-icon-167x167.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-icon-180x180.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1125x2436.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-launch-1125x2436.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1242x2208.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-launch-1242x2208.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1242x2688.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-launch-1242x2688.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1536x2048.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-launch-1536x2048.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1668x2224.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-launch-1668x2224.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1668x2388.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-launch-1668x2388.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-2048x2732.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-launch-2048x2732.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-640x1136.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-launch-640x1136.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-750x1334.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-launch-750x1334.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-828x1792.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/public/icons/apple-launch-828x1792.png
--------------------------------------------------------------------------------
/src/assets/1280px-Markdown-mark.svg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arcbtc/nostr/HEAD/src/assets/1280px-Markdown-mark.svg.png
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "vetur.experimental.templateInterpolationService": true,
3 | "editor.defaultFormatter": "esbenp.prettier-vscode",
4 | "editor.formatOnSave": true
5 | }
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/src-pwa/custom-service-worker.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file (which will be your service worker)
3 | * is picked up by the build system ONLY if
4 | * quasar.conf > pwa > workboxPluginMode is set to "InjectManifest"
5 | */
6 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | arrowParens: avoid
2 | bracketSpacing: false
3 | jsxBracketSameLine: false
4 | printWidth: 80
5 | proseWrap: preserve
6 | semi: false
7 | singleQuote: true
8 | trailingComma: none
9 | useTabs: false
10 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | plugins: [
5 | // to edit target browsers: use "browserslist" field in package.json
6 | require('autoprefixer')
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 |
--------------------------------------------------------------------------------
/src/pages/PageNotifications.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Notifications
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "octref.vetur"
4 | ],
5 | "unwantedRecommendations": [
6 | "hookyqr.beautify",
7 | "dbaeumer.jshint",
8 | "ms-vscode.vscode-typescript-tslint-plugin"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/src-pwa/pwa-flag.d.ts:
--------------------------------------------------------------------------------
1 | // THIS FEATURE-FLAG FILE IS AUTOGENERATED,
2 | // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
3 | import "quasar/dist/types/feature-flag";
4 |
5 | declare module "quasar/dist/types/feature-flag" {
6 | interface QuasarFeatureFlags {
7 | pwa: true;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/store/store-flag.d.ts:
--------------------------------------------------------------------------------
1 | // THIS FEATURE-FLAG FILE IS AUTOGENERATED,
2 | // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
3 | import 'quasar/dist/types/feature-flag'
4 |
5 | declare module 'quasar/dist/types/feature-flag' {
6 | interface QuasarFeatureFlags {
7 | store: true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/css/app.sass:
--------------------------------------------------------------------------------
1 | // app global css in Sass form
2 | .small-screen-only
3 | @media (max-width: $breakpoint-xs-max)
4 | display: block
5 | @media (min-width: $breakpoint-sm-min)
6 | display: none
7 | .large-screen-only
8 | @media (max-width: $breakpoint-xs-max)
9 | display: none
10 | @media (min-width: $breakpoint-sm-min)
11 | display: block
--------------------------------------------------------------------------------
/src/boot/components.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import Generate from '../components/Generate.vue'
4 | import Publish from '../components/Publish.vue'
5 | import Reply from '../components/Reply.vue'
6 | import Post from '../components/Post.vue'
7 |
8 | Vue.component('Generate', Generate)
9 | Vue.component('Publish', Publish)
10 | Vue.component('Reply', Reply)
11 | Vue.component('Post', Post)
12 |
--------------------------------------------------------------------------------
/src/store/state.js:
--------------------------------------------------------------------------------
1 | import {LocalStorage} from 'quasar'
2 |
3 | export default function () {
4 | return {
5 | myProfile: LocalStorage.getItem('myProfile'),
6 | theirProfile: LocalStorage.getItem('theirProfile') || {},
7 |
8 | kind0: {}, // temporary, will be merged with theirProfile or erased at the end
9 | kind1: LocalStorage.getItem('kind1') || [],
10 |
11 | chatUpdated: 1 // hack
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/mixin.js:
--------------------------------------------------------------------------------
1 | import {date} from 'quasar'
2 |
3 | import {emojis1, emojis2} from './emojis'
4 |
5 | export default {
6 | data() {
7 | return {
8 | emojis1,
9 | emojis2
10 | }
11 | },
12 | filters: {
13 | niceDate(value) {
14 | return date.formatDate(value, 'YYYY MMM D h:mm A')
15 | }
16 | },
17 | methods: {
18 | toProfile(pubkey) {
19 | this.$router.push('/user/' + pubkey)
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/pages/PageHome.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
25 |
--------------------------------------------------------------------------------
/src/pages/Error404.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
404
7 |
8 |
Oops. Nothing here...
9 |
10 |
19 |
20 |
21 |
22 |
23 |
28 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "src/*": [
6 | "src/*"
7 | ],
8 | "app/*": [
9 | "*"
10 | ],
11 | "components/*": [
12 | "src/components/*"
13 | ],
14 | "layouts/*": [
15 | "src/layouts/*"
16 | ],
17 | "pages/*": [
18 | "src/pages/*"
19 | ],
20 | "assets/*": [
21 | "src/assets/*"
22 | ],
23 | "boot/*": [
24 | "src/boot/*"
25 | ],
26 | "vue$": [
27 | "node_modules/vue/dist/vue.esm.js"
28 | ]
29 | }
30 | },
31 | "exclude": [
32 | "dist",
33 | ".quasar",
34 | "node_modules"
35 | ]
36 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .thumbs.db
3 | node_modules
4 |
5 | # Quasar core related directories
6 | .quasar
7 | /dist
8 |
9 | # Cordova related directories and files
10 | /src-cordova/node_modules
11 | /src-cordova/platforms
12 | /src-cordova/plugins
13 | /src-cordova/www
14 |
15 | # Capacitor related directories and files
16 | /src-capacitor/www
17 | /src-capacitor/node_modules
18 |
19 | # BEX related directories and files
20 | /src-bex/www
21 | /src-bex/js/core
22 |
23 | # Log files
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # Editor directories and files
29 | .idea
30 | *.suo
31 | *.ntvs*
32 | *.njsproj
33 | *.sln
34 |
35 | # Local Netlify folder
36 | .netlify
37 |
38 | yarn.lock
39 | package-lock.json
40 |
--------------------------------------------------------------------------------
/src/store/storage.js:
--------------------------------------------------------------------------------
1 | import {LocalStorage} from 'quasar'
2 |
3 | export default function (store) {
4 | store.subscribe(({type, payload}, state) => {
5 | switch (type) {
6 | case 'setProfile':
7 | case 'relayPush':
8 | case 'relaySplice':
9 | console.log('storing', state.myProfile)
10 | LocalStorage.set('myProfile', state.myProfile)
11 | break
12 | case 'startFollowing':
13 | case 'stopFollowing':
14 | case 'addKind0':
15 | LocalStorage.set('theirProfile', state.theirProfile)
16 | break
17 | case 'addKind1':
18 | case 'replaceKind1':
19 | case 'deleteKind1':
20 | LocalStorage.set('kind1', state.kind1)
21 | break
22 | }
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/src/utils/emojis.js:
--------------------------------------------------------------------------------
1 | export const emojis1 = [
2 | {
3 | item: '😂'
4 | },
5 | {
6 | item: '😃'
7 | },
8 | {
9 | item: '😍'
10 | },
11 | {
12 | item: '😘'
13 | },
14 | {
15 | item: '😭'
16 | },
17 | {
18 | item: '🤣'
19 | },
20 | {
21 | item: '🧐'
22 | },
23 | {
24 | item: '👊'
25 | },
26 | {
27 | item: '🤘'
28 | }
29 | ]
30 |
31 | export const emojis2 = [
32 | {
33 | item: '👌'
34 | },
35 | {
36 | item: '🙌'
37 | },
38 | {
39 | item: '🤦'
40 | },
41 | {
42 | item: '🚀'
43 | },
44 | {
45 | item: '🔥'
46 | },
47 | {
48 | item: '💯'
49 | },
50 | {
51 | item: '⚡'
52 | },
53 | {
54 | item: '🏴'
55 | },
56 | {
57 | item: '🌑'
58 | }
59 | ]
60 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | import state from './state'
5 | import * as getters from './getters'
6 | import * as mutations from './mutations'
7 | import * as actions from './actions'
8 | import storagePlugin from './storage'
9 |
10 | Vue.use(Vuex)
11 |
12 | /*
13 | * If not building with SSR mode, you can
14 | * directly export the Store instantiation;
15 | *
16 | * The function below can be async too; either use
17 | * async/await or return a Promise which resolves
18 | * with the Store instance.
19 | */
20 |
21 | export default function (/* { ssrContext } */) {
22 | const Store = new Vuex.Store({
23 | state,
24 | getters,
25 | mutations,
26 | actions,
27 | plugins: [storagePlugin]
28 | })
29 |
30 | return Store
31 | }
32 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | import identicon from 'identicon'
2 |
3 | export function disabled(state) {
4 | return !state.myProfile
5 | }
6 |
7 | export function handle(state, pubkey) {
8 | return pubkey => {
9 | let profile = state.theirProfile[pubkey]
10 | if (profile && profile.name) return profile.name
11 |
12 | let kind0 = state.kind0[pubkey]
13 | if (kind0 && kind0.name) return profile.name
14 |
15 | return pubkey.slice(0, 20) + '...'
16 | }
17 | }
18 |
19 | export function avatar(state) {
20 | return pubkey => {
21 | let profile = state.theirProfile[pubkey]
22 | if (profile && profile.picture) return profile.picture
23 |
24 | let kind0 = state.kind0[pubkey]
25 | if (kind0 && kind0.picture) return profile.picture
26 | return identicon.generateSync({id: pubkey, size: 40})
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/css/quasar.variables.scss:
--------------------------------------------------------------------------------
1 | // Quasar SCSS (& Sass) Variables
2 | // --------------------------------------------------
3 | // To customize the look and feel of this app, you can override
4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
5 |
6 | // Check documentation for full list of Quasar variables
7 |
8 | // Your own variables (that are declared here) and Quasar's own
9 | // ones will be available out of the box in your .vue/.scss/.sass files
10 |
11 | // It's highly recommended to change the default colors
12 | // to match your app's branding.
13 | // Tip: Use the "Theme Builder" on Quasar's documentation website.
14 |
15 | $primary: #1976d2;
16 | $secondary: #26a69a;
17 | $accent: #ccffff;
18 |
19 | $dark: #1d1d1d;
20 |
21 | $positive: #21ba45;
22 | $negative: #c10015;
23 | $info: #31ccec;
24 | $warning: #f2c037;
25 |
--------------------------------------------------------------------------------
/src/css/quasar.variables.sass:
--------------------------------------------------------------------------------
1 | // Quasar Sass (& SCSS) Variables
2 | // --------------------------------------------------
3 | // To customize the look and feel of this app, you can override
4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
5 |
6 | // Check documentation for full list of Quasar variables
7 |
8 | // Your own variables (that are declared here) and Quasar's own
9 | // ones will be available out of the box in your .vue/.scss/.sass files
10 |
11 | // It's highly recommended to change the default colors
12 | // to match your app's branding.
13 | // Tip: Use the "Theme Builder" on Quasar's documentation website.
14 |
15 | $primary : #26A69A
16 | $secondary : #00d6c2
17 | $accent : #ccffff
18 |
19 | $dark : #1d2d2d
20 |
21 | $positive : #21BA45
22 | $negative : #f1948a
23 | $info : #31CCEC
24 | $warning : #F2C037
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 |
4 | import routes from './routes'
5 |
6 | Vue.use(VueRouter)
7 |
8 | /*
9 | * If not building with SSR mode, you can
10 | * directly export the Router instantiation;
11 | *
12 | * The function below can be async too; either use
13 | * async/await or return a Promise which resolves
14 | * with the Router instance.
15 | */
16 |
17 | export default function ({store, ssrContext}) {
18 | const Router = new VueRouter({
19 | scrollBehavior: () => ({x: 0, y: 0}),
20 | routes,
21 |
22 | // Leave these as they are and change in quasar.conf.js instead!
23 | // quasar.conf.js -> build -> vueRouterMode
24 | // quasar.conf.js -> build -> publicPath
25 | mode: process.env.VUE_ROUTER_MODE,
26 | base: process.env.VUE_ROUTER_BASE
27 | })
28 |
29 | Router.beforeEach((to, from, next) => {
30 | if (to.name !== 'help' && store.getters.disabled) next({name: 'help'})
31 | else next()
32 | })
33 |
34 | return Router
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Ben Arc
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 |
--------------------------------------------------------------------------------
/src/router/routes.js:
--------------------------------------------------------------------------------
1 | const routes = [
2 | {
3 | path: '/',
4 | component: () => import('layouts/MainLayout.vue'),
5 | children: [
6 | {path: '', component: () => import('pages/PageHome.vue'), name: 'home'},
7 | {
8 | path: '/messages',
9 | component: () => import('pages/PageMessages.vue'),
10 | name: 'messages'
11 | },
12 | {path: '/chat/:pubkey', component: () => import('pages/PageChat.vue')},
13 | {
14 | path: '/user/:pubkey',
15 | component: () => import('pages/PageProfile.vue')
16 | },
17 | {
18 | path: '/notifications',
19 | component: () => import('pages/PageNotifications.vue')
20 | },
21 | {
22 | path: '/settings',
23 | component: () => import('pages/PageSettings.vue'),
24 | name: 'settings'
25 | },
26 | {
27 | path: '/help',
28 | component: () => import('pages/PageHelp.vue'),
29 | name: 'help'
30 | }
31 | ]
32 | },
33 |
34 | // Always leave this as last one,
35 | // but you can also remove it
36 | {
37 | path: '*',
38 | component: () => import('pages/Error404.vue')
39 | }
40 | ]
41 |
42 | export default routes
43 |
--------------------------------------------------------------------------------
/src/utils/nip04.js:
--------------------------------------------------------------------------------
1 | import crypto from 'crypto'
2 | import * as secp from 'noble-secp256k1'
3 |
4 | export function encrypt(privkey, pubkey, text) {
5 | const key = secp.getSharedSecret(privkey, '02' + pubkey)
6 | const normalizedKey = getOnlyXFromFullSharedSecret(key)
7 |
8 | let iv = crypto.randomFillSync(new Uint8Array(16))
9 | var cipher = crypto.createCipheriv(
10 | 'aes-256-cbc',
11 | Buffer.from(normalizedKey, 'hex'),
12 | iv
13 | )
14 | let encryptedMessage = cipher.update(text, 'utf8', 'base64')
15 | encryptedMessage += cipher.final('base64')
16 |
17 | return [encryptedMessage, Buffer.from(iv.buffer).toString('base64')]
18 | }
19 |
20 | export function decrypt(privkey, pubkey, ciphertext, iv) {
21 | const key = secp.getSharedSecret(privkey, '02' + pubkey)
22 | const normalizedKey = getOnlyXFromFullSharedSecret(key)
23 |
24 | var decipher = crypto.createDecipheriv(
25 | 'aes-256-cbc',
26 | Buffer.from(normalizedKey, 'hex'),
27 | Buffer.from(iv, 'base64')
28 | )
29 | let decryptedMessage = decipher.update(ciphertext, 'base64')
30 | decryptedMessage += decipher.final('utf8')
31 |
32 | return decryptedMessage
33 | }
34 |
35 | function getOnlyXFromFullSharedSecret(fullSharedSecretCoordinates) {
36 | return fullSharedSecretCoordinates.substr(2, 64)
37 | }
38 |
--------------------------------------------------------------------------------
/src/index.template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= productName %>
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
21 |
27 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src-pwa/register-service-worker.js:
--------------------------------------------------------------------------------
1 | import { register } from 'register-service-worker'
2 |
3 | // The ready(), registered(), cached(), updatefound() and updated()
4 | // events passes a ServiceWorkerRegistration instance in their arguments.
5 | // ServiceWorkerRegistration: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
6 |
7 | register(process.env.SERVICE_WORKER_FILE, {
8 | // The registrationOptions object will be passed as the second argument
9 | // to ServiceWorkerContainer.register()
10 | // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#Parameter
11 |
12 | // registrationOptions: { scope: './' },
13 |
14 | ready (/* registration */) {
15 | // console.log('Service worker is active.')
16 | },
17 |
18 | registered (/* registration */) {
19 | // console.log('Service worker has been registered.')
20 | },
21 |
22 | cached (/* registration */) {
23 | // console.log('Content has been cached for offline use.')
24 | },
25 |
26 | updatefound (/* registration */) {
27 | // console.log('New content is downloading.')
28 | },
29 |
30 | updated (/* registration */) {
31 | // console.log('New content is available; please refresh.')
32 | },
33 |
34 | offline () {
35 | // console.log('No internet connection found. App is running in offline mode.')
36 | },
37 |
38 | error (/* err */) {
39 | // console.error('Error during service worker registration:', err)
40 | }
41 | })
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NOSTR-twitter
2 | ## Currently very buggy not even MVP, but we'll get there!
3 |
4 | 280 character limited social network using the decentralised, censorship-resistant nostr protocol.
5 | Checkout an example running nostr.com
6 |
7 |
8 |
9 | | Easily make/restore accounts! | Send encrypted private messages! |
10 | | ------------- | ------------- |
11 | | | |
12 |
13 | ## Tutorials
14 | https://www.youtube.com/watch?v=BpvjL6pAw7o
15 | https://www.youtube.com/watch?v=G6xFOBWI7S8
16 |
17 | ## NOSTR (Notes and Other Stuff Relays) protocol
18 | https://github.com/fiatjaf/nostr
19 |
20 | ## Installing Nostr-twitter
21 |
22 | ### Install Quasar
23 | ```bash
24 | npm install -g @quasar/cli
25 | ```
26 |
27 | ### Install the dependencies
28 | ```bash
29 | npm install
30 | ```
31 |
32 | ### Start the app in development mode (hot-code reloading, error reporting, etc.)
33 | ```bash
34 | quasar dev
35 | ```
36 |
37 | ### Start the app in development mode as PWA (hot-code reloading, error reporting, etc.)
38 | ```bash
39 | quasar dev -m pwa
40 | ```
41 |
42 | ### Build the app for production
43 | ```bash
44 | quasar build
45 | ```
46 |
47 | ### Build the app for production as PWA
48 | ```bash
49 | quasar build pwa
50 | ```
51 |
52 | ### Customize the configuration
53 | See [Configuring quasar.conf.js](https://quasar.dev/quasar-cli/quasar-conf-js).
54 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | export function setProfile(state, profile) {
2 | state.myProfile = profile
3 | }
4 |
5 | export function relayPush(state, url) {
6 | state.myProfile.relays.push(url)
7 | }
8 |
9 | export function relaySplice(state, url) {
10 | let index = state.myProfile.relays.indexOf(url)
11 | if (index === -1) return
12 | state.myProfile.relays.splice(index, 1)
13 | }
14 |
15 | export function startFollowing(state, key) {
16 | // use metadata from kind0 or leave everything blank
17 | state.theirProfile = {
18 | [key]: state.kind0[key] || {name: null, about: null, picture: null},
19 | ...state.theirProfile
20 | }
21 | }
22 |
23 | export function stopFollowing(state, key) {
24 | delete state.theirProfile[key]
25 | }
26 |
27 | export function addKind1(state, event) {
28 | state.kind1.unshift(event)
29 | }
30 |
31 | export function replaceKind1(state, {index, event}) {
32 | state.kind1 = [
33 | ...state.kind1.slice(0, index),
34 | event,
35 | ...state.kind1.slice(index + 1)
36 | ]
37 | }
38 | export function deleteKind1(state, id) {
39 | console.log(state.kind1)
40 | console.log(id)
41 | let index = state.kind1.findIndex(event => event.id === id)
42 | console.log(index)
43 | if (index !== -1) state.kind1.splice(index, 1)
44 | }
45 |
46 | export function addKind0(state, event) {
47 | // increment theirProfile with this or store it temporarily
48 | try {
49 | let {name, about, picture} = JSON.parse(event.content)
50 |
51 | if (event.pubkey in state.theirProfile) {
52 | state.theirProfile[event.pubkey] = {name, about, picture}
53 | return
54 | }
55 | state.kind0[event.pubkey] = {name, about, picture}
56 | } catch (err) {
57 | return
58 | }
59 | }
60 |
61 | export function chatUpdated(state) {
62 | state.chatUpdated++
63 | }
64 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nostr",
3 | "version": "0.0.1",
4 | "description": "280 character limited social network using the nostr protocol",
5 | "productName": "Nostr",
6 | "author": "benarc ",
7 | "private": true,
8 | "scripts": {
9 | "dev": "quasar dev",
10 | "pwa": "quasar dev -m pwa",
11 | "build": "quasar build",
12 | "prettier": "prettier --write src/",
13 | "eslint": "eslint --fix src/",
14 | "styling": "prettier --check src/",
15 | "linting": "eslint src/"
16 | },
17 | "pre-commit": [
18 | "styling",
19 | "linting"
20 | ],
21 | "dependencies": {
22 | "@quasar/cli": "^1.1.3",
23 | "@quasar/extras": "^1.9.13",
24 | "axios": "^0.18.1",
25 | "bip32": "^2.0.6",
26 | "bip39": "^3.0.3",
27 | "bitcoinjs-lib": "^5.2.0",
28 | "bs58": "^4.0.1",
29 | "core-js": "^3.8.2",
30 | "identicon": "^3.0.1",
31 | "md-gum-polyfill": "^1.0.0",
32 | "noble-secp256k1": "^1.1.1",
33 | "nostr-tools": "^0.4.2",
34 | "quasar": "^1.15.0",
35 | "wif": "^2.0.6"
36 | },
37 | "devDependencies": {
38 | "@quasar/app": "^2.1.14",
39 | "babel-eslint": "^10.1.0",
40 | "eslint": "^7.18.0",
41 | "eslint-plugin-babel": "^5.3.1",
42 | "eslint-plugin-vue": "^7.5.0",
43 | "pre-commit": "^1.2.2",
44 | "prettier": "^2.2.1",
45 | "workbox-webpack-plugin": "^5.1.4"
46 | },
47 | "browserslist": [
48 | "ie >= 11",
49 | "last 10 Chrome versions",
50 | "last 10 Firefox versions",
51 | "last 4 Edge versions",
52 | "last 7 Safari versions",
53 | "last 8 Android versions",
54 | "last 8 ChromeAndroid versions",
55 | "last 8 FirefoxAndroid versions",
56 | "last 10 iOS versions",
57 | "last 5 Opera versions"
58 | ],
59 | "engines": {
60 | "node": ">= 10.18.1",
61 | "npm": ">= 6.13.4",
62 | "yarn": ">= 1.21.1"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/pages/PageMessages.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Encrypted Messages
5 |
6 |
14 |
15 |
16 |
17 | Currently you can only private message a key you are following. All
18 | private messages are end-to-end encrypted.
19 |
20 |
21 |
22 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {{
37 | $store.getters.handle(followedKey)
38 | }}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
52 |
53 |
54 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
84 |
--------------------------------------------------------------------------------
/public/icons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Post.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {{ $store.getters.handle(post.pubkey) }}
21 | {{
22 | (post.created_at * 1000) | niceDate
23 | }}
25 | {{ post.content }}
26 |
27 |
28 |
29 |
40 |
41 |
42 |
53 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
92 |
--------------------------------------------------------------------------------
/src/components/Publish.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
32 |
33 | {{ emoji.item }}
43 |
44 | {{ emoji.item }}
54 |
55 |
56 |
66 |
67 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
101 |
--------------------------------------------------------------------------------
/src/pages/PageHelp.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Help
5 |
6 |
7 |
15 |
16 |
17 | What is Nostr (Notes and other stuff relays)?
18 |
19 | Nostr is a decentralised collection of relays passing data between
20 | clients. Anyone can run a client or relay. This particular 240char limited
21 | client is just one way to send data through Nostr.
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 | Nostr uses public key cryptography. Posts are signed with your private key
34 | and people can follow your posts using your public key. Direct messages in
35 | this client are encrypted before being sent through nostr network.
36 |
37 |
38 | Learn more about the Nostr protocol
44 |
45 |
46 | https://github.com/arcbtc/nostr
52 |
53 |
54 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | Warning!
74 |
75 |
76 |
77 |
78 | This is buggy and experimental software running for testing purposes
79 | ONLY, any data you put on here will be lost!
80 |
81 |
82 |
83 |
84 |
85 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
117 |
--------------------------------------------------------------------------------
/src/pages/PageProfile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 | Profile
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
27 | {{ $route.params.pubkey }}
28 |
29 |
30 |
31 |
32 |
33 |
44 |
55 |
56 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
142 |
--------------------------------------------------------------------------------
/src/components/Reply.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{ $store.getters.handle(post.pubkey) }}
16 | {{ post.created_at | niceDate }}
17 |
18 | {{ post.content }}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Coming soon
30 |
38 |
39 |
40 |
41 |
51 |
52 | {{ emoji.item }}
62 |
63 | {{ emoji.item }}
73 |
74 |
75 |
85 |
86 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
128 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 |
4 | "parser": "vue-eslint-parser",
5 | "parserOptions": {
6 | "parser": "babel-eslint",
7 | "ecmaVersion": 9,
8 | "sourceType": "module",
9 | "allowImportExportEverywhere": false
10 | },
11 |
12 | "env": {
13 | "es6": true,
14 | "node": true
15 | },
16 |
17 | "plugins": [
18 | "babel"
19 | ],
20 |
21 | "extends": [
22 | "plugin:vue/recommended"
23 | ],
24 |
25 | "globals": {
26 | "document": false,
27 | "navigator": false,
28 | "window": false,
29 | "location": false,
30 | "URL": false,
31 | "URLSearchParams": false,
32 | "fetch": false,
33 | "EventSource": false,
34 | "localStorage": false,
35 | "sessionStorage": false
36 | },
37 |
38 | "rules": {
39 | "strict": 0,
40 |
41 | "vue/require-prop-types": 0,
42 | "vue/multiline-html-element-content-newline": 0,
43 | "vue/singleline-html-element-content-newline": 0,
44 | "vue/max-attributes-per-line": 0,
45 | "vue/html-self-closing": 0,
46 | "vue/no-use-v-if-with-v-for": 0,
47 | "vue/html-indent": 0,
48 | "vue/no-deprecated-filter": 0,
49 | "vue/html-closing-bracket-newline": 0,
50 |
51 | "accessor-pairs": 2,
52 | "arrow-spacing": [2, { "before": true, "after": true }],
53 | "block-spacing": [2, "always"],
54 | "brace-style": [2, "1tbs", { "allowSingleLine": true }],
55 | "comma-dangle": 0,
56 | "comma-spacing": [2, { "before": false, "after": true }],
57 | "comma-style": [2, "last"],
58 | "constructor-super": 2,
59 | "curly": [0, "multi-line"],
60 | "dot-location": [2, "property"],
61 | "eol-last": 2,
62 | "eqeqeq": [2, "allow-null"],
63 | "generator-star-spacing": [2, { "before": true, "after": true }],
64 | "handle-callback-err": [2, "^(err|error)$" ],
65 | "indent": 0,
66 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }],
67 | "keyword-spacing": [2, { "before": true, "after": true }],
68 | "new-cap": 0,
69 | "new-parens": 0,
70 | "no-array-constructor": 2,
71 | "no-caller": 2,
72 | "no-class-assign": 2,
73 | "no-cond-assign": 2,
74 | "no-const-assign": 2,
75 | "no-control-regex": 0,
76 | "no-debugger": 0,
77 | "no-delete-var": 2,
78 | "no-dupe-args": 2,
79 | "no-dupe-class-members": 2,
80 | "no-dupe-keys": 2,
81 | "no-duplicate-case": 2,
82 | "no-empty-character-class": 2,
83 | "no-empty-pattern": 2,
84 | "no-eval": 0,
85 | "no-ex-assign": 2,
86 | "no-extend-native": 2,
87 | "no-extra-bind": 2,
88 | "no-extra-boolean-cast": 2,
89 | "no-extra-parens": [2, "functions"],
90 | "no-fallthrough": 2,
91 | "no-floating-decimal": 2,
92 | "no-func-assign": 2,
93 | "no-implied-eval": 2,
94 | "no-inner-declarations": [0, "functions"],
95 | "no-invalid-regexp": 2,
96 | "no-irregular-whitespace": 2,
97 | "no-iterator": 2,
98 | "no-label-var": 2,
99 | "no-labels": [2, { "allowLoop": false, "allowSwitch": false }],
100 | "no-lone-blocks": 2,
101 | "no-mixed-spaces-and-tabs": 2,
102 | "no-multi-spaces": 2,
103 | "no-multi-str": 2,
104 | "no-multiple-empty-lines": [2, { "max": 2 }],
105 | "no-native-reassign": 2,
106 | "no-negated-in-lhs": 2,
107 | "no-new": 0,
108 | "no-new-func": 2,
109 | "no-new-object": 2,
110 | "no-new-require": 2,
111 | "no-new-symbol": 2,
112 | "no-new-wrappers": 2,
113 | "no-obj-calls": 2,
114 | "no-octal": 2,
115 | "no-octal-escape": 2,
116 | "no-path-concat": 0,
117 | "no-proto": 2,
118 | "no-redeclare": 2,
119 | "no-regex-spaces": 2,
120 | "no-return-assign": 0,
121 | "no-self-assign": 2,
122 | "no-self-compare": 2,
123 | "no-sequences": 2,
124 | "no-shadow-restricted-names": 2,
125 | "no-spaced-func": 2,
126 | "no-sparse-arrays": 2,
127 | "no-this-before-super": 2,
128 | "no-throw-literal": 2,
129 | "no-trailing-spaces": 2,
130 | "no-undef": 2,
131 | "no-undef-init": 2,
132 | "no-unexpected-multiline": 2,
133 | "no-unneeded-ternary": [2, { "defaultAssignment": false }],
134 | "no-unreachable": 2,
135 | "no-unused-vars": [2, { "vars": "local", "args": "none", "varsIgnorePattern": "^_"}],
136 | "no-useless-call": 2,
137 | "no-useless-constructor": 2,
138 | "no-with": 2,
139 | "one-var": [0, { "initialized": "never" }],
140 | "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }],
141 | "padded-blocks": [2, "never"],
142 | "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
143 | "semi": [2, "never"],
144 | "semi-spacing": [2, { "before": false, "after": true }],
145 | "space-before-blocks": [2, "always"],
146 | "space-before-function-paren": 0,
147 | "space-in-parens": [2, "never"],
148 | "space-infix-ops": 2,
149 | "space-unary-ops": [2, { "words": true, "nonwords": false }],
150 | "spaced-comment": 0,
151 | "template-curly-spacing": [2, "never"],
152 | "use-isnan": 2,
153 | "valid-typeof": 2,
154 | "wrap-iife": [2, "any"],
155 | "yield-star-spacing": [2, "both"],
156 | "yoda": [0]
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/quasar.conf.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only
3 | * the ES6 features that are supported by your Node version. https://node.green/
4 | */
5 |
6 | // Configuration for your app
7 | // https://quasar.dev/quasar-cli/quasar-conf-js
8 |
9 | module.exports = function (/* ctx */) {
10 | return {
11 | // https://quasar.dev/quasar-cli/supporting-ts
12 | supportTS: false,
13 |
14 | // https://quasar.dev/quasar-cli/prefetch-feature
15 | // preFetch: true,
16 |
17 | // app boot file (/src/boot)
18 | // --> boot files are part of "main.js"
19 | // https://quasar.dev/quasar-cli/boot-files
20 | boot: ['components'],
21 |
22 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css
23 | css: ['app.scss'],
24 |
25 | // https://github.com/quasarframework/quasar/tree/dev/extras
26 | extras: [
27 | // 'ionicons-v4',
28 | // 'mdi-v5',
29 | // 'fontawesome-v5',
30 | // 'eva-icons',
31 | // 'themify',
32 | // 'line-awesome',
33 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
34 |
35 | 'roboto-font', // optional, you are not bound to it
36 | 'material-icons' // optional, you are not bound to it
37 | ],
38 |
39 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
40 | build: {
41 | vueRouterMode: 'hash', // available values: 'hash', 'history'
42 |
43 | // transpile: false,
44 |
45 | // Add dependencies for transpiling with Babel (Array of string/regex)
46 | // (from node_modules, which are by default not transpiled).
47 | // Applies only if "transpile" is set to true.
48 | // transpileDependencies: [],
49 |
50 | // rtl: false, // https://quasar.dev/options/rtl-support
51 | // preloadChunks: true,
52 | // showProgress: false,
53 | // gzip: true,
54 | // analyze: true,
55 |
56 | // Options below are automatically set depending on the env, set them if you want to override
57 | // extractCSS: false,
58 |
59 | // https://quasar.dev/quasar-cli/handling-webpack
60 | extendWebpack(cfg) {}
61 | },
62 |
63 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
64 | devServer: {
65 | https: false,
66 | port: 8080,
67 | open: true // opens browser window automatically
68 | },
69 |
70 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
71 | framework: {
72 | iconSet: 'material-icons', // Quasar icon set
73 | lang: 'en-us', // Quasar language pack
74 |
75 | // Possible values for "importStrategy":
76 | // * 'auto' - (DEFAULT) Auto-import needed Quasar components & directives
77 | // * 'all' - Manually specify what to import
78 | importStrategy: 'auto',
79 |
80 | // For special cases outside of where "auto" importStrategy can have an impact
81 | // (like functional components as one of the examples),
82 | // you can manually specify Quasar components/directives to be available everywhere:
83 | //
84 | // components: [],
85 | // directives: [],
86 |
87 | // Quasar plugins
88 | plugins: ['LocalStorage', 'Notify'],
89 | config: {
90 | loading: {
91 | /* look at QUASARCONFOPTIONS from the API card (bottom of page) */
92 | }
93 | }
94 | },
95 |
96 | // animations: 'all', // --- includes all animations
97 | // https://quasar.dev/options/animations
98 | animations: [],
99 |
100 | // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
101 | ssr: {
102 | pwa: false
103 | },
104 |
105 | // https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
106 | pwa: {
107 | workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
108 | workboxOptions: {}, // only for GenerateSW
109 | manifest: {
110 | name: `Nostr - Decentralised Twitter`,
111 | short_name: `Nostr`,
112 | description: `280 character limited social network using the nostr protocol`,
113 | display: 'standalone',
114 | orientation: 'portrait',
115 | background_color: '#ffffff',
116 | theme_color: '#26A69A',
117 | icons: [
118 | {
119 | src: 'icons/icon-128x128.png',
120 | sizes: '128x128',
121 | type: 'image/png'
122 | },
123 | {
124 | src: 'icons/icon-192x192.png',
125 | sizes: '192x192',
126 | type: 'image/png'
127 | },
128 | {
129 | src: 'icons/icon-256x256.png',
130 | sizes: '256x256',
131 | type: 'image/png'
132 | },
133 | {
134 | src: 'icons/icon-384x384.png',
135 | sizes: '384x384',
136 | type: 'image/png'
137 | },
138 | {
139 | src: 'icons/icon-512x512.png',
140 | sizes: '512x512',
141 | type: 'image/png'
142 | }
143 | ]
144 | }
145 | },
146 |
147 | // Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
148 | cordova: {
149 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
150 | },
151 |
152 | // Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
153 | capacitor: {
154 | hideSplashscreen: true
155 | },
156 |
157 | // Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
158 | electron: {
159 | bundler: 'packager', // 'packager' or 'builder'
160 |
161 | packager: {
162 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
163 | // OS X / Mac App Store
164 | // appBundleId: '',
165 | // appCategoryType: '',
166 | // osxSign: '',
167 | // protocol: 'myapp://path',
168 | // Windows only
169 | // win32metadata: { ... }
170 | },
171 |
172 | builder: {
173 | // https://www.electron.build/configuration/configuration
174 |
175 | appId: 'nostr'
176 | },
177 |
178 | // More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
179 | nodeIntegration: true,
180 |
181 | extendWebpack(/* cfg */) {
182 | // do something with Electron main process Webpack cfg
183 | // chainWebpack also available besides this extendWebpack
184 | }
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/components/Generate.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 | Nostr.org uses a word list of 12 words is used to create your keys, to
11 | restore either enter a word list or a Nostr private key.
12 |
20 |
21 |
27 |
33 |
34 |
40 |
41 |
42 |
43 | In this client you can restore from a word list but for other clients
44 | you will need to use your keys as well.
45 | Your private key is used to sign/publish posts.
46 |
47 |
52 |
53 |
58 |
59 |
60 |
65 |
66 |
67 |
68 | Your public key allows other people to read your posts, follow you, and
69 | send you private messages.
70 |
71 |
72 |
73 |
78 |
79 |
80 |
81 |
82 |
83 |
90 |
91 |
92 |
93 |
94 | To publish your posts this client needs to sign messages with your
95 | private key. Choose how this client will access your private key.
96 |
97 |
98 |
104 |
111 |
118 |
119 |
120 |
121 |
122 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
223 |
--------------------------------------------------------------------------------
/src/pages/PageChat.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Chat
5 |
6 |
14 |
15 |
16 |
17 |
18 |
22 |
27 |
28 | clickMessageAction(ev, message.id, message.text)"
49 | >
50 |
51 |
52 |
53 |
54 |
55 |
56 |
61 |
62 |
63 |
69 |
70 |
71 |
80 |
81 | {{ emoji.item }}
91 |
92 | {{ emoji.item }}
102 |
103 |
104 |
114 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
234 |
235 |
249 |
--------------------------------------------------------------------------------
/src/pages/PageSettings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Settings
5 |
6 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
28 |
29 |
30 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | If your desired handle is available our relay will use open-timestamps
48 | to secure it to your public key, and share the proof with other
49 | relays.
50 |
51 |
62 |
63 |
64 |
65 |
66 |
74 |
82 |
90 |
91 | Coming soon
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
106 |
107 |
108 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
128 |
129 |
130 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
150 |
157 |
158 |
159 |
160 |
161 |
162 | Your keys
163 |
164 | Make sure you back up your private key!
165 | *Posts are published using your private key. Others can see your
167 | posts/follow you using your public key.
169 |
170 |
171 |
172 |
173 | Private key:
174 |
179 |
180 |
185 |
186 |
187 |
192 |
193 |
194 |
195 | Public key:
196 |
197 |
198 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 | Are you sure?
217 | Deleting storage will remove all traces of this account!
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
303 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | import {getEventHash} from 'nostr-tools'
2 | import {LocalStorage, Notify} from 'quasar'
3 | import 'md-gum-polyfill'
4 |
5 | import {pool} from '../global'
6 | import {encrypt, decrypt} from '../utils/nip04'
7 |
8 | export function launch(store) {
9 | pool.setPrivateKey(store.state.myProfile.privkey)
10 |
11 | store.state.myProfile.relays.forEach(relay => {
12 | pool.addRelay(relay)
13 | })
14 |
15 | pool.onNotice((notice, relay) => {
16 | Notify.create({
17 | message: `Relay ${relay.url} says: ${notice}`,
18 | color: 'pink'
19 | })
20 | })
21 |
22 | store.dispatch('restartHomeFeed')
23 | }
24 |
25 | var homeSubscription = pool
26 |
27 | export function restartHomeFeed(store) {
28 | homeSubscription = homeSubscription.sub({
29 | filter: [
30 | {
31 | authors: Object.keys(store.state.theirProfile).length
32 | ? Object.keys(store.state.theirProfile)
33 | : null
34 | },
35 | {
36 | author: store.state.myProfile.pubkey
37 | },
38 | {
39 | '#p': store.state.myProfile.pubkey
40 | }
41 | ],
42 | cb: (event, relay) => {
43 | switch (event.kind) {
44 | case 0:
45 | store.commit('addKind0', event)
46 | break
47 |
48 | case 1:
49 | for (let i = 0; i < store.state.kind1.length; i++) {
50 | if (
51 | (store.state.kind1[i].loading === true ||
52 | store.state.kind1[i].retry === true) &&
53 | store.state.kind1[i].id === event.id
54 | ) {
55 | event.retry = false
56 | event.loading = false
57 | store.commit('replaceKind1', {index: i, event})
58 | return
59 | } else if (store.state.kind1[i].id === event.id) {
60 | return
61 | }
62 | }
63 |
64 | store.commit('addKind1', event)
65 | break
66 |
67 | case 4:
68 | // a direct encrypted message
69 | if (
70 | event.tags.find(
71 | tag => tag[0] === 'p' && tag[1] === store.state.myProfile.pubkey
72 | )
73 | ) {
74 | // it is addressed to us
75 | let lsKey = `messages.${event.pubkey}`
76 | var messages = LocalStorage.getItem(lsKey) || []
77 |
78 | if (messages.find(({id}) => id === event.id)) {
79 | // we already have this one, discard
80 | return
81 | }
82 |
83 | // decrypt it
84 | let [ciphertext, iv] = event.content.split('?iv=')
85 | let text = decrypt(
86 | store.state.myProfile.privkey,
87 | event.pubkey,
88 | ciphertext,
89 | iv
90 | )
91 |
92 | // store it locally push
93 | messages.push({
94 | text,
95 | from: event.pubkey,
96 | id: event.id,
97 | created_at: event.created_at,
98 | tags: event.tags,
99 | loading: false,
100 | retry: false
101 | })
102 |
103 | LocalStorage.set(lsKey, messages)
104 |
105 | // a hack to update the UI
106 | store.commit('chatUpdated')
107 | } else if (
108 | event.pubkey === store.state.myProfile.pubkey &&
109 | event.tags[0][1] in store.state.theirProfile
110 | ) {
111 | // it is coming from us
112 | let p = event.tags.find(tag => tag[0] === 'p')
113 | let lsKey = `messages.${p[1]}`
114 | var messagesS = LocalStorage.getItem(lsKey)
115 |
116 | if (messagesS.length > 0) {
117 | for (var i = 0; i < messagesS.length; i++) {
118 | if (
119 | messagesS[i].id === event.id &&
120 | messagesS[i].loading === true
121 | ) {
122 | messagesS[i].loading = false
123 | LocalStorage.set(lsKey, messagesS)
124 | return
125 | }
126 | }
127 |
128 | if (messagesS.find(({id}) => id === event.id)) {
129 | // we already have this one, discard
130 | return
131 | }
132 |
133 | // decrypt it
134 | let [ciphertext, iv] = event.content.split('?iv=')
135 | let text = decrypt(
136 | store.state.myProfile.privkey,
137 | p[1],
138 | ciphertext,
139 | iv
140 | )
141 | messagesS.push({
142 | text,
143 | from: event.pubkey,
144 | id: event.id,
145 | created_at: event.created_at,
146 | tags: event.tags,
147 | loading: false,
148 | retry: false
149 | })
150 | LocalStorage.set(lsKey, messagesS)
151 | }
152 | }
153 |
154 | break
155 | }
156 | }
157 | })
158 | }
159 |
160 | export function relayPush(store, url) {
161 | store.commit('relayPush', url)
162 | pool.addRelay(url, {
163 | read: true,
164 | write: true
165 | })
166 | }
167 |
168 | export async function relayRemove(store, url) {
169 | store.commit('relaySplice', url)
170 | pool.removeRelay(url)
171 | }
172 |
173 | export async function sendPost(store, {message, tags = [], kind = 1}) {
174 | if (message.length === 0) return
175 |
176 | let event = {
177 | pubkey: store.state.myProfile.pubkey,
178 | created_at: Math.floor(Date.now() / 1000),
179 | kind,
180 | tags,
181 | content: message
182 | }
183 |
184 | event.id = await getEventHash(event)
185 | pool.publish(event)
186 |
187 | store.commit('addKind1', {
188 | ...event,
189 | loading: true,
190 | retry: false
191 | })
192 | }
193 |
194 | export function postAgain(store, event) {
195 | for (let i = 0; i < store.state.kind1.length; i++) {
196 | if (store.state.kind1[i].id === event.id) {
197 | store.commit('replaceKind1', {
198 | index: i,
199 | event: {
200 | ...event,
201 | loading: true,
202 | retry: false
203 | }
204 | })
205 | }
206 | }
207 | pool.publish(event)
208 | }
209 |
210 | export async function saveMeta(store, {image, handle, about}) {
211 | store.commit('setProfile', {
212 | ...store.state.myProfile,
213 | picture: image,
214 | name: handle,
215 | about
216 | })
217 |
218 | var event = {
219 | pubkey: store.state.myProfile.pubkey,
220 | created_at: Math.floor(Date.now() / 1000),
221 | kind: 0,
222 | tags: [],
223 | content: JSON.stringify({
224 | name: store.state.myProfile.name,
225 | about: store.state.myProfile.about,
226 | picture: store.state.myProfile.picture
227 | })
228 | }
229 |
230 | event.id = await getEventHash(event)
231 | pool.publish(event)
232 | }
233 |
234 | export function deletePost(store, postId) {
235 | store.commit('deleteKind1', postId)
236 | }
237 |
238 | export function startFollowing(store, key) {
239 | if (key in store.state.theirProfile) {
240 | Notify.create({
241 | message: 'Already following',
242 | color: 'pink'
243 | })
244 | return
245 | }
246 |
247 | if (!key.match(/^[0-9a-fA-F]{64}$/)) {
248 | Notify.create({
249 | message:
250 | 'Invalid public key. Must be 32 bytes hex-encoded (64 characters).',
251 | color: 'pink'
252 | })
253 | return
254 | }
255 |
256 | LocalStorage.set(`messages.${key}`, [])
257 | store.commit('startFollowing', key)
258 | store.dispatch('restartHomeFeed')
259 | }
260 |
261 | export async function stopFollowing(store, key) {
262 | if (!(key in store.state.theirProfile)) {
263 | Notify.create({
264 | message: 'No such user',
265 | color: 'pink'
266 | })
267 | return
268 | }
269 |
270 | store.commit('stopFollowing', key)
271 | store.dispatch('restartHomeFeed')
272 | }
273 |
274 | export function finalGenerate(store, {keystoreoption, publickey, privatekey}) {
275 | var profile = {
276 | pubkey: publickey,
277 | privkey: privatekey,
278 | relays: [
279 | 'wss://nostr-relay.herokuapp.com/ws',
280 | 'wss://nostr-relay.bigsun.xyz/ws',
281 | 'wss://freedom-relay.herokuapp.com/ws'
282 | // 'wss://relay.nostr.org',
283 | // 'wss://nodestr-relay.dolu.dev/ws'
284 | ],
285 | avatar: null,
286 | handle: null,
287 | about: null
288 | }
289 |
290 | if (keystoreoption === 'external') {
291 | profile.privkey = null
292 | }
293 |
294 | store.commit('setProfile', profile)
295 | LocalStorage.set('theirProfile', {})
296 | LocalStorage.set('kind1', [])
297 |
298 | store.dispatch('launch')
299 | }
300 |
301 | export async function sendChatMessage(store, {pubkey, text}) {
302 | if (text.length === 0) return
303 |
304 | let [ciphertext, iv] = encrypt(store.state.myProfile.privkey, pubkey, text)
305 |
306 | // make event
307 | let event = {
308 | pubkey: store.state.myProfile.pubkey,
309 | created_at: Math.floor(Date.now() / 1000),
310 | kind: 4,
311 | tags: [['p', pubkey]],
312 | content: ciphertext + '?iv=' + iv
313 | }
314 |
315 | let lsKey = `messages.${pubkey}`
316 | var messages = LocalStorage.getItem(lsKey) || []
317 |
318 | if (messages.length > 0) {
319 | event.tags.push(['e', messages[messages.length - 1].id])
320 | }
321 | event.id = await getEventHash(event)
322 |
323 | let message = {
324 | text,
325 | from: store.state.myProfile.pubkey,
326 | id: event.id,
327 | created_at: event.created_at,
328 | tags: event.tags,
329 | loading: true,
330 | failed: false
331 | }
332 |
333 | messages.push(message)
334 | LocalStorage.set(lsKey, messages)
335 | pool.publish(event)
336 | }
337 |
338 | export function deleteChatMessage(store, {pubkey, id}) {
339 | let lsKey = `messages.${pubkey}`
340 | var messages = LocalStorage.getItem(lsKey) || []
341 |
342 | let index = messages.findIndex(message => message.id === id)
343 | if (index === -1) return
344 |
345 | messages.splice(index, 1)
346 | LocalStorage.set(lsKey, messages)
347 | }
348 |
--------------------------------------------------------------------------------
/src/layouts/MainLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 | Home
38 |
39 |
48 |
49 |
50 |
51 |
52 | Home
53 |
54 |
55 |
60 |
61 |
62 |
63 |
64 | Messages
65 |
66 |
67 |
76 |
77 |
78 |
79 |
80 | Messages
81 |
82 |
83 |
88 |
89 |
90 |
91 |
92 | Settings
93 |
94 |
103 |
104 |
105 |
106 |
107 | Settings
108 |
109 |
110 |
118 |
119 |
120 |
121 |
122 | Help
123 |
124 |
125 |
126 |
136 |
146 |
147 |
148 |
157 | Copy public key
159 |
160 |
161 |
162 |
163 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
201 |
202 | Add public key to follow
203 |
204 |
205 |
206 |
209 | Following
210 |
211 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 | {{ $store.getters.handle(pubkey) }}
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
244 |
245 |
246 |
247 |
248 |
249 | INSTALL NOSTR?
250 |
251 |
252 |
259 |
266 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
285 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
374 |
375 |
391 |
--------------------------------------------------------------------------------
/src/assets/quasar-logo-full.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
43 |
45 |
46 |
48 | image/svg+xml
49 |
51 |
52 |
53 |
54 |
55 |
60 |
63 |
66 |
69 |
75 |
79 |
83 |
87 |
91 |
95 |
99 |
103 |
104 |
105 |
106 |
107 |
113 |
118 |
126 |
133 |
142 |
151 |
160 |
169 |
178 |
187 |
188 |
189 |
190 |
191 |
192 |
--------------------------------------------------------------------------------