├── .editorconfig ├── .erb ├── configs │ ├── .eslintrc │ ├── webpack.config.base.ts │ ├── webpack.config.eslint.ts │ ├── webpack.config.main.prod.ts │ ├── webpack.config.preload.dev.ts │ ├── webpack.config.renderer.dev.dll.ts │ ├── webpack.config.renderer.dev.ts │ ├── webpack.config.renderer.prod.ts │ └── webpack.paths.ts ├── img │ ├── electron-nostr.png │ ├── erb-banner.svg │ ├── erb-logo.png │ └── palette-sponsor-banner.svg ├── mocks │ └── fileMock.js └── scripts │ ├── .eslintrc │ ├── check-build-exists.ts │ ├── check-native-dep.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── clean.js │ ├── delete-source-maps.js │ ├── electron-rebuild.js │ ├── link-modules.ts │ └── notarize.js ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-Bug_report.md │ ├── 2-Question.md │ └── 3-Feature_request.md ├── config.yml ├── stale.yml └── workflows │ ├── codeql-analysis.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── assets ├── assets.d.ts ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icon.png ├── icon.svg └── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ └── 96x96.png ├── notes.txt ├── package-lock.json ├── package.json ├── release └── app │ ├── images │ └── missingAvatar.png │ ├── package-lock.json │ └── package.json ├── src ├── __tests__ │ └── App.test.tsx ├── main │ ├── main.ts │ ├── menu.ts │ ├── preload.ts │ └── util.ts └── renderer │ ├── App-depr.tsx │ ├── App.tsx │ ├── css │ ├── app.css │ ├── directMessaging.css │ ├── editMyProfile.css │ ├── feed.css │ ├── follows.css │ ├── grapevine.css │ ├── grapevineSettings.css │ ├── grapevineToggleSwitch.css │ ├── mastheads.css │ ├── misc.css │ ├── myProfile.css │ ├── navbars.css │ ├── newPost.css │ ├── nfgGraphic.css │ ├── settings.css │ ├── toggleSwitch1.css │ ├── userList.css │ ├── userProfile.css │ └── youTubeEmbed.css │ ├── errorBoundary.js │ ├── grapevine │ ├── landingPage │ │ └── index.js │ └── nostrFollowGrapevine │ │ ├── graphic.js │ │ ├── index.js │ │ └── missingAvatar.png │ ├── index.ejs │ ├── index.tsx │ ├── lib │ ├── app │ │ ├── misc.ts │ │ ├── startup.ts │ │ ├── timer.js │ │ └── toggleSwitch1.js │ ├── nostr │ │ ├── eventValidation.js │ │ └── getFollowing.js │ └── visjs │ │ └── visjs-style.js │ ├── mastheads │ ├── avatarElem.js │ ├── blankAvatar.png │ └── mainMasthead.js │ ├── navbars │ ├── leftNav.js │ └── leftNav2 │ │ └── settings.js │ ├── pages │ ├── components │ │ ├── actionButtons.js │ │ ├── blankAvatar.png │ │ ├── followButton.tsx │ │ ├── relaysStatus.tsx │ │ ├── rootMessage.tsx │ │ ├── userPost.js │ │ ├── userPosts.js │ │ └── youTubeEmbed.js │ ├── createPost │ │ ├── index.tsx │ │ └── publishPost.tsx │ ├── directMessageConversation │ │ ├── convoHistory-deprecated.js │ │ ├── convoHistory.js │ │ ├── directMessage-archived.js │ │ ├── directMessage-deprecated.js │ │ ├── directMessage.js │ │ ├── index.js │ │ └── sendDirectMessage.tsx │ ├── downloadProfiles │ │ └── index.js │ ├── editMyProfile │ │ └── index.tsx │ ├── extendedFollowingList │ │ ├── fetchFollowingList.js │ │ └── index.js │ ├── followingList │ │ ├── index.js │ │ └── singleUserElem.js │ ├── grapevineSettings │ │ ├── grapevineIsActive.js │ │ ├── grapevineIsInactive.js │ │ ├── index.js │ │ ├── settingsMainComp.js │ │ └── toggleSwitch.js │ ├── landingPage │ │ └── index.js │ ├── mainFeed │ │ ├── index.js │ │ ├── mainFeedTypeSelector.js │ │ └── nameElem_deprecated.js │ ├── manageChannels │ │ └── index.js │ ├── myProfile │ │ └── index.tsx │ ├── reply │ │ └── index.tsx │ ├── searchForUser │ │ └── index.js │ ├── settings │ │ ├── index.js │ │ ├── profilekeys │ │ │ ├── index.js │ │ │ └── pk.js │ │ ├── relays │ │ │ ├── index.js │ │ │ └── relays.js │ │ └── sql │ │ │ ├── index.js │ │ │ └── sql.js │ ├── thread │ │ └── index.tsx │ ├── userList │ │ └── index.js │ ├── userProfile │ │ ├── followCounts.js │ │ ├── index.js │ │ ├── leaveGrapevineRatings.js │ │ ├── storeProfileLocallyButton.js │ │ └── userInfo.js │ └── visJsHelloWorld │ │ └── index.js │ └── preload.d.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.erb/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import webpackPaths from './webpack.paths'; 7 | import { dependencies as externals } from '../../release/app/package.json'; 8 | 9 | const configuration: webpack.Configuration = { 10 | externals: [...Object.keys(externals || {})], 11 | 12 | stats: 'errors-only', 13 | 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.[jt]sx?$/, 18 | exclude: /node_modules/, 19 | use: { 20 | loader: 'ts-loader', 21 | options: { 22 | // Remove this line to enable type checking in webpack builds 23 | transpileOnly: true, 24 | compilerOptions: { 25 | module: 'esnext', 26 | }, 27 | }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | 33 | output: { 34 | path: webpackPaths.srcPath, 35 | // https://github.com/webpack/webpack/issues/1114 36 | library: { 37 | type: 'commonjs2', 38 | }, 39 | }, 40 | 41 | /** 42 | * Determine the array of extensions that should be used to resolve modules. 43 | */ 44 | resolve: { 45 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 46 | modules: [webpackPaths.srcPath, 'node_modules'], 47 | fallback: { 48 | "stream": false, 49 | "util": false, 50 | } 51 | }, 52 | 53 | plugins: [ 54 | new webpack.EnvironmentPlugin({ 55 | NODE_ENV: 'production', 56 | }), 57 | ], 58 | }; 59 | 60 | export default configuration; 61 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | module.exports = require('./webpack.config.renderer.dev').default; 4 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.main.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { merge } from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | import deleteSourceMaps from '../scripts/delete-source-maps'; 14 | 15 | checkNodeEnv('production'); 16 | deleteSourceMaps(); 17 | 18 | const configuration: webpack.Configuration = { 19 | devtool: 'source-map', 20 | 21 | mode: 'production', 22 | 23 | target: 'electron-main', 24 | 25 | entry: { 26 | main: path.join(webpackPaths.srcMainPath, 'main.ts'), 27 | preload: path.join(webpackPaths.srcMainPath, 'preload.ts'), 28 | }, 29 | 30 | output: { 31 | path: webpackPaths.distMainPath, 32 | filename: '[name].js', 33 | library: { 34 | type: 'umd', 35 | }, 36 | }, 37 | 38 | optimization: { 39 | minimizer: [ 40 | new TerserPlugin({ 41 | parallel: true, 42 | }), 43 | ], 44 | }, 45 | 46 | plugins: [ 47 | new BundleAnalyzerPlugin({ 48 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 49 | analyzerPort: 8888, 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'production', 63 | DEBUG_PROD: false, 64 | START_MINIMIZED: false, 65 | }), 66 | 67 | new webpack.DefinePlugin({ 68 | 'process.type': '"main"', 69 | }), 70 | ], 71 | 72 | /** 73 | * Disables webpack processing of __dirname and __filename. 74 | * If you run the bundle in node.js it falls back to these values of node.js. 75 | * https://github.com/webpack/webpack/issues/2010 76 | */ 77 | node: { 78 | __dirname: false, 79 | __filename: false, 80 | }, 81 | }; 82 | 83 | export default merge(baseConfig, configuration); 84 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.preload.dev.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { merge } from 'webpack-merge'; 4 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 5 | import baseConfig from './webpack.config.base'; 6 | import webpackPaths from './webpack.paths'; 7 | import checkNodeEnv from '../scripts/check-node-env'; 8 | 9 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 10 | // at the dev webpack config is not accidentally run in a production environment 11 | if (process.env.NODE_ENV === 'production') { 12 | checkNodeEnv('development'); 13 | } 14 | 15 | const configuration: webpack.Configuration = { 16 | devtool: 'inline-source-map', 17 | 18 | mode: 'development', 19 | 20 | target: 'electron-preload', 21 | 22 | entry: path.join(webpackPaths.srcMainPath, 'preload.ts'), 23 | 24 | output: { 25 | path: webpackPaths.dllPath, 26 | filename: 'preload.js', 27 | library: { 28 | type: 'umd', 29 | }, 30 | }, 31 | 32 | plugins: [ 33 | new BundleAnalyzerPlugin({ 34 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 35 | }), 36 | 37 | /** 38 | * Create global constants which can be configured at compile time. 39 | * 40 | * Useful for allowing different behaviour between development builds and 41 | * release builds 42 | * 43 | * NODE_ENV should be production so that modules do not perform certain 44 | * development checks 45 | * 46 | * By default, use 'development' as NODE_ENV. This can be overriden with 47 | * 'staging', for example, by changing the ENV variables in the npm scripts 48 | */ 49 | new webpack.EnvironmentPlugin({ 50 | NODE_ENV: 'development', 51 | }), 52 | 53 | new webpack.LoaderOptionsPlugin({ 54 | debug: true, 55 | }), 56 | ], 57 | 58 | /** 59 | * Disables webpack processing of __dirname and __filename. 60 | * If you run the bundle in node.js it falls back to these values of node.js. 61 | * https://github.com/webpack/webpack/issues/2010 62 | */ 63 | node: { 64 | __dirname: false, 65 | __filename: false, 66 | }, 67 | 68 | watch: true, 69 | }; 70 | 71 | export default merge(baseConfig, configuration); 72 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.dev.dll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import path from 'path'; 7 | import { merge } from 'webpack-merge'; 8 | import baseConfig from './webpack.config.base'; 9 | import webpackPaths from './webpack.paths'; 10 | import { dependencies } from '../../package.json'; 11 | import checkNodeEnv from '../scripts/check-node-env'; 12 | 13 | checkNodeEnv('development'); 14 | 15 | const dist = webpackPaths.dllPath; 16 | 17 | const configuration: webpack.Configuration = { 18 | context: webpackPaths.rootPath, 19 | 20 | devtool: 'eval', 21 | 22 | mode: 'development', 23 | 24 | target: 'electron-renderer', 25 | 26 | externals: ['fsevents', 'crypto-browserify'], 27 | 28 | /** 29 | * Use `module` from `webpack.config.renderer.dev.js` 30 | */ 31 | module: require('./webpack.config.renderer.dev').default.module, 32 | 33 | entry: { 34 | renderer: Object.keys(dependencies || {}), 35 | }, 36 | 37 | output: { 38 | path: dist, 39 | filename: '[name].dev.dll.js', 40 | library: { 41 | name: 'renderer', 42 | type: 'var', 43 | }, 44 | }, 45 | 46 | plugins: [ 47 | new webpack.DllPlugin({ 48 | path: path.join(dist, '[name].json'), 49 | name: '[name]', 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'development', 63 | }), 64 | 65 | new webpack.LoaderOptionsPlugin({ 66 | debug: true, 67 | options: { 68 | context: webpackPaths.srcPath, 69 | output: { 70 | path: webpackPaths.dllPath, 71 | }, 72 | }, 73 | }), 74 | ], 75 | }; 76 | 77 | export default merge(baseConfig, configuration); 78 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.dev.ts: -------------------------------------------------------------------------------- 1 | import 'webpack-dev-server'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import webpack from 'webpack'; 5 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 6 | import chalk from 'chalk'; 7 | import { merge } from 'webpack-merge'; 8 | import { execSync, spawn } from 'child_process'; 9 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | 14 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 15 | // at the dev webpack config is not accidentally run in a production environment 16 | if (process.env.NODE_ENV === 'production') { 17 | checkNodeEnv('development'); 18 | } 19 | 20 | const port = process.env.PORT || 1212; 21 | const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json'); 22 | const skipDLLs = 23 | module.parent?.filename.includes('webpack.config.renderer.dev.dll') || 24 | module.parent?.filename.includes('webpack.config.eslint'); 25 | 26 | /** 27 | * Warn if the DLL is not built 28 | */ 29 | if ( 30 | !skipDLLs && 31 | !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest)) 32 | ) { 33 | console.log( 34 | chalk.black.bgYellow.bold( 35 | 'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"' 36 | ) 37 | ); 38 | execSync('npm run postinstall'); 39 | } 40 | 41 | const configuration: webpack.Configuration = { 42 | devtool: 'inline-source-map', 43 | 44 | mode: 'development', 45 | 46 | target: ['web', 'electron-renderer'], 47 | 48 | entry: [ 49 | `webpack-dev-server/client?http://localhost:${port}/dist`, 50 | 'webpack/hot/only-dev-server', 51 | path.join(webpackPaths.srcRendererPath, 'index.tsx'), 52 | ], 53 | 54 | output: { 55 | path: webpackPaths.distRendererPath, 56 | publicPath: '/', 57 | filename: 'renderer.dev.js', 58 | library: { 59 | type: 'umd', 60 | }, 61 | }, 62 | 63 | module: { 64 | rules: [ 65 | { 66 | test: /\.s?(c|a)ss$/, 67 | use: [ 68 | 'style-loader', 69 | { 70 | loader: 'css-loader', 71 | options: { 72 | modules: true, 73 | sourceMap: true, 74 | importLoaders: 1, 75 | }, 76 | }, 77 | 'sass-loader', 78 | ], 79 | include: /\.module\.s?(c|a)ss$/, 80 | }, 81 | { 82 | test: /\.s?css$/, 83 | use: ['style-loader', 'css-loader', 'sass-loader'], 84 | exclude: /\.module\.s?(c|a)ss$/, 85 | }, 86 | // Fonts 87 | { 88 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 89 | type: 'asset/resource', 90 | }, 91 | // Images 92 | { 93 | test: /\.(png|jpg|jpeg|gif)$/i, 94 | type: 'asset/resource', 95 | }, 96 | // SVG 97 | { 98 | test: /\.svg$/, 99 | use: [ 100 | { 101 | loader: '@svgr/webpack', 102 | options: { 103 | prettier: false, 104 | svgo: false, 105 | svgoConfig: { 106 | plugins: [{ removeViewBox: false }], 107 | }, 108 | titleProp: true, 109 | ref: true, 110 | }, 111 | }, 112 | 'file-loader', 113 | ], 114 | }, 115 | ], 116 | }, 117 | plugins: [ 118 | ...(skipDLLs 119 | ? [] 120 | : [ 121 | new webpack.DllReferencePlugin({ 122 | context: webpackPaths.dllPath, 123 | manifest: require(manifest), 124 | sourceType: 'var', 125 | }), 126 | ]), 127 | 128 | new webpack.NoEmitOnErrorsPlugin(), 129 | 130 | /** 131 | * Create global constants which can be configured at compile time. 132 | * 133 | * Useful for allowing different behaviour between development builds and 134 | * release builds 135 | * 136 | * NODE_ENV should be production so that modules do not perform certain 137 | * development checks 138 | * 139 | * By default, use 'development' as NODE_ENV. This can be overriden with 140 | * 'staging', for example, by changing the ENV variables in the npm scripts 141 | */ 142 | new webpack.EnvironmentPlugin({ 143 | NODE_ENV: 'development', 144 | }), 145 | 146 | new webpack.LoaderOptionsPlugin({ 147 | debug: true, 148 | }), 149 | 150 | new ReactRefreshWebpackPlugin(), 151 | 152 | new HtmlWebpackPlugin({ 153 | filename: path.join('index.html'), 154 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 155 | minify: { 156 | collapseWhitespace: true, 157 | removeAttributeQuotes: true, 158 | removeComments: true, 159 | }, 160 | isBrowser: false, 161 | env: process.env.NODE_ENV, 162 | isDevelopment: process.env.NODE_ENV !== 'production', 163 | nodeModules: webpackPaths.appNodeModulesPath, 164 | }), 165 | ], 166 | 167 | node: { 168 | __dirname: false, 169 | __filename: false, 170 | }, 171 | 172 | devServer: { 173 | port, 174 | compress: true, 175 | hot: true, 176 | headers: { 'Access-Control-Allow-Origin': '*' }, 177 | static: { 178 | publicPath: '/', 179 | }, 180 | historyApiFallback: { 181 | verbose: true, 182 | }, 183 | setupMiddlewares(middlewares) { 184 | console.log('Starting preload.js builder...'); 185 | const preloadProcess = spawn('npm', ['run', 'start:preload'], { 186 | shell: true, 187 | stdio: 'inherit', 188 | }) 189 | .on('close', (code: number) => process.exit(code!)) 190 | .on('error', (spawnError) => console.error(spawnError)); 191 | 192 | console.log('Starting Main Process...'); 193 | let args = ['run', 'start:main']; 194 | if (process.env.MAIN_ARGS) { 195 | args = args.concat( 196 | ['--', ...process.env.MAIN_ARGS.matchAll(/"[^"]+"|[^\s"]+/g)].flat() 197 | ); 198 | } 199 | spawn('npm', args, { 200 | shell: true, 201 | stdio: 'inherit', 202 | }) 203 | .on('close', (code: number) => { 204 | preloadProcess.kill(); 205 | process.exit(code!); 206 | }) 207 | .on('error', (spawnError) => console.error(spawnError)); 208 | return middlewares; 209 | }, 210 | }, 211 | }; 212 | 213 | export default merge(baseConfig, configuration); 214 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron renderer process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 8 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; 11 | import { merge } from 'webpack-merge'; 12 | import TerserPlugin from 'terser-webpack-plugin'; 13 | import baseConfig from './webpack.config.base'; 14 | import webpackPaths from './webpack.paths'; 15 | import checkNodeEnv from '../scripts/check-node-env'; 16 | import deleteSourceMaps from '../scripts/delete-source-maps'; 17 | 18 | checkNodeEnv('production'); 19 | deleteSourceMaps(); 20 | 21 | const configuration: webpack.Configuration = { 22 | devtool: 'source-map', 23 | 24 | mode: 'production', 25 | 26 | target: ['web', 'electron-renderer'], 27 | 28 | entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')], 29 | 30 | output: { 31 | path: webpackPaths.distRendererPath, 32 | publicPath: './', 33 | filename: 'renderer.js', 34 | library: { 35 | type: 'umd', 36 | }, 37 | }, 38 | 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.s?(a|c)ss$/, 43 | use: [ 44 | MiniCssExtractPlugin.loader, 45 | { 46 | loader: 'css-loader', 47 | options: { 48 | modules: true, 49 | sourceMap: true, 50 | importLoaders: 1, 51 | }, 52 | }, 53 | 'sass-loader', 54 | ], 55 | include: /\.module\.s?(c|a)ss$/, 56 | }, 57 | { 58 | test: /\.s?(a|c)ss$/, 59 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 60 | exclude: /\.module\.s?(c|a)ss$/, 61 | }, 62 | // Fonts 63 | { 64 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 65 | type: 'asset/resource', 66 | }, 67 | // Images 68 | { 69 | test: /\.(png|jpg|jpeg|gif)$/i, 70 | type: 'asset/resource', 71 | }, 72 | // SVG 73 | { 74 | test: /\.svg$/, 75 | use: [ 76 | { 77 | loader: '@svgr/webpack', 78 | options: { 79 | prettier: false, 80 | svgo: false, 81 | svgoConfig: { 82 | plugins: [{ removeViewBox: false }], 83 | }, 84 | titleProp: true, 85 | ref: true, 86 | }, 87 | }, 88 | 'file-loader', 89 | ], 90 | }, 91 | ], 92 | }, 93 | 94 | optimization: { 95 | minimize: true, 96 | minimizer: [ 97 | new TerserPlugin({ 98 | parallel: true, 99 | }), 100 | new CssMinimizerPlugin(), 101 | ], 102 | }, 103 | 104 | plugins: [ 105 | /** 106 | * Create global constants which can be configured at compile time. 107 | * 108 | * Useful for allowing different behaviour between development builds and 109 | * release builds 110 | * 111 | * NODE_ENV should be production so that modules do not perform certain 112 | * development checks 113 | */ 114 | new webpack.EnvironmentPlugin({ 115 | NODE_ENV: 'production', 116 | DEBUG_PROD: false, 117 | }), 118 | 119 | new MiniCssExtractPlugin({ 120 | filename: 'style.css', 121 | }), 122 | 123 | new BundleAnalyzerPlugin({ 124 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 125 | analyzerPort: 8889, 126 | }), 127 | 128 | new HtmlWebpackPlugin({ 129 | filename: 'index.html', 130 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 131 | minify: { 132 | collapseWhitespace: true, 133 | removeAttributeQuotes: true, 134 | removeComments: true, 135 | }, 136 | isBrowser: false, 137 | isDevelopment: process.env.NODE_ENV !== 'production', 138 | }), 139 | 140 | new webpack.DefinePlugin({ 141 | 'process.type': '"renderer"', 142 | }), 143 | ], 144 | }; 145 | 146 | export default merge(baseConfig, configuration); 147 | -------------------------------------------------------------------------------- /.erb/configs/webpack.paths.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootPath = path.join(__dirname, '../..'); 4 | 5 | const dllPath = path.join(__dirname, '../dll'); 6 | 7 | const srcPath = path.join(rootPath, 'src'); 8 | const srcMainPath = path.join(srcPath, 'main'); 9 | const srcRendererPath = path.join(srcPath, 'renderer'); 10 | 11 | const releasePath = path.join(rootPath, 'release'); 12 | const appPath = path.join(releasePath, 'app'); 13 | const appPackagePath = path.join(appPath, 'package.json'); 14 | const appNodeModulesPath = path.join(appPath, 'node_modules'); 15 | const srcNodeModulesPath = path.join(srcPath, 'node_modules'); 16 | 17 | const distPath = path.join(appPath, 'dist'); 18 | const distMainPath = path.join(distPath, 'main'); 19 | const distRendererPath = path.join(distPath, 'renderer'); 20 | 21 | const buildPath = path.join(releasePath, 'build'); 22 | 23 | export default { 24 | rootPath, 25 | dllPath, 26 | srcPath, 27 | srcMainPath, 28 | srcRendererPath, 29 | releasePath, 30 | appPath, 31 | appPackagePath, 32 | appNodeModulesPath, 33 | srcNodeModulesPath, 34 | distPath, 35 | distMainPath, 36 | distRendererPath, 37 | buildPath, 38 | }; 39 | -------------------------------------------------------------------------------- /.erb/img/electron-nostr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/.erb/img/electron-nostr.png -------------------------------------------------------------------------------- /.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /.erb/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/check-build-exists.ts: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); 8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); 9 | 10 | if (!fs.existsSync(mainPath)) { 11 | throw new Error( 12 | chalk.whiteBright.bgRed.bold( 13 | 'The main process is not built yet. Build it by running "npm run build:main"' 14 | ) 15 | ); 16 | } 17 | 18 | if (!fs.existsSync(rendererPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"' 22 | ) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /.erb/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { execSync } from 'child_process'; 4 | import { dependencies } from '../../package.json'; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | 9 | const nativeDeps = fs 10 | .readdirSync('node_modules') 11 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 12 | if (nativeDeps.length === 0) { 13 | process.exit(0); 14 | } 15 | console.log("nativeDeps: "+JSON.stringify(nativeDeps,null,4)) 16 | try { 17 | // Find the reason for why the dependency is installed. If it is installed 18 | // because of a devDependency then that is okay. Warn when it is installed 19 | // because of a dependency 20 | const { dependencies: dependenciesObject } = JSON.parse( 21 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() 22 | ); 23 | const rootDependencies = Object.keys(dependenciesObject); 24 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 25 | dependenciesKeys.includes(rootDependency) 26 | ); 27 | if (filteredRootDependencies.length > 0) { 28 | const plural = filteredRootDependencies.length > 1; 29 | console.log("dependenciesObject: "+JSON.stringify(dependenciesObject,null,4)) 30 | console.log("filteredRootDependencies: "+JSON.stringify(filteredRootDependencies,null,4)) 31 | console.log(` 32 | ${chalk.whiteBright.bgYellow.bold( 33 | 'Webpack does not work with native dependencies.' 34 | )} 35 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 36 | plural ? 'are native dependencies' : 'is a native dependency' 37 | } and should be installed inside of the "./release/app" folder. 38 | First, uninstall the packages from "./package.json": 39 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 40 | ${chalk.bold( 41 | 'Then, instead of installing the package to the root "./package.json":' 42 | )} 43 | ${chalk.whiteBright.bgRed.bold('npm install your-package')} 44 | ${chalk.bold('Install the package to "./release/app/package.json"')} 45 | ${chalk.whiteBright.bgGreen.bold( 46 | 'cd ./release/app && npm install your-package' 47 | )} 48 | Read more about native dependencies at: 49 | ${chalk.bold( 50 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure' 51 | )} 52 | `); 53 | process.exit(1); 54 | } 55 | } catch (e) { 56 | console.log('Native dependencies could not be checked'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.erb/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 12 | ) 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.erb/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start` 11 | ) 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import rimraf from 'rimraf'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const foldersToRemove = [ 5 | webpackPaths.distPath, 6 | webpackPaths.buildPath, 7 | webpackPaths.dllPath, 8 | ]; 9 | 10 | foldersToRemove.forEach((folder) => { 11 | rimraf.sync(folder); 12 | }); 13 | -------------------------------------------------------------------------------- /.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rimraf from 'rimraf'; 3 | import webpackPaths from '../configs/webpack.paths'; 4 | 5 | export default function deleteSourceMaps() { 6 | rimraf.sync(path.join(webpackPaths.distMainPath, '*.js.map')); 7 | rimraf.sync(path.join(webpackPaths.distRendererPath, '*.js.map')); 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | import { dependencies } from '../../release/app/package.json'; 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | if ( 7 | Object.keys(dependencies || {}).length > 0 && 8 | fs.existsSync(webpackPaths.appNodeModulesPath) 9 | ) { 10 | const electronRebuildCmd = 11 | '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .'; 12 | const cmd = 13 | process.platform === 'win32' 14 | ? electronRebuildCmd.replace(/\//g, '\\') 15 | : electronRebuildCmd; 16 | execSync(cmd, { 17 | cwd: webpackPaths.appPath, 18 | stdio: 'inherit', 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /.erb/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const { srcNodeModulesPath } = webpackPaths; 5 | const { appNodeModulesPath } = webpackPaths; 6 | 7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction'); 9 | } 10 | -------------------------------------------------------------------------------- /.erb/scripts/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('electron-notarize'); 2 | const { build } = require('../../package.json'); 3 | 4 | exports.default = async function notarizeMacos(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | if (process.env.CI !== 'true') { 11 | console.warn('Skipping notarizing step. Packaging is not running in CI'); 12 | return; 13 | } 14 | 15 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { 16 | console.warn( 17 | 'Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set' 18 | ); 19 | return; 20 | } 21 | 22 | const appName = context.packager.appInfo.productFilename; 23 | 24 | await notarize({ 25 | appBundleId: build.appId, 26 | appPath: `${appOutDir}/${appName}.app`, 27 | appleId: process.env.APPLE_ID, 28 | appleIdPassword: process.env.APPLE_ID_PASS, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | 31 | # eslint ignores hidden directories by default: 32 | # https://github.com/eslint/eslint/issues/8429 33 | !.erb 34 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb', 3 | rules: { 4 | // A temporary hack related to IDE not resolving correct package.json 5 | 'import/no-extraneous-dependencies': 'off', 6 | 'import/no-unresolved': 'error', 7 | // Since React 17 and typescript 4.1 you can safely disable the rule 8 | 'react/react-in-jsx-scope': 'off', 9 | }, 10 | parserOptions: { 11 | ecmaVersion: 2020, 12 | sourceType: 'module', 13 | project: './tsconfig.json', 14 | tsconfigRootDir: __dirname, 15 | createDefaultProgram: true, 16 | }, 17 | settings: { 18 | 'import/resolver': { 19 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 20 | node: {}, 21 | webpack: { 22 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'), 23 | }, 24 | typescript: {}, 25 | }, 26 | 'import/parsers': { 27 | '@typescript-eslint/parser': ['.ts', '.tsx'], 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.exe binary 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.ico binary 7 | *.icns binary 8 | *.eot binary 9 | *.otf binary 10 | *.ttf binary 11 | *.woff binary 12 | *.woff2 binary 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [electron-react-boilerplate-nostr, wds4] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: You're having technical issues. 🐞 4 | labels: 'bug' 5 | --- 6 | 7 | 8 | 9 | ## Prerequisites 10 | 11 | 12 | 13 | - [ ] Using npm 14 | - [ ] Using an up-to-date [`main` branch](https://github.com/electron-react-boilerplate/electron-react-boilerplate/tree/main) 15 | - [ ] Using latest version of devtools. [Check the docs for how to update](https://electron-react-boilerplate.js.org/docs/dev-tools/) 16 | - [ ] Tried solutions mentioned in [#400](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400) 17 | - [ ] For issue in production release, add devtools output of `DEBUG_PROD=true npm run build && npm start` 18 | 19 | ## Expected Behavior 20 | 21 | 22 | 23 | ## Current Behavior 24 | 25 | 26 | 27 | ## Steps to Reproduce 28 | 29 | 30 | 31 | 32 | 1. 33 | 34 | 2. 35 | 36 | 3. 37 | 38 | 4. 39 | 40 | ## Possible Solution (Not obligatory) 41 | 42 | 43 | 44 | ## Context 45 | 46 | 47 | 48 | 49 | 50 | ## Your Environment 51 | 52 | 53 | 54 | - Node version : 55 | - electron-react-boilerplate version or branch : 56 | - Operating System and version : 57 | - Link to your project : 58 | 59 | 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question.❓ 4 | labels: 'question' 5 | --- 6 | 7 | ## Summary 8 | 9 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: You want something added to the boilerplate. 🎉 4 | labels: 'enhancement' 5 | --- 6 | 7 | 16 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | requiredHeaders: 2 | - Prerequisites 3 | - Expected Behavior 4 | - Current Behavior 5 | - Possible Solution 6 | - Your Environment 7 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - discussion 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '44 16 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | # To enable auto publishing to github, update your electron publisher 11 | # config in package.json > "build" and remove the conditional below 12 | if: ${{ github.repository_owner == 'electron-react-boilerplate' }} 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [macos-latest] 19 | 20 | steps: 21 | - name: Checkout git repo 22 | uses: actions/checkout@v3 23 | 24 | - name: Install Node and NPM 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: 16 28 | cache: npm 29 | 30 | - name: Install and build 31 | run: | 32 | npm install 33 | npm run postinstall 34 | npm run build 35 | 36 | - name: Publish releases 37 | env: 38 | # These values are used for auto updates signing 39 | APPLE_ID: ${{ secrets.APPLE_ID }} 40 | APPLE_ID_PASS: ${{ secrets.APPLE_ID_PASS }} 41 | CSC_LINK: ${{ secrets.CSC_LINK }} 42 | CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} 43 | # This is used for uploading release assets to github 44 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | run: | 46 | npm exec electron-builder -- --publish always --win --mac --linux 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: [macos-latest, windows-latest, ubuntu-latest] 12 | 13 | steps: 14 | - name: Check out Git repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Install Node.js and NPM 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 16 21 | cache: npm 22 | 23 | - name: npm install 24 | run: | 25 | npm install 26 | 27 | - name: npm test 28 | env: 29 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: | 31 | npm run package 32 | npm run lint 33 | npm exec tsc 34 | npm test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/app/sql 23 | release/build 24 | .erb/dll 25 | 26 | .idea 27 | npm-debug.log.* 28 | *.css.d.ts 29 | *.sass.d.ts 30 | *.scss.d.ts 31 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Electron: Main", 6 | "type": "node", 7 | "request": "launch", 8 | "protocol": "inspector", 9 | "runtimeExecutable": "npm", 10 | "runtimeArgs": ["run", "start"], 11 | "env": { 12 | "MAIN_ARGS": "--inspect=5858 --remote-debugging-port=9223" 13 | } 14 | }, 15 | { 16 | "name": "Electron: Renderer", 17 | "type": "chrome", 18 | "request": "attach", 19 | "port": 9223, 20 | "webRoot": "${workspaceFolder}", 21 | "timeout": 15000 22 | } 23 | ], 24 | "compounds": [ 25 | { 26 | "name": "Electron: All", 27 | "configurations": ["Electron: Main", "Electron: Renderer"] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".eslintrc": "jsonc", 4 | ".prettierrc": "jsonc", 5 | ".eslintignore": "ignore" 6 | }, 7 | 8 | "eslint.validate": [ 9 | "javascript", 10 | "javascriptreact", 11 | "html", 12 | "typescriptreact" 13 | ], 14 | 15 | "javascript.validate.enable": false, 16 | "javascript.format.enable": false, 17 | "typescript.format.enable": false, 18 | 19 | "search.exclude": { 20 | ".git": true, 21 | ".eslintcache": true, 22 | ".erb/dll": true, 23 | "release/{build,app/dist}": true, 24 | "node_modules": true, 25 | "npm-debug.log.*": true, 26 | "test/**/__snapshots__": true, 27 | "package-lock.json": true, 28 | "*.{css,sass,scss}.d.ts": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at electronreactboilerplate@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Electron React Boilerplate 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | As of early 2023, this project has been refactored and moved to [Pretty Good Apps](https://github.com/wds4/pretty-good). See also this repo: [Decentralized Curation of Simple Lists](https://github.com/wds4/DCoSL), which specifies the protocol for decentralized reputation that is implemented in the Pretty Good Apps client. 2 | 3 |
4 | 5 | Boilerplate nostr desktop client using electron, react, sqlite3, and electron-builder. [electron-react-boilerplate](https://github.com/electron-react-boilerplate/electron-react-boilerplate) is my starting point, to which I have added [nostr-tools](https://github.com/fiatjaf/nostr-tools) and [nostr-react](https://github.com/t4t5/nostr-react), as well as [sqlite3](https://github.com/TryGhost/node-sqlite3). 6 | 7 | Once this project has progressed far enough, I plan to incorporate a system of decentralized reputation, two projects that I call the Grapevine and the Concept Graph. I have some very thoroughly fleshed out ideas on how to do this. 8 | 9 | However, I think a lot of people have a lot of different ideas on how to build decentralized ratings, reputation, and web of trust. In my mind, we haven't yet figured out how to do this right, and this is perhaps the greatest unsolved problem in this space. Therefore, I also plan to maintain the electron-nostr boilerpate as a template minus my own proposed solutions (the Grapevine and the Concept Graph). My hope is that the electron-nostr template will be a useful tool for a wide range of people who want to play around with their own ideas on how to build a web of trust. If you have ideas you want to try out and you know css, html, and javascript, hopefully your learning curve (basics of git, react, sqlite3) will be as painless as possible. The more ideas we can try, the better for us all! 10 | 11 | I have been using [vis.js](https://visjs.org) tools (currently: [vis-data](https://github.com/visjs/vis-data) and [vis-network](https://github.com/visjs/vis-network)) to visualize the Grapevine web of trust, so I may include these in the electron-nostr boilerplate as well as any other tools that I think may be generically useful for others in their experimentation. Support for IPFS and [gun](https://github.com/amark/gun) come to mind. 12 | 13 | Let me know if you would like to use and/or contribute to the basic electron-nostr template. If there is enough interest, I may be able to secure a small amount of funding to support its development and maintenance. 14 | 15 | ## Features 16 | 17 | Currently, this app works in dev mode. The mac release (v0.1.x-alpha) also is functional. linux and windows releases are not yet functional. 18 | 19 | - automatic generation of privkey and pubkey 20 | - abiliity to import privkey and pubkey 21 | - view and manage basics of your profile (name, picture_url, etc) 22 | - support for multiple profiles 23 | - main feed in 3 modes: "following" (your follows), "Extended following" (+ their follows) and "firehose" (completely unfiltered) 24 | - view other profiles 25 | - follow / unfollow button 26 | - submit a post 27 | - reply to a post 28 | - thread viewer (very basic implementation) 29 | - youtube video playback 30 | - extended 31 | - graphical visualization of extended following list using vis.js (in v0.2.0-alpha release) 32 | 33 | 34 | 35 | ## Todo 36 | 37 | - direct messaging 38 | - refactor thread viewer 39 | - refactor following page (need to request events for info on all users at once, not separate request for each user) 40 | - add retweet, like buttons 41 | - add notifications 42 | - show pictures in posts 43 | - implement caching (redux?) 44 | - calculate and show number of followers 45 | - maybe change location of sql database 46 | - lightning invoice support 47 | - back button 48 | - bookmark events 49 | - pin events 50 | - support for multiple accounts 51 | - support for multithreading (multiple electron rendering processes) using the same techniques of [this template](https://github.com/wds4/electron-react-boilerplate-multiple-windows) 52 | 53 | ## Known issues 54 | 55 | - Websockets sometimes do not reconnect after being dropped 56 | - Follow buttons sometimes do not show change in follow status (or are slow to reflect change) 57 | - v0.1.x-alpha linux build so far does not function - problem with sqlite3 I think 58 | 59 | ## Install 60 | 61 | Clone the repo, install dependencies, make sql directory: 62 | 63 | ```bash 64 | git clone https://github.com/wds4/electron-react-boilerplate-nostr.git your-project-name 65 | cd your-project-name 66 | npm install 67 | cd release/app 68 | mkdir sql 69 | cd ../.. 70 | ``` 71 | 72 | ## Starting Development 73 | 74 | Start the app in the `dev` environment: 75 | 76 | ```bash 77 | npm start 78 | ``` 79 | 80 | ## Packaging for Production 81 | 82 | To package apps for the local platform: 83 | 84 | ```bash 85 | npm run package 86 | ``` 87 | To package for other platforms, see [these instructions](https://electron-react-boilerplate.js.org/docs/packaging). 88 | 89 | ## License 90 | 91 | MIT © [Electron React Boilerplate](https://github.com/electron-react-boilerplate) 92 | -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module '*.svg' { 4 | export const ReactComponent: React.FC>; 5 | 6 | const content: string; 7 | export default content; 8 | } 9 | 10 | declare module '*.png' { 11 | const content: string; 12 | export default content; 13 | } 14 | 15 | declare module '*.jpg' { 16 | const content: string; 17 | export default content; 18 | } 19 | 20 | declare module '*.scss' { 21 | const content: Styles; 22 | export default content; 23 | } 24 | 25 | declare module '*.sass' { 26 | const content: Styles; 27 | export default content; 28 | } 29 | 30 | declare module '*.css' { 31 | const content: Styles; 32 | export default content; 33 | } 34 | -------------------------------------------------------------------------------- /assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icon.png -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icons/24x24.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icons/48x48.png -------------------------------------------------------------------------------- /assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icons/512x512.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icons/64x64.png -------------------------------------------------------------------------------- /assets/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/assets/icons/96x96.png -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | 2 | 19 Dec 2022 3 | electron-react-boilerplate-nostr 4 | Purpose: to do a hello world of nostr in erb 5 | Forked erb in github 6 | clones to MBPro 7 | 8 | made cosmetic changes: imported from my template electron-react-boilerplate-ipfs 9 | 10 | following https://github.com/fiatjaf/nostr-tools 11 | 12 | npm install nostr-tools 13 | added to home.js: 14 | import { generatePrivateKey, getPublicKey } from 'nostr-tools' 15 | 16 | Got polyfill error; this is how I fixed it: 17 | Followed advice from: 18 | https://stackoverflow.com/questions/64557638/how-to-polyfill-node-core-modules-in-webpack-5 19 | and updated .erb/configs/webpack.config.base.ts by adding under resolve: 20 | fallback: { 21 | "stream": false, 22 | } 23 | -------------------------------------------------------------------------------- /release/app/images/missingAvatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/release/app/images/missingAvatar.png -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-react-boilerplate-nostr", 3 | "version": "0.3.1-alpha", 4 | "description": "A foundation for building a decentralized web client using electron and nostr", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Electron React Boilerplate Nostr Maintainers", 8 | "url": "https://github.com/wds4" 9 | }, 10 | "main": "./dist/main/main.js", 11 | "scripts": { 12 | "rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", 13 | "postinstall": "npm run rebuild && npm run link-modules", 14 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts" 15 | }, 16 | "dependencies": { 17 | "sqlite3": "^5.1.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { render } from '@testing-library/react'; 3 | import App from '../renderer/App'; 4 | 5 | describe('App', () => { 6 | it('should render', () => { 7 | expect(render()).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/main/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; 2 | 3 | export type Channels = 'ipc-example'; 4 | 5 | contextBridge.exposeInMainWorld('electron', { 6 | ipcRenderer: { 7 | sendMessage(channel: Channels, args: unknown[]) { 8 | ipcRenderer.send(channel, args); 9 | }, 10 | on(channel: Channels, func: (...args: unknown[]) => void) { 11 | const subscription = (_event: IpcRendererEvent, ...args: unknown[]) => 12 | func(...args); 13 | ipcRenderer.on(channel, subscription); 14 | 15 | return () => { 16 | ipcRenderer.removeListener(channel, subscription); 17 | }; 18 | }, 19 | once(channel: Channels, func: (...args: unknown[]) => void) { 20 | ipcRenderer.once(channel, (_event, ...args) => func(...args)); 21 | }, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | import { URL } from 'url'; 3 | import path from 'path'; 4 | 5 | export function resolveHtmlPath(htmlFileName: string) { 6 | if (process.env.NODE_ENV === 'development') { 7 | const port = process.env.PORT || 1212; 8 | const url = new URL(`http://localhost:${port}`); 9 | url.pathname = htmlFileName; 10 | return url.href; 11 | } 12 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/App-depr.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MemoryRouter as Router, Routes, Route } from 'react-router-dom'; 3 | import { NostrProvider } from 'nostr-react'; 4 | import * as MiscAppFxns from './lib/app/misc'; 5 | import ErrorBoundary from './errorBoundary'; 6 | import * as TsdxTest from '@wds4/tsdxtest'; 7 | import * as TsPlex from '@wds4/tsplex'; 8 | 9 | 10 | // import { sum as summ } from '@wds4/tsdxtest'; 11 | 12 | // import { Provider } from 'react-redux' 13 | // import store from './store' 14 | 15 | // NOSTR PAGES 16 | import LandingPage from './pages/landingPage/index'; 17 | import Settings from './pages/settings/index'; 18 | import Settings_keys from './pages/settings/profilekeys/index'; 19 | import Settings_relays from './pages/settings/relays/index'; 20 | import Settings_sql from './pages/settings/sql/index'; 21 | import MainFeed from './pages/mainFeed/index'; 22 | import ManageChannels from './pages/manageChannels/index'; 23 | import UserProfile from './pages/userProfile/index'; 24 | import FollowingList from './pages/followingList/index'; 25 | import ExtendedFollowingList from './pages/extendedFollowingList/index'; 26 | import DownloadProfiles from './pages/downloadProfiles/index'; 27 | import UserList from './pages/userList/index'; 28 | import MyProfile from './pages/myProfile/index'; 29 | import EditMyProfile from './pages/editMyProfile/index'; 30 | import CreatePost from './pages/createPost/index'; 31 | import Thread from './pages/thread/index'; 32 | import Reply from './pages/reply/index'; 33 | import SearchForUser from './pages/searchForUser/index'; 34 | import GrapevineSettings from './pages/grapevineSettings/index'; 35 | import VisJsHelloWorld from './pages/visJsHelloWorld/index'; 36 | import DirectMessageConversation from './pages/directMessageConversation/index'; 37 | 38 | // GRAPEVINE PAGES 39 | import GrapevineLandingPage from './grapevine/landingPage/index'; 40 | import NostrFollowGrapevineVisualization from './grapevine/nostrFollowGrapevine/index'; 41 | 42 | import './css/app.css'; 43 | import './css/mastheads.css'; 44 | import './css/navbars.css'; 45 | import './css/feed.css'; 46 | import './css/userProfile.css'; 47 | import './css/myProfile.css'; 48 | import './css/editMyProfile.css'; 49 | import './css/newPost.css'; 50 | import './css/userList.css'; 51 | import './css/follows.css'; 52 | import './css/grapevine.css'; 53 | import './css/toggleSwitch1.css'; // probably will use just one of these 54 | import './css/grapevineToggleSwitch.css'; 55 | import './css/grapevineSettings.css'; 56 | import './css/nfgGraphic.css'; 57 | import './css/youTubeEmbed.css'; 58 | import './css/misc.css'; 59 | import './css/settings.css'; 60 | import './css/directMessaging.css'; 61 | 62 | import { asyncSql } from "./index.tsx"; 63 | 64 | const fetchRelays = MiscAppFxns.fetchRelays; 65 | const timeout = MiscAppFxns.timeout; 66 | 67 | const relayUrls = [ 68 | "wss://nostr-pub.wellorder.net", 69 | "wss://nostr-relay.untethr.me", 70 | "wss://relay.damus.io", 71 | "wss://nostr-relay.wlvs.space", 72 | "wss://nostr.fmt.wiz.biz", 73 | "wss://nostr.oxtr.dev", 74 | ]; 75 | 76 | // window.relayUrls = []; 77 | 78 | const updateMainColWidth = MiscAppFxns.updateMainColWidth; 79 | 80 | export default class App extends React.Component { 81 | constructor(props) { 82 | super(props); 83 | this.state = { 84 | } 85 | } 86 | 87 | async componentDidMount() { 88 | } 89 | render() { 90 | return ( 91 | 92 | 93 |
94 | sum of 1+2: {TsPlex.grapevineSum(1,2)} 95 |
 96 |                     
 97 |                         {typeof this.state.relayUrls}
 98 |                         {this.state.relayUrls.length}
 99 |                         {this.state.relayUrls}
100 |                         

101 | {window.relayUrls} 102 |
103 | 104 | 105 | 106 | } /> 107 | } /> 108 | } /> 109 | } /> 110 | } /> 111 | } /> 112 | } /> 113 | } /> 114 | } /> 115 | } /> 116 | } /> 117 | } /> 118 | } /> 119 | } /> 120 | } /> 121 | } /> 122 | } /> 123 | } /> 124 | } /> 125 | } /> 126 | } /> 127 | } /> 128 | } /> 129 | } /> 130 | } /> 131 | } /> 132 | 133 | 134 |
135 |
136 |
137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/renderer/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 'Helvetica'; 4 | position: relative; 5 | color: white; 6 | height: 100%; 7 | width:100%; 8 | overflow-y: hidden; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | 14 | html, body, body>div, #app { 15 | color:black; 16 | box-sizing: border-box; 17 | height:100%; 18 | width:100%; 19 | } 20 | 21 | fieldset#app { 22 | box-sizing: border-box; 23 | height:100%; 24 | width:100%; 25 | } 26 | 27 | div, pre { 28 | box-sizing: border-box; 29 | vertical-align:top; 30 | 31 | white-space: pre-wrap; 32 | white-space: -moz-pre-wrap; 33 | white-space: -pre-wrap; 34 | white-space: -o-pre-wrap; 35 | word-wrap: break-word; 36 | 37 | } 38 | 39 | :root { 40 | --select-border: ; 41 | --select-focus: #101484; 42 | --select-arrow: var(--select-border); 43 | } 44 | 45 | select { 46 | min-width: 5ch; 47 | max-width: 30ch; 48 | border: 1px solid #393939; 49 | border-radius: 0.25em; 50 | padding: 3px; 51 | font-size: 1.0rem; 52 | cursor: pointer; 53 | background: linear-gradient(to bottom, #ffffff 0%, #e5e5e5 100%); 54 | } 55 | 56 | 57 | div.h1 { 58 | font-size:36px; 59 | text-align:center; 60 | } 61 | div.h2 { 62 | font-size:28px; 63 | text-align:center; 64 | } 65 | div.h3 { 66 | font-size:22px; 67 | text-align:center; 68 | } 69 | div.h4 { 70 | font-size:18px; 71 | text-align:center; 72 | } 73 | .leftCol { 74 | display:inline-block; 75 | width:200px; 76 | } 77 | .rightCol { 78 | display:inline-block; 79 | width:700px; 80 | } 81 | #root { 82 | padding:0px; 83 | margin:0px; 84 | height:100%; 85 | width:100%; 86 | } 87 | #app { 88 | padding:0px; 89 | margin:0px; 90 | height:100%; 91 | width:100%; 92 | white-space: nowrap; 93 | } 94 | #menuCol { 95 | display:inline-block; 96 | height:100%; 97 | } 98 | #mainCol { 99 | padding:0px 5px 0px 5px; 100 | margin:0px; 101 | display:inline-block; 102 | height:100%; 103 | background-color:#EFEFEF; 104 | } 105 | #mainPanel { 106 | width:100%; 107 | height:80%; 108 | padding-top:20px; 109 | font-size:12px; 110 | overflow:scroll; 111 | } 112 | 113 | .doSomethingButton { 114 | font-size:12px; 115 | display:inline-block; 116 | border:1px solid black; 117 | border-radius:3px; 118 | padding:4px; 119 | margin:4px; 120 | background-color:#DFDFDF; 121 | color:black; 122 | } 123 | .doSomethingButton:hover { 124 | background-color:yellow; 125 | } 126 | .doSomethingButton_small { 127 | font-size:10px; 128 | display:inline-block; 129 | border:1px solid black; 130 | border-radius:3px; 131 | padding:1px; 132 | margin:1px; 133 | background-color:#DFDFDF; 134 | color:black; 135 | } 136 | .doSomethingButton_small:hover { 137 | background-color:yellow; 138 | } 139 | .doSomethingButton_tiny { 140 | font-size:9px; 141 | display:inline-block; 142 | border:1px solid black; 143 | border-radius:2px; 144 | padding:0px 1px 0px 1px; 145 | margin:0px 1px 0px 1px; 146 | background-color:#DFDFDF; 147 | color:black; 148 | } 149 | .doSomethingButton_tiny:hover { 150 | background-color:yellow; 151 | } 152 | .block_show { 153 | display:block; 154 | } 155 | .block_hide { 156 | display:none; 157 | } 158 | -------------------------------------------------------------------------------- /src/renderer/css/directMessaging.css: -------------------------------------------------------------------------------- 1 | .directMessageContainer { 2 | margin-bottom:10px; 3 | width:100%; 4 | } 5 | .directMessageContainerFloatLeft { 6 | float:left; 7 | text-align:left; 8 | } 9 | .directMessageContainerFloatRight { 10 | float:right; 11 | text-align:right; 12 | } 13 | .directMessageContentContainer { 14 | display:inline-block; 15 | font-size:16px; 16 | max-width:80%; 17 | background-color:white; 18 | border-radius:5px; 19 | margin-left:5px; 20 | border:1px solid grey; 21 | padding:5px 10px 5px 10px; 22 | color:black; 23 | } 24 | .directMessageTimeContainer { 25 | display:inline-block; 26 | } 27 | 28 | /* Tooltip container */ 29 | .directMessageTooltip { 30 | position: relative; 31 | } 32 | 33 | /* Tooltip text */ 34 | .directMessageTooltip .directMessageTooltipText { 35 | visibility: hidden; 36 | background-color: black; 37 | color: #fff; 38 | text-align: center; 39 | padding: 5px 10px 5px 10px; 40 | border-radius: 6px; 41 | 42 | /* Position the tooltip text - see examples below! */ 43 | position: absolute; 44 | top:0px; 45 | z-index: 1; 46 | } 47 | 48 | /* Show the tooltip text when you mouse over the tooltip container */ 49 | .directMessageTooltip:hover .directMessageTooltipText { 50 | visibility: visible; 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/css/editMyProfile.css: -------------------------------------------------------------------------------- 1 | .editProfileFieldContainer { 2 | 3 | } 4 | .editProfileLeftColContainer { 5 | display:inline-block; 6 | width:200px; 7 | text-align:right; 8 | } 9 | .editProfileRightColContainer { 10 | display:inline-block; 11 | width:400px; 12 | margin-left:10px; 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/css/feed.css: -------------------------------------------------------------------------------- 1 | .mainFeedContainer { 2 | margin:0px 5px 0px 5px; 3 | } 4 | .eventContainer { 5 | margin-bottom:10px; 6 | } 7 | .smallAvatarContainer { 8 | display:inline-block; 9 | position:relative; 10 | width:50px; 11 | height:50px; 12 | } 13 | .smallAvatarBox { 14 | display:inline-block; 15 | background-color:white; 16 | border:1px solid black; 17 | border-radius:250px; 18 | width:95%; 19 | height:95%; 20 | margin: 0; 21 | position: absolute; 22 | top: 50%; 23 | left: 50%; 24 | transform: translate(-50%, -50%); 25 | } 26 | .smallAvatarBox_show { 27 | display:inline-block; 28 | background-color:white; 29 | border:1px solid black; 30 | border-radius:250px; 31 | width:95%; 32 | height:95%; 33 | margin: 0; 34 | position: absolute; 35 | top: 50%; 36 | left: 50%; 37 | transform: translate(-50%, -50%); 38 | } 39 | .smallAvatarBox_hide { 40 | display:none; 41 | background-color:white; 42 | border:1px solid black; 43 | border-radius:250px; 44 | width:95%; 45 | height:95%; 46 | margin: 0; 47 | position: absolute; 48 | top: 50%; 49 | left: 50%; 50 | transform: translate(-50%, -50%); 51 | } 52 | .eventMainBodyContainer { 53 | display:inline-block; 54 | width:calc(100% - 70px); 55 | min-height:50px; 56 | background-color:white; 57 | border-radius:10px; 58 | margin-left:5px; 59 | border:1px solid grey; 60 | padding:3px; 61 | } 62 | .eventNameAndTimeContainer { 63 | height:20px; 64 | padding:2px; 65 | font-size:12px; 66 | padding:2px; 67 | } 68 | .eventNameContainer { 69 | display:inline-block; 70 | color:black; 71 | margin-right:10px; 72 | max-width:80%; 73 | overflow:scroll; 74 | padding-left:3px; 75 | } 76 | .eventNameContainer:hover { 77 | border-radius:5px; 78 | padding-left:3px; 79 | background-color:grey; 80 | color:white; 81 | } 82 | .eventTimeContainer { 83 | display:inline-block; 84 | color:grey; 85 | float:right; 86 | } 87 | .eventContentContainer { 88 | padding:5px; 89 | text-decoration:none; 90 | font-size:16px; 91 | color:black; 92 | display:block; 93 | border-radius:5px; 94 | } 95 | .eventContentContainer:hover { 96 | background-color:grey; 97 | } 98 | .nameKnown { 99 | color:black; 100 | } 101 | .nameUnknown { 102 | color:grey; 103 | } 104 | .mainFeedTypeSelector { 105 | margin-bottom:30px; 106 | position:absolute; 107 | right:5px; 108 | } 109 | .numFollowingContainer_show { 110 | display:inline-block; 111 | margin-right:10px; 112 | } 113 | .numFollowingContainer_hide { 114 | display:none; 115 | margin-right:10px; 116 | } 117 | .numFFollowingContainer_show { 118 | display:inline-block; 119 | margin-right:10px; 120 | } 121 | .numFFollowingContainer_hide { 122 | display:none; 123 | margin-right:10px; 124 | } 125 | .eventActionButtonsContainer { 126 | padding:2px; 127 | } 128 | .singleActionButtonContainer { 129 | margin:0px 10px 0px 10px; 130 | padding:2px 10px 2px 10px; 131 | border-radius:5px; 132 | display:inline-block; 133 | } 134 | .singleActionButtonContainer:hover { 135 | background-color:grey; 136 | } 137 | .rootEventContainer { 138 | border-top:20px; 139 | border-bottom:20px; 140 | border-color:brown; 141 | border-style: solid; 142 | background-color:brown; 143 | border-radius:10px; 144 | } 145 | .expandedEventContainer { 146 | border-top:20px; 147 | border-bottom:20px; 148 | border-color:green; 149 | border-style: solid; 150 | background-color:green; 151 | border-radius:10px; 152 | } 153 | .replyContainer_show { 154 | border-radius:10px; 155 | margin-left:55px; 156 | margin-right:15px; 157 | margin-top:5px; 158 | vertical-align:bottom; 159 | } 160 | .replyContainer_hide { 161 | display:none; 162 | } 163 | -------------------------------------------------------------------------------- /src/renderer/css/follows.css: -------------------------------------------------------------------------------- 1 | .followsNavLink { 2 | display:inline-block; 3 | text-decoration:none; 4 | } 5 | .followButton { 6 | border:1px solid black; 7 | border-radius:10px; 8 | font-size:12px; 9 | display:inline-block; 10 | padding:4px; 11 | margin:4px; 12 | background-color:#DFDFDF; 13 | color:black; 14 | } 15 | .unfollowButton { 16 | border:1px solid black; 17 | border-radius:10px; 18 | font-size:12px; 19 | display:inline-block; 20 | padding:4px; 21 | margin:4px; 22 | background-color:yellow; 23 | color:black; 24 | } 25 | .followButton:hover { 26 | background-color:yellow; 27 | } 28 | .unfollowButton:hover { 29 | background-color:#DFDFDF; 30 | } 31 | .followCountContainer { 32 | margin-top:10px; 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/css/grapevine.css: -------------------------------------------------------------------------------- 1 | .userProfileGrapevineContainer { 2 | border-radius:10px; 3 | background-color:#EFEFEF; 4 | } 5 | .userProfileGrapevineContainer_hidden { 6 | display:none; 7 | } 8 | .leaveRatingButton { 9 | font-size:12px; 10 | display:inline-block; 11 | border:1px solid black; 12 | border-radius:5px; 13 | padding:4px; 14 | margin-left:10px; 15 | background-color:#DFDFDF; 16 | color:black; 17 | } 18 | .leaveRatingButton:hover { 19 | background-color:yellow; 20 | } 21 | .leaveRatingButton_hidden { 22 | display:none; 23 | } 24 | .grapevineSelector { 25 | margin-left:10px; 26 | position:absolute; 27 | right:50px; 28 | } 29 | .grapevineSelector_hidden { 30 | display:none; 31 | } 32 | .grapevineContainerLeftCol { 33 | display:inline-block; 34 | width:120px; 35 | padding-top:5px; 36 | position:relative; 37 | font-size:14px; 38 | text-align:left; 39 | color:purple; 40 | } 41 | .grapevineContainerLeftCol:hover { 42 | background-color:yellow; 43 | } 44 | 45 | /* Tooltip text */ 46 | .grapevineContainerLeftCol .tooltiptext { 47 | display: none; 48 | color: #fff; 49 | background-color:purple; 50 | border:1px solid black; 51 | padding: 5px; 52 | border-radius: 6px; 53 | position: absolute; 54 | z-index: 2; 55 | top:-5px; 56 | left:110px; 57 | font-size:16px; 58 | width:350px; 59 | } 60 | 61 | /* Show the tooltip text when you mouse over the tooltip container */ 62 | .grapevineContainerLeftCol:hover .tooltiptext { 63 | display: inline-block; 64 | } 65 | .grapevineItemContainer { 66 | margin-bottom:5px; 67 | position:relative; 68 | } 69 | .grapevineItemContainer_hidden { 70 | margin-bottom:5px; 71 | position:relative; 72 | display:none; 73 | } 74 | -------------------------------------------------------------------------------- /src/renderer/css/grapevineSettings.css: -------------------------------------------------------------------------------- 1 | .grapevineSettingsItemContainer { 2 | margin-left:100px; 3 | margin-bottom:20px; 4 | } 5 | .grapevineSettingsItemLeftCol { 6 | display:inline-block; 7 | width:150px; 8 | font-size:22px; 9 | padding-top:5px; 10 | 11 | text-align:left; 12 | } 13 | .grapevineSettingsItemMainToggleCol { 14 | display:inline-block; 15 | width:150px; 16 | margin-left:10px; 17 | text-align:left; 18 | } 19 | .grapevineSettingsItemRatingNameCol { 20 | display:inline-block; 21 | width:90px; 22 | margin-left:10px; 23 | text-align:left; 24 | font-size:16px; 25 | } 26 | .grapevineSettingsItemRatingToggleButtonCol { 27 | display:inline-block; 28 | width:100px; 29 | margin-left:10px; 30 | text-align:left; 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/css/grapevineToggleSwitch.css: -------------------------------------------------------------------------------- 1 | .grapevineSettingsToggleSwitchContainer { 2 | text-align: center; 3 | display:inline-block; 4 | } 5 | .toggle-switch { 6 | position: relative; 7 | width: 75px; 8 | display: inline-block; 9 | text-align: left; 10 | top: 0px; 11 | } 12 | .checkbox { 13 | display: none; 14 | } 15 | .label { 16 | display: block; 17 | overflow: hidden; 18 | cursor: pointer; 19 | border: 0 solid #bbb; 20 | border-radius: 20px; 21 | } 22 | .inner { 23 | display: block; 24 | width: 200%; 25 | margin-left: -100%; 26 | transition: margin 0.3s ease-in 0s; 27 | } 28 | .inner:before, 29 | .inner:after { 30 | float: left; 31 | width: 50%; 32 | height: 34px; 33 | padding: 0; 34 | line-height: 36px; 35 | color: #fff; 36 | font-weight: bold; 37 | box-sizing: border-box; 38 | } 39 | .inner:before { 40 | content: "ON"; 41 | padding-left: 10px; 42 | background-color: #060; 43 | color: #fff; 44 | } 45 | .inner:after { 46 | content: "OFF"; 47 | padding-right: 10px; 48 | background-color: #bbb; 49 | color: #fff; 50 | text-align: right; 51 | } 52 | .switch { 53 | display: block; 54 | width: 26px; 55 | height: 26px; 56 | margin: 4px; 57 | background: #fff; 58 | position: absolute; 59 | top: 0; 60 | bottom: 0; 61 | right: 40px; 62 | border: 0 solid #bbb; 63 | border-radius: 20px; 64 | transition: all 0.3s ease-in 0s; 65 | } 66 | .checkbox:checked + .label .inner { 67 | margin-left: 0; 68 | } 69 | .checkbox:checked + .label .switch { 70 | right: 0px; 71 | } 72 | -------------------------------------------------------------------------------- /src/renderer/css/mastheads.css: -------------------------------------------------------------------------------- 1 | .mastheadContainer { 2 | overflow:hidden; 3 | position:relative; 4 | height:60px; 5 | } 6 | .mastheadLeftContainer { 7 | font-size:28px; 8 | color:#1B2631; 9 | position: absolute; 10 | top: 50%; 11 | left: 5px; 12 | transform: translate(0%, -50%); 13 | } 14 | .mastheadCenterContainer { 15 | font-size:18px; 16 | text-align:center; 17 | display:inline-block; 18 | margin-top:0px; 19 | margin-bottom:0px; 20 | color:#1B2631; 21 | position: absolute; 22 | top: 50%; 23 | left: 50%; 24 | transform: translate(-50%, -50%); 25 | } 26 | .mastheadRightContainer { 27 | font-size:32px; 28 | position: absolute; 29 | top: 50%; 30 | right: 5px; 31 | transform: translate(0%, -50%); 32 | color:purple; 33 | } 34 | .mastheadEmojiNavButton { 35 | width:50px; 36 | height:50px; 37 | margin-top:-20px; 38 | display:inline-block; 39 | color:#3f3f3f; 40 | margin-left:5px; 41 | font-size:56px; 42 | vertical-align:middle; 43 | text-align:center; 44 | text-decoration:none; 45 | } 46 | .mastheadEmojiNavButton:hover { 47 | color:orange; 48 | } 49 | .mastheadNavButton { 50 | width:35px; 51 | height:35px; 52 | text-align:center; 53 | display:inline-block; 54 | color:black; 55 | border:0px solid black; 56 | background-color:#EFEFEF; 57 | border-radius:3px; 58 | padding:5px; 59 | margin-left:10px; 60 | font-size:10px; 61 | vertical-align:middle; 62 | text-decoration:none; 63 | } 64 | .mastheadNavButton:hover { 65 | color:white; 66 | background-color:#1B2631; 67 | } 68 | 69 | .mastheadSubBanner { 70 | border-top:2px solid #1B2631; 71 | border-bottom:2px solid #1B2631; 72 | text-align:center; 73 | font-size:12px; 74 | color:grey; 75 | } 76 | 77 | .mastheadAvatarContainer { 78 | display:inline-block; 79 | position:relative; 80 | width:50px; 81 | height:50px; 82 | } 83 | .mastheadAvatarBox { 84 | display:inline-block; 85 | background-color:white; 86 | border:1px solid black; 87 | border-radius:250px; 88 | width:95%; 89 | height:95%; 90 | margin: 0; 91 | position: absolute; 92 | top: 50%; 93 | left: 50%; 94 | transform: translate(-50%, -50%); 95 | } 96 | -------------------------------------------------------------------------------- /src/renderer/css/misc.css: -------------------------------------------------------------------------------- 1 | .goToUserProfileButton { 2 | text-decoration:none; 3 | } 4 | table#fetchingtable > tr > td.data { 5 | width:80px; 6 | padding:3px; 7 | border:1px solid black; 8 | } 9 | table#fetchingtable > tr > td { 10 | width:60px; 11 | padding:3px; 12 | } 13 | #connectedRelaysButton { 14 | display:inline-block; 15 | } 16 | #connectedRelaysButton:hover { 17 | background-color:yellow; 18 | cursor:pointer; 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/css/myProfile.css: -------------------------------------------------------------------------------- 1 | .myProfileBox { 2 | position:relative; 3 | } 4 | .myProfileRightColumnContainer { 5 | display:inline-block; 6 | width:400px; 7 | height:200px; 8 | margin-left:10px; 9 | } 10 | 11 | .myProfileNameEditContainer { 12 | width:300px; 13 | height:50px; 14 | display:inline-block; 15 | } 16 | .myProfileNameContainer { 17 | font-size:18px; 18 | width:100%; 19 | height:50px; 20 | } 21 | .myProfileAboutContainer { 22 | width:100%; 23 | height:70px; 24 | } 25 | .myProfileAvatarContainer { 26 | display:inline-block; 27 | position:relative; 28 | width:250px; 29 | height:250px; 30 | } 31 | .myProfileAvatarImg { 32 | display:inline-block; 33 | background-color:blue; 34 | border:5px solid black; 35 | border-radius:250px; 36 | width:95%; 37 | height:95%; 38 | margin: 0; 39 | position: absolute; 40 | top: 50%; 41 | left: 50%; 42 | transform: translate(-50%, -50%); 43 | } 44 | .radioButtonDescriptor { 45 | margin-left:10px; 46 | font-size:18px; 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/css/navbars.css: -------------------------------------------------------------------------------- 1 | .leftNavButton { 2 | width:80px; 3 | height:80px; 4 | border:1px solid #5f5f5f; 5 | display:inline-block; 6 | color:black; 7 | background-color:#EFEFEF; 8 | margin:0px 0px 5px 0px; 9 | border-radius:3px; 10 | padding:5px; 11 | font-size:12px; 12 | text-decoration:none; 13 | } 14 | .leftNavButton:hover { 15 | color:white; 16 | background-color:#1B2631; 17 | } 18 | .leftNavPanel { 19 | padding-top:5px; 20 | display:inline-block; 21 | background-color: #616A6B; 22 | height:100%; 23 | width:100px; 24 | vertical-align:top; 25 | text-align:center; 26 | white-space: normal; 27 | } 28 | 29 | .leftNav2Button { 30 | width:40px; 31 | height:40px; 32 | border:1px solid #5f5f5f; 33 | display:inline-block; 34 | color:black; 35 | background-color:#EFEFEF; 36 | margin:0px 0px 2px 0px; 37 | border-radius:2px; 38 | padding:2px; 39 | font-size:10px; 40 | text-decoration:none; 41 | } 42 | .leftNav2Button:hover { 43 | color:white; 44 | background-color:#1B2631; 45 | } 46 | .leftNav2Panel { 47 | padding-top:5px; 48 | display:inline-block; 49 | background-color: #BDC3C7; 50 | height:100%; 51 | width:52px; 52 | vertical-align:top; 53 | text-align:center; 54 | white-space: normal; 55 | } 56 | -------------------------------------------------------------------------------- /src/renderer/css/newPost.css: -------------------------------------------------------------------------------- 1 | .newPostTextarea { 2 | width:100%; 3 | height:200px 4 | } 5 | -------------------------------------------------------------------------------- /src/renderer/css/nfgGraphic.css: -------------------------------------------------------------------------------- 1 | .nfgGraphicContainer { 2 | border:1px dashed grey; 3 | display:inline-block; 4 | width:700px; 5 | height:500px; 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/css/settings.css: -------------------------------------------------------------------------------- 1 | div.relays:before { 2 | content: attr(data-currentstate); 3 | } 4 | .singleProfileDataContainer { 5 | width:100%; 6 | margin-bottom:5px; 7 | position:relative; 8 | } 9 | .singleProfileDataContainer_inactive { 10 | border:1px dashed grey; 11 | padding:3px; 12 | background-color:#EFEFEF; 13 | } 14 | .singleProfileDataContainer_active { 15 | border:2px solid purple; 16 | padding:2px; 17 | background-color:white; 18 | } 19 | .relayInfoContainer { 20 | margin-bottom:2px; 21 | font-size:18px; 22 | padding:2px; 23 | } 24 | .relayUrlContainer { 25 | display:inline-block; 26 | margin-left:5px; 27 | width:400px; 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/css/toggleSwitch1.css: -------------------------------------------------------------------------------- 1 | /* The switch - the box around the slider */ 2 | .switch { 3 | position: relative; 4 | display: inline-block; 5 | width: 60px; 6 | height: 34px; 7 | } 8 | 9 | /* Hide default HTML checkbox */ 10 | .switch input { 11 | opacity: 0; 12 | width: 0; 13 | height: 0; 14 | } 15 | 16 | /* The slider */ 17 | .slider { 18 | position: absolute; 19 | cursor: pointer; 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | bottom: 0; 24 | background-color: #ccc; 25 | -webkit-transition: .4s; 26 | transition: .4s; 27 | } 28 | 29 | .slider:before { 30 | position: absolute; 31 | content: ""; 32 | height: 26px; 33 | width: 26px; 34 | left: 4px; 35 | bottom: 4px; 36 | background-color: white; 37 | -webkit-transition: .4s; 38 | transition: .4s; 39 | } 40 | 41 | input:checked + .slider { 42 | background-color: #2196F3; 43 | } 44 | 45 | input:focus + .slider { 46 | box-shadow: 0 0 1px #2196F3; 47 | } 48 | 49 | input:checked + .slider:before { 50 | -webkit-transform: translateX(26px); 51 | -ms-transform: translateX(26px); 52 | transform: translateX(26px); 53 | } 54 | 55 | /* Rounded sliders */ 56 | .slider.round { 57 | border-radius: 34px; 58 | } 59 | 60 | .slider.round:before { 61 | border-radius: 50%; 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/css/userList.css: -------------------------------------------------------------------------------- 1 | .singleUserContainer { 2 | border:1px solid black; 3 | border-radius:5px; 4 | margin-bottom:5px; 5 | padding:5px; 6 | position:relative; 7 | background-color:#c6c6ff; 8 | } 9 | .userListSmallAvatarContainer { 10 | display:inline-block; 11 | position:relative; 12 | width:50px; 13 | height:50px; 14 | } 15 | .userListSmallAvatarBox { 16 | display:inline-block; 17 | background-color:white; 18 | border:1px solid black; 19 | border-radius:250px; 20 | width:95%; 21 | height:95%; 22 | margin: 0; 23 | position: absolute; 24 | top: 50%; 25 | left: 50%; 26 | transform: translate(-50%, -50%); 27 | } 28 | .singleUserMainBodyContainer { 29 | display:inline-block; 30 | width:calc(86% - 70px); 31 | min-height:50px; 32 | background-color:white; 33 | border-radius:5px; 34 | margin-left:5px; 35 | } 36 | .singleUserRightContainer { 37 | display:inline-block; 38 | width:15%; 39 | min-height:50px; 40 | border-radius:5px; 41 | margin-left:5px; 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/css/userProfile.css: -------------------------------------------------------------------------------- 1 | .mainUserProfileBox { 2 | width:100%; 3 | margin-bottom:0px; 4 | } 5 | .largeAvatarContainer { 6 | display:inline-block; 7 | position:relative; 8 | width:250px; 9 | height:250px; 10 | } 11 | .mainProfilePageAvatarBox { 12 | display:inline-block; 13 | background-color:blue; 14 | border:5px solid black; 15 | border-radius:250px; 16 | width:95%; 17 | height:95%; 18 | margin: 0; 19 | position: absolute; 20 | top: 50%; 21 | left: 50%; 22 | transform: translate(-50%, -50%); 23 | } 24 | .mainUserProfileRightColumnContainer { 25 | display:inline-block; 26 | margin-left:10px; 27 | position:relative; 28 | } 29 | .mainUserProfileLeftColumnContainer { 30 | display:inline-block; 31 | width:250px; 32 | overflow:scroll; 33 | } 34 | .mainUserAboutContainer { 35 | width:600px; 36 | height:40px; 37 | overflow:scroll; 38 | } 39 | .mainUserNameContainer { 40 | font-size:18px; 41 | width:100%; 42 | overflow:scroll; 43 | margin-bottom:5px; 44 | } 45 | .userProfilePubkeyContainer { 46 | font-size:10px; 47 | color:brown; 48 | margin-bottom:5px; 49 | } 50 | -------------------------------------------------------------------------------- /src/renderer/css/youTubeEmbed.css: -------------------------------------------------------------------------------- 1 | .video-responsive { 2 | overflow: hidden; 3 | padding-bottom: 56.25%; 4 | position: relative; 5 | height: 0; 6 | } 7 | 8 | .video-responsive iframe { 9 | left: 0; 10 | top: 0; 11 | height: 100%; 12 | width: 100%; 13 | position: absolute; 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/errorBoundary.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class ErrorBoundary extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false }; 7 | } 8 | 9 | static getDerivedStateFromError(error) { 10 | // Update state so the next render will show the fallback UI. 11 | return { hasError: true }; 12 | } 13 | 14 | componentDidCatch(error, errorInfo) { 15 | // You can also log the error to an error reporting service 16 | logErrorToMyService(error, errorInfo); 17 | } 18 | 19 | render() { 20 | if (this.state.hasError) { 21 | // You can render any custom fallback UI 22 | return

Something went wrong.

; 23 | } 24 | 25 | return this.props.children; 26 | } 27 | } 28 | export default ErrorBoundary 29 | -------------------------------------------------------------------------------- /src/renderer/grapevine/landingPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { NavLink } from "react-router-dom"; 4 | import Masthead from '../../mastheads/mainMasthead.js'; 5 | import LeftNavbar from '../../navbars/leftNav.js'; 6 | import * as MiscAppFxns from "../../lib/app/misc.ts"; 7 | import * as StartupFxns from "../../lib/app/startup.ts"; 8 | 9 | const jQuery = require("jquery"); 10 | const updateMainColWidth = MiscAppFxns.updateMainColWidth; 11 | const cloneObj = MiscAppFxns.cloneObj 12 | const secsToTime = MiscAppFxns.secsToTime 13 | const timeout = MiscAppFxns.timeout 14 | 15 | export default class GrapevineLandingPage extends React.Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | events: [] 21 | } 22 | } 23 | 24 | async componentDidMount() { 25 | updateMainColWidth(); 26 | document.getElementById("mastheadCenterContainer").innerHTML = "grapevine landing page" 27 | 28 | await timeout(10) 29 | jQuery("#goToMainGrapevinePageButton").get(0).click() 30 | } 31 | render() { 32 | return ( 33 | <> 34 | 37 |
38 |
39 | 40 |
41 |
42 | 43 |
go to main grapevine page
44 |
45 | 46 |
47 |
48 | 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/grapevine/nostrFollowGrapevine/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import Masthead from '../../mastheads/mainMasthead.js'; 3 | import LeftNavbar from '../../navbars/leftNav.js'; 4 | import * as MiscAppFxns from "../../lib/app/misc.ts"; 5 | import NFG_Graphic1 from "./graphic"; 6 | import { 7 | relayInit, 8 | generatePrivateKey, 9 | getPublicKey, 10 | getEventHash, 11 | signEvent, 12 | validateEvent, 13 | verifySignature, 14 | } from 'nostr-tools' 15 | 16 | import { asyncSql } from "../../index.tsx"; 17 | 18 | const jQuery = require("jquery"); 19 | 20 | const updateMainColWidth = MiscAppFxns.updateMainColWidth; 21 | 22 | const fetchFollowingNetworkInfo = async () => { 23 | var sql = "" 24 | sql += " SELECT * FROM followingNetwork WHERE id=1 " 25 | var aFollowingNetworkData = await asyncSql(sql); 26 | console.log("aFollowingNetworkData.length: "+aFollowingNetworkData.length) 27 | if (aFollowingNetworkData.length > 0) { 28 | const oFollowingNetworkData = aFollowingNetworkData[0] 29 | console.log("oFollowingNetworkData.id: "+oFollowingNetworkData.id) 30 | return oFollowingNetworkData; 31 | } else { 32 | return false; 33 | } 34 | } 35 | 36 | var numUpdates = 0; 37 | export default class ExtendedFollowerList extends React.Component { 38 | constructor(props) { 39 | super(props); 40 | this.state = { 41 | seed: window.myPubkey, 42 | pubkeys: {} 43 | } 44 | } 45 | async componentDidMount() { 46 | updateMainColWidth(); 47 | document.getElementById("mastheadCenterContainer").innerHTML = "visualization of the social graph
based on following" 48 | 49 | var oFollowingNetworkData = await fetchFollowingNetworkInfo() 50 | if (oFollowingNetworkData) { 51 | this.setState({ 52 | seed: oFollowingNetworkData.seed, 53 | pubkeys: JSON.parse(oFollowingNetworkData.pubkeys) 54 | }) 55 | } else { 56 | console.log("You need to initialize your first followingNetwork!") 57 | } 58 | 59 | } 60 | render() { 61 | return ( 62 | <> 63 | 66 |
67 |
68 | 69 |
70 |
71 |
72 | 76 |
77 |
78 |
79 |
0
80 |
of {Object.keys(this.state.pubkeys).length}
81 | profiles loaded 82 |
83 |
84 |
85 |
86 |
87 | 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/renderer/grapevine/nostrFollowGrapevine/missingAvatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/src/renderer/grapevine/nostrFollowGrapevine/missingAvatar.png -------------------------------------------------------------------------------- /src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Hello sovereign individual! 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import App from './App'; 3 | // import { createStore } from 'redux' 4 | // import { Provider } from 'react-redux' 5 | // import store from './store' 6 | 7 | // const store = createStore(rootReducer) 8 | 9 | // wrap this in a function and don't call it until ipc-fetch-relays returns with data from sqlite3 10 | const startApp = () => { 11 | const container = document.getElementById('root')!; 12 | const root = createRoot(container); 13 | root.render(); 14 | }; 15 | 16 | window.electron.ipcRenderer.once('ipc-fetch-relays', (relayUrls) => { 17 | console.log("ipc-fetch-relays; relayUrls: "+relayUrls); 18 | window.relayUrls = relayUrls; 19 | startApp() 20 | }); 21 | window.electron.ipcRenderer.sendMessage('ipc-fetch-relays', ['ping']); 22 | 23 | // calling IPC exposed from preload script 24 | window.electron.ipcRenderer.once('ipc-example', (arg) => { 25 | // eslint-disable-next-line no-console 26 | console.log(arg); 27 | }); 28 | window.electron.ipcRenderer.sendMessage('ipc-example', ['ping']); 29 | 30 | /* 31 | We will use asyncSql(sql) in src/renderer/pages/sqlDemoApp to send sql commmands and replies back and forth 32 | between the main process and the renderer process. 33 | */ 34 | export const asyncSql = async (sql) => { 35 | const nonce = Math.floor(( Math.random() * 100000 )) 36 | console.log("asyncSql; nonce: "+nonce) 37 | return new Promise((resolve) => { 38 | window.electron.ipcRenderer.once('asynchronous-sql-reply-'+nonce, (arg) => { 39 | resolve(arg) 40 | }); 41 | const data = [ sql, nonce ] 42 | window.electron.ipcRenderer.sendMessage('asynchronous-sql-command', data); 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/lib/app/misc.ts: -------------------------------------------------------------------------------- 1 | 2 | export const updateMainColWidth = () => { 3 | const menuColWidth = document.getElementById("menuCol").offsetWidth; 4 | const rootWidth = document.getElementById("root").offsetWidth; 5 | const newWidth = rootWidth - menuColWidth - 4; 6 | document.getElementById("mainCol").style.width = newWidth+"px"; 7 | 8 | const mastheadHeight = document.getElementById("mastheadElem").offsetHeight; 9 | const rootHeight = document.getElementById("root").offsetHeight; 10 | const newHeight = rootHeight - mastheadHeight - 6; 11 | document.getElementById("mainPanel").style.height = newHeight+"px"; 12 | } 13 | 14 | export const isValidObj = (x) => { 15 | // test if the input is a string that converts into JSON 16 | try { 17 | const obj1 = JSON.parse(x); 18 | return true; 19 | } catch (e1) {} 20 | 21 | // test if the input is already an object 22 | try { 23 | const obj2 = JSON.parse(JSON.stringify(x)); 24 | return true; 25 | } catch (e2) {} 26 | 27 | return false; 28 | } 29 | 30 | export const cloneObj = (obj) => { 31 | return JSON.parse(JSON.stringify(obj)); 32 | } 33 | 34 | export const secsToTime = (secs) => { 35 | var displayTime = "--"; 36 | var currentTime = Math.floor(Date.now() / 1000); 37 | var age_secs = currentTime - secs; 38 | var age_mins = Math.floor(age_secs / 60); 39 | var age_hours = Math.floor(age_secs / (60 * 60) ); 40 | var age_days = Math.floor(age_secs / (60 * 60 * 24) ); 41 | var age_years = Math.floor(age_secs / (60 * 60 * 24 * 365) ); 42 | var discoveredUnit = false; 43 | if ( (!discoveredUnit) && (age_secs < 60) ) { 44 | // if less than one minute, 45 | // display in seconds 46 | displayTime = age_secs + " s"; 47 | discoveredUnit = true; 48 | } 49 | if ( (!discoveredUnit) && (age_secs < 60 * 60) ) { 50 | // if less than one hour, 51 | // display in minutes 52 | displayTime = age_mins + " m"; 53 | discoveredUnit = true; 54 | } 55 | if ( (!discoveredUnit) && (age_secs < 24 * 60 * 60) ) { 56 | // if less than one day, 57 | // display in hours 58 | displayTime = age_hours + " h"; 59 | discoveredUnit = true; 60 | } 61 | if ( (!discoveredUnit) && (age_secs < 365 * 24 * 60 * 60) ) { 62 | // if less than one year, 63 | // display in days 64 | displayTime = age_days + " d"; 65 | discoveredUnit = true; 66 | } 67 | if (!discoveredUnit) { 68 | // else display in years 69 | displayTime = age_years + " y"; 70 | discoveredUnit = true; 71 | } 72 | 73 | return displayTime; 74 | } 75 | 76 | export const timeout = (ms) => { 77 | return new Promise(resolve => setTimeout(resolve, ms)); 78 | } 79 | 80 | export const fetchMySk = async () => { 81 | var sql = "" 82 | sql += "SELECT * FROM myProfile WHERE active=true" 83 | var aMyProfileData = await asyncSql(sql); 84 | 85 | if (aMyProfileData.length > 0) { 86 | var oMyProfileData = aMyProfileData[0] 87 | 88 | var myPk = oMyProfileData.pubkey; 89 | var mySk = oMyProfileData.privkey; 90 | return mySk 91 | } 92 | return false; 93 | } 94 | 95 | export const fetchMyPk = async () => { 96 | var sql = "" 97 | sql += "SELECT * FROM myProfile WHERE active=true" 98 | var aMyProfileData = await asyncSql(sql); 99 | 100 | if (aMyProfileData.length > 0) { 101 | var oMyProfileData = aMyProfileData[0] 102 | 103 | var myPk = oMyProfileData.pubkey; 104 | var mySk = oMyProfileData.privkey; 105 | return myPk 106 | } 107 | return false; 108 | } 109 | 110 | export const fetchRelays = async (which) => { 111 | var sql = "" 112 | sql += "SELECT * FROM relays " 113 | var aRelaysData = await asyncSql(sql); 114 | 115 | var aDefault = []; 116 | var aActive = []; 117 | var aAll = []; 118 | console.log("aRelaysData.length: "+aRelaysData.length) 119 | for (var r=0;r { 5 | const [count, setCount] = useState(0); 6 | 7 | useEffect(() => { 8 | setTimeout(() => { 9 | var foo = jQuery("#scoresCalculationTimer").data("status") 10 | // console.log("ScoresCalculationTimer; count: "+count+"; foo: "+foo) 11 | if (foo=="run") { 12 | jQuery("#calculateScoresSingleIterationButton").get(0).click() 13 | if (count%5 == 0) { 14 | jQuery("#changeUserCalcsDisplayButton").get(0).click() 15 | } 16 | } 17 | setCount((count) => count + 1); 18 | }, 200); 19 | }, [count]); // <- add empty brackets here 20 | 21 | return
I've rendered {count} times!
; 22 | } 23 | export default Timer; 24 | -------------------------------------------------------------------------------- /src/renderer/lib/app/toggleSwitch1.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class ToggleButtonOnOff extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.handleClick = this.handleClick.bind(this); 7 | this.state = {isOff: false}; 8 | } 9 | 10 | handleClick() { 11 | this.setState({isOff:!this.state.isOff}); 12 | } 13 | 14 | render() { 15 | const { isOff } = this.state; 16 | let title=this.state.isOff? "ON":"OFF"; 17 | return ( 18 | 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/lib/nostr/eventValidation.js: -------------------------------------------------------------------------------- 1 | import * as MiscAppFxns from "../app/misc"; 2 | import { 3 | validateEvent, 4 | verifySignature, 5 | } from "nostr-tools"; 6 | 7 | const isValidObj = MiscAppFxns.isValidObj 8 | 9 | export const doesEventValidate = (event) => { 10 | let ok = false; 11 | let veryOk = false; 12 | if (isValidObj(event)) { 13 | ok = validateEvent(event) 14 | veryOk = verifySignature(event) 15 | } 16 | if ((ok) && (veryOk)) { 17 | return true; 18 | } 19 | return false; 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/lib/nostr/getFollowing.js: -------------------------------------------------------------------------------- 1 | import { 2 | relayInit 3 | } from 'nostr-tools' 4 | import * as MiscAppFxns from "../../lib/app/misc.ts"; 5 | 6 | const timeout = MiscAppFxns.timeout 7 | const relay = relayInit('wss://nostr-pub.wellorder.net') 8 | 9 | // ms is timeout in milliseconds 10 | export const getFollowing = async (pk, ms) => { 11 | await timeout(4000) 12 | 13 | 14 | var aFollowing = []; 15 | 16 | await relay.connect() 17 | 18 | await relay.on('connect', async () => { 19 | console.log(`getFollowing connected to ${relay.url}`) 20 | }) 21 | relay.on('error', () => { 22 | console.log(`getFollowing failed to connect to ${relay.url}`) 23 | }) 24 | 25 | let sub = relay.sub([ 26 | { 27 | authors: [ pk ], 28 | since: 0, 29 | kinds: [3], 30 | } 31 | ]) 32 | await sub.on('event', async (event) => { 33 | console.log('getFollowing we got the event we wanted:', event) 34 | const aTags = event.tags; 35 | return await aTags; 36 | // console.log("getFollowing event.tags A: "+JSON.stringify(aTags,null,4)) 37 | sub.unsub(); 38 | // console.log("getFollowing event.tags B: "+JSON.stringify(aTags,null,4)) 39 | }) 40 | sub.on('eose', () => { 41 | sub.unsub() 42 | }) 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/lib/visjs/visjs-style.js: -------------------------------------------------------------------------------- 1 | 2 | export const groupOptions = { 3 | user: { 4 | shape: "circle", 5 | borderWidth: "3", 6 | color: { 7 | background: "white", 8 | border: "black" 9 | } 10 | } 11 | } 12 | 13 | export const edgeOptions = { 14 | follows: { 15 | polarity: "forward", 16 | color: "blue", 17 | width: "5", 18 | dashes: false, 19 | physics: true 20 | } 21 | } 22 | 23 | function editEdgeFunction() { 24 | 25 | } 26 | function deleteEdgeFunction() { 27 | 28 | } 29 | function deleteNodeFunction() { 30 | 31 | } 32 | 33 | export const options = { 34 | clickToUse: false, 35 | layout: { 36 | improvedLayout: false 37 | }, 38 | interaction: {hover: true}, 39 | manipulation: { 40 | enabled: false, 41 | }, 42 | physics: { 43 | enabled: true 44 | }, 45 | nodes:{ 46 | margin: 10, 47 | borderWidth:1, 48 | color: { background: 'white', border: 'black' }, 49 | widthConstraint: { 50 | minimum: 0, 51 | maximum: 100 52 | } 53 | }, 54 | edges: { 55 | physics: false, 56 | arrows: { 57 | to: { 58 | enabled: true, 59 | scaleFactor: 4 60 | } 61 | } 62 | }, 63 | groups: groupOptions 64 | }; 65 | -------------------------------------------------------------------------------- /src/renderer/mastheads/avatarElem.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { useNostrEvents } from "nostr-react"; 3 | import BlankAvatar from "./blankAvatar.png"; 4 | 5 | const jQuery = require("jquery"); 6 | 7 | const AvatarElem2 = (props) => { 8 | 9 | if (window.myProfile) { 10 | if (window.myProfile.picture_url) { 11 | return ( 12 | <> 13 |
14 | 15 |
16 | 17 | ); 18 | } else { 19 | return ( 20 | <> 21 |
22 | 23 |
24 | 25 | ); 26 | } 27 | } else { 28 | var { events } = useNostrEvents({ 29 | filter: { 30 | authors: [ props.pubkey ], 31 | since: 0, // all new events from now 32 | kinds: [0], 33 | }, 34 | }); 35 | return ( 36 | <> 37 | {events.map( (event) => { 38 | var pic_url = JSON.parse(event.content).picture; 39 | return ( 40 | <> 41 |
42 | 43 |
44 | 45 | )} 46 | )} 47 | 48 | ); 49 | } 50 | } 51 | 52 | export default class AvatarElem extends React.Component { 53 | constructor(props) { 54 | super(props); 55 | this.state = { 56 | } 57 | } 58 | async componentDidMount() { 59 | } 60 | 61 | render() { 62 | const pk = this.props.pubkey; 63 | return ( 64 | <> 65 | 66 | 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/renderer/mastheads/blankAvatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/src/renderer/mastheads/blankAvatar.png -------------------------------------------------------------------------------- /src/renderer/mastheads/mainMasthead.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from "react-router-dom"; 3 | import AvatarElem from "./avatarElem"; 4 | import RelaysStatus from "../pages/components/relaysStatus" 5 | 6 | export default class Masthead extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | myPubkey: window.myPubkey 11 | } 12 | } 13 | async componentDidMount() { 14 | } 15 | render() { 16 | return ( 17 | <> 18 |
19 |
20 | 21 | back button 22 | 23 | electron-nostr 24 |
25 | 26 |
27 | Home 28 |
29 | 30 |
31 | 32 | visjs hello world 33 | 34 | 35 |
🍇
36 | settings 37 |
38 | 39 |
🔍
40 |
search
41 |
42 | 43 |
✏️
44 |
post
45 |
46 | 47 | 48 | 49 | 50 |
⚙️
51 |
settings
52 |
53 |
54 |
55 | 56 |
57 | 58 |
59 | 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/navbars/leftNav.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | 4 | export default class LeftNavbar extends React.PureComponent { 5 | render() { 6 | return ( 7 | <> 8 |
9 | 10 |
🏠
11 |
main feed
12 |
13 | 14 | 15 |
🌎
16 |
graph
17 |
18 |
19 | 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/navbars/leftNav2/settings.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | 4 | export default class LeftNavbar2_Settings extends React.PureComponent { 5 | render() { 6 | return ( 7 | <> 8 |
9 | 10 | keys 11 | 12 | 13 | 14 | relays 15 | 16 | 17 | 18 | sql 19 | 20 | 21 | 22 |
🌐
23 |
24 |
25 | 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/pages/components/actionButtons.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { Link, NavLink } from "react-router-dom"; 3 | import * as MiscAppFxns from "../../lib/app/misc.ts"; 4 | 5 | const jQuery = require("jquery"); 6 | 7 | const ActionButtons = ({event}) => { 8 | // var randomNumber = Math.random(); 9 | if (!window.linkToReply_base) { window.linkToReply_base="Reply" } 10 | 11 | var linkToReply = "/"+window.linkToReply_base+"/"+event.id 12 | return ( 13 | <> 14 |
15 | 16 | { 18 | window.expandedEvent = event; 19 | 20 | // doing the same inelegant workaround as I am for linkToThread_base -- see userPost.js 21 | if (window.linkToReply_base=="Reply") { 22 | window.linkToReply_base="Reply2" 23 | } else { 24 | window.linkToReply_base="Reply" 25 | } 26 | } } 27 | className="eventContentContainer" 28 | to={linkToReply} 29 | to_base={window.linkToReply_base} 30 | event={event} 31 | > 32 | 💬 33 | 34 | 35 | 🔁 36 | 🤍 37 | 💚 38 | 🤙 39 |
40 |
41 |             {JSON.stringify(event,null,4)}
42 |             
43 | 44 | ); 45 | } 46 | export default ActionButtons 47 | -------------------------------------------------------------------------------- /src/renderer/pages/components/blankAvatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wds4/electron-react-boilerplate-nostr/e8d90f8b520381e54d80c77a919ea27e839c33ea/src/renderer/pages/components/blankAvatar.png -------------------------------------------------------------------------------- /src/renderer/pages/components/followButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNostr, dateToUnix } from "nostr-react"; 3 | import { asyncSql } from "../../index.tsx"; 4 | import { 5 | type Event as NostrEvent, 6 | getEventHash, 7 | getPublicKey, 8 | signEvent, 9 | validateEvent, 10 | verifySignature, 11 | } from "nostr-tools"; 12 | 13 | import * as MiscAppFxns from "../../lib/app/misc.ts"; 14 | 15 | const cloneObj = MiscAppFxns.cloneObj 16 | 17 | const jQuery = require("jquery"); 18 | 19 | const fetchMyData = async () => { 20 | var sql = "" 21 | sql += "SELECT * FROM myProfile WHERE id=1" 22 | console.log("fetchMyData sql: "+sql) 23 | var aMyProfileData = await asyncSql(sql); 24 | console.log("fetchMyData aMyProfileData: "+JSON.stringify(aMyProfileData,null,4)) 25 | if (aMyProfileData.length > 0) { 26 | var oMyProfileData = aMyProfileData[0] 27 | 28 | var myPk = oMyProfileData.pubkey; 29 | var mySk = oMyProfileData.privkey; 30 | var sMyFollowingList = oMyProfileData.following; 31 | if (!sMyFollowingList) { sMyFollowingList = "[]"; } 32 | var aMyFollowingList = JSON.parse(sMyFollowingList) 33 | return [ mySk, aMyFollowingList ] 34 | } 35 | return false; 36 | } 37 | 38 | const updateMyFollowingInSql = async (aFollowing_new) => { 39 | window.myProfile.following = aFollowing_new 40 | var sFollowing_new = JSON.stringify(aFollowing_new) 41 | var sql = ""; 42 | sql += " UPDATE myProfile SET following = '"+sFollowing_new+"' WHERE id=1 " 43 | var result = await asyncSql(sql); 44 | } 45 | 46 | export default function FollowButton({pubkey,aFollowing}) { 47 | const { publish } = useNostr(); 48 | 49 | var buttonHTML = "follow"; 50 | var buttonClass = "followButton"; 51 | var currentAction = "follow" 52 | // if (window.myProfile.following.includes(pubkey)) { 53 | if (aFollowing.includes(pubkey)) { 54 | buttonHTML = "unfollow"; 55 | buttonClass = "unfollowButton"; 56 | currentAction = "unfollow" 57 | } 58 | 59 | const onPublish = async () => { 60 | const [ privKey, aMyFollowingList_current ] = await fetchMyData(); 61 | 62 | var aMyFollowingList_new = []; 63 | 64 | console.log("currentAction: "+currentAction) 65 | 66 | if (!window.myProfile.following.includes(pubkey)) { 67 | // If this pubkey is not currently in my following list, then add it 68 | for (var t=0;t 132 |
onPublish()} className={buttonClass} data-currentaction={currentAction} > 133 | {buttonHTML} 134 |
135 | 136 | ); 137 | 138 | /* 139 | if (window.myProfile.following.includes(pubkey)) { 140 | return ( 141 | <> 142 |
onPublish()} className={buttonClass} data-currentaction={currentAction} style={{position:"absolute",right:"5px",top:"5px"}} > 143 | {buttonHTML} 144 |
145 | 146 | ); 147 | } else { 148 | return ( 149 | <> 150 |
onPublish()} className={buttonClass} data-currentaction={currentAction} style={{position:"absolute",right:"5px",top:"5px"}} > 151 | {buttonHTML} 152 |
153 | 154 | ); 155 | } 156 | */ 157 | } 158 | -------------------------------------------------------------------------------- /src/renderer/pages/components/relaysStatus.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { useNostr } from "nostr-react"; 3 | const jQuery = require("jquery"); 4 | 5 | const RelaysStatusComponent = () => { 6 | const { updateRelayList, connectedRelays } = useNostr(); 7 | 8 | /* 9 | const addRelayer = (url) => { 10 | updateRelayList([ 11 | ...connectedRelays, 12 | url, 13 | ]) 14 | } 15 | const url = "wss://brb.io"; 16 | addRelayer(url) 17 | */ 18 | 19 | return ( 20 | <> 21 |
22 | connected relays: {connectedRelays.length} 23 |
24 |
25 |
26 | {connectedRelays.map( (oRelay,item)=>{ 27 | var url = oRelay.url; 28 | return ( 29 |
30 |
31 | {JSON.stringify(oRelay,null,4)} 32 |
33 | {item+1}: {url} 34 |
35 | ) 36 | })} 37 |
38 |
39 | 40 | ) 41 | }; 42 | 43 | export default class RelaysStatus extends React.Component { 44 | constructor(props) { 45 | super(props); 46 | this.state = { 47 | } 48 | } 49 | 50 | async componentDidMount() { 51 | jQuery("#connectedRelaysButton").click(function(){ 52 | var currentStatus = jQuery(this).data("status"); 53 | if (currentStatus=="closed") { 54 | jQuery(this).data("status","open"); 55 | // jQuery("#connectedRelaysVerboseContainer").css("overflow","hidden") 56 | jQuery("#connectedRelaysVerboseContainer").animate({ 57 | height:"80px", 58 | },200) 59 | } 60 | if (currentStatus=="open") { 61 | jQuery(this).data("status","closed"); 62 | jQuery("#connectedRelaysVerboseContainer").animate({ 63 | height:"0px", 64 | },200) 65 | // jQuery("#connectedRelaysVerboseContainer").css("overflow","scroll") 66 | } 67 | }) 68 | } 69 | render() { 70 | return ( 71 | <> 72 | 73 | 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/renderer/pages/components/userPosts.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { Link, NavLink } from "react-router-dom"; 3 | import { useNostrEvents, useProfile } from "nostr-react"; 4 | import * as MiscAppFxns from "../../lib/app/misc.ts"; 5 | import UserPost from "./userPost"; 6 | 7 | const UserPosts = ({pubkey}) => { 8 | var { events } = useNostrEvents({ 9 | filter: { 10 | authors: [ pubkey ], 11 | since: 0, // all new events from now 12 | kinds: [1], 13 | }, 14 | }); 15 | const currentPage = "userProfile"; 16 | return ( 17 | <> 18 |
{events.length} posts
19 | {events.map( (event) => { 20 | return ( 21 | <> 22 | 26 | 27 | )} 28 | )} 29 | 30 | ) 31 | }; 32 | 33 | export default UserPosts; 34 | -------------------------------------------------------------------------------- /src/renderer/pages/components/youTubeEmbed.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | // src={`https://www.youtube.com/embed/${embedId}`} 5 | // vs: 6 | // src={`${extractedUrl}`} 7 | const YoutubeEmbed = ({ embedId, extractedUrl }) => { 8 | const testUrl = "https://www.youtube.com/watch?v=ljvpz2fEyVE" 9 | if (embedId) { 10 | return ( 11 |
12 |