├── 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 | 6 | 14 | -------------------------------------------------------------------------------- /src/pages/PageNotifications.vue: -------------------------------------------------------------------------------- 1 | 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 | 7 | 8 | 25 | -------------------------------------------------------------------------------- /src/pages/Error404.vue: -------------------------------------------------------------------------------- 1 | 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 | 68 | 69 | 84 | -------------------------------------------------------------------------------- /public/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Post.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 92 | -------------------------------------------------------------------------------- /src/components/Publish.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 101 | -------------------------------------------------------------------------------- /src/pages/PageHelp.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 117 | -------------------------------------------------------------------------------- /src/pages/PageProfile.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 142 | -------------------------------------------------------------------------------- /src/components/Reply.vue: -------------------------------------------------------------------------------- 1 | 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 | 134 | 135 | 223 | -------------------------------------------------------------------------------- /src/pages/PageChat.vue: -------------------------------------------------------------------------------- 1 | 131 | 132 | 234 | 235 | 249 | -------------------------------------------------------------------------------- /src/pages/PageSettings.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | --------------------------------------------------------------------------------