├── .gitignore ├── assets ├── logo.png ├── pin.png ├── how-to.png ├── delegate-trust.png ├── delegate-vote.png ├── list-shortages.png ├── list-solutions.png ├── pin-to-local-node.png └── suggest-solution.png ├── makefile ├── .github └── FUNDING.yml ├── LICENSE ├── go.mod ├── README.md ├── go.sum └── cyber-acid.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | web/app.wasm 3 | cyber-acid -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/pin.png -------------------------------------------------------------------------------- /assets/how-to.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/how-to.png -------------------------------------------------------------------------------- /assets/delegate-trust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/delegate-trust.png -------------------------------------------------------------------------------- /assets/delegate-vote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/delegate-vote.png -------------------------------------------------------------------------------- /assets/list-shortages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/list-shortages.png -------------------------------------------------------------------------------- /assets/list-solutions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/list-solutions.png -------------------------------------------------------------------------------- /assets/pin-to-local-node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/pin-to-local-node.png -------------------------------------------------------------------------------- /assets/suggest-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/suggest-solution.png -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | build: 2 | GOARCH=wasm GOOS=js go build -o web/app.wasm 3 | go build 4 | 5 | run: build 6 | ./cyber-acid -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: stateless-minds-collective 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Stateless Minds 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 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stateless-minds/cyber-acid 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.3 6 | 7 | require ( 8 | github.com/NYTimes/gziphandler v1.1.1 9 | github.com/foolin/mixer v0.0.8 10 | github.com/maxence-charriere/go-app/v10 v10.0.8 11 | github.com/mitchellh/mapstructure v1.5.0 12 | github.com/stateless-minds/go-ipfs-api v0.7.5 13 | ) 14 | 15 | require ( 16 | github.com/benbjohnson/clock v1.3.5 // indirect 17 | github.com/blang/semver/v4 v4.0.0 // indirect 18 | github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf // indirect 19 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/ipfs/go-cid v0.4.1 // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 23 | github.com/libp2p/go-buffer-pool v0.1.0 // indirect 24 | github.com/libp2p/go-flow-metrics v0.2.0 // indirect 25 | github.com/libp2p/go-libp2p v0.37.0 // indirect 26 | github.com/minio/sha256-simd v1.0.1 // indirect 27 | github.com/mitchellh/go-homedir v1.1.0 // indirect 28 | github.com/mr-tron/base58 v1.2.0 // indirect 29 | github.com/multiformats/go-base32 v0.1.0 // indirect 30 | github.com/multiformats/go-base36 v0.2.0 // indirect 31 | github.com/multiformats/go-multiaddr v0.13.0 // indirect 32 | github.com/multiformats/go-multibase v0.2.0 // indirect 33 | github.com/multiformats/go-multicodec v0.9.0 // indirect 34 | github.com/multiformats/go-multihash v0.2.3 // indirect 35 | github.com/multiformats/go-multistream v0.5.0 // indirect 36 | github.com/multiformats/go-varint v0.0.7 // indirect 37 | github.com/spaolacci/murmur3 v1.1.0 // indirect 38 | github.com/stateless-minds/boxo v0.24.3 // indirect 39 | golang.org/x/crypto v0.28.0 // indirect 40 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect 41 | golang.org/x/sys v0.26.0 // indirect 42 | google.golang.org/protobuf v1.35.1 // indirect 43 | lukechampine.com/blake3 v1.3.0 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Cyber Acid 3 | 4 | 5 | 6 | ![Logo](./assets/logo.png) 7 | 8 | "A lot of people say if you care about global problems just be a politician. But is only the politician allowed to care about such issues? 9 | Isn't every living human being having that same right which is taken by the politicians with no other options presented? That's what representative democracy is all about. Full unconditional representation until the next election vote. Immense transfer of power with no guarantees whatsoever." 10 | 11 | Cyber Acid is a political simulator based on the liquid democracy concept. It is designed as an integration module that works with Cyber Stasis - the moneyless economy simulator. 12 | 13 | 14 | ## Screenshots 15 | 16 | 17 | 18 | 19 | 20 | Screenshot of how-to 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Screenshot of the shortages 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Screenshot of list solutions 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Screenshot of suggest solution 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Screenshot of delegate vote 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Screenshot of delegate trust 61 | 62 | 63 | 64 | ## Features 65 | 66 | 67 | 68 | * **Check shortages** - Review pressing issues. 69 | 70 | * **Suggest a solution** - Contribute with your expertise. 71 | 72 | * **Vote for solutions** - Vote for the best solution. 73 | 74 | * **Delegate your vote** - Not competent? Delegate your vote. 75 | 76 | * **Infinite delegation** - Delegation can be chained for maximum participation. 77 | 78 | * **Cross delegation** - Cross delegation is also supported. 79 | 80 | ## Community 81 | 82 | https://www.reddit.com/r/CyberAcid/ 83 | 84 | 85 | ## How to Play 86 | 87 | 88 | 89 | The game runs on the public IPFS network. In order to play it follow the steps below: 90 | 91 | 92 | 93 | 1. Install the official IPFS Desktop http://docs.ipfs.io/install/ipfs-desktop/ 94 | 95 | 2. Install IPFS Companion http://docs.ipfs.io/install/ipfs-companion/ 96 | 97 | 3. Clone https://github.com/stateless-minds/kubo to your local machine, build it with `make build` and run it with the following command: `~/cmd/ipfs/ipfs daemon --enable-pubsub-experiment` 98 | 99 | 4. Follow the instructions here to open your config file: https://github.com/ipfs/kubo/blob/master/docs/config.md. Usually it's `~/.ipfs/config` on Linux. Add the following snippet to the `HTTPHeaders`: 100 | 101 | ```{ 102 | 103 | "API": { 104 | 105 | "HTTPHeaders": { 106 | 107 | "Access-Control-Allow-Origin": ["webui://-", "http://localhost:3000", "http://k51qzi5uqu5dktpxn6nobipyd1i5q8kc4316ybqevx8ardb3diamyvtixshh0m.ipns.localhost:8080", "http://127.0.0.1:5001", "https://webui.ipfs.io"], 108 | 109 | "Access-Control-Allow-Credentials": ["true"], 110 | 111 | "Access-Control-Allow-Methods": ["PUT", "POST"] 112 | 113 | } 114 | 115 | }, 116 | 117 | ``` 118 | 119 | 6. Navigate to Cyber Acid and let's simulate the future together! 120 | 121 | 7. If you like the game consider pinning it to your local node so that you become a permanent host of it while you have IPFS daemon running 122 | 123 | ![SetPinning](./assets/pin.png) 124 | 125 | ![PinToLocalNode](./assets/pin-to-local-node.png) 126 | 127 | 128 | 129 | Please note the game has been developed on a WQHD resolution(2560x1440) and is currently not responsive or optimized for mobile devices. For best gaming experience if you play in FHD(1920x1080) please set your browser zoom settings to 150%. 130 | 131 | 132 | ## Roadmap 133 | 134 | 1. Make reputation index context/category based 135 | 136 | 2. Make voting time-restricted 137 | 138 | 139 | 140 | ## Acknowledgments 141 | 142 | 143 | 144 | 1. go-app 145 | 146 | 2. IPFS 147 | 148 | 3. Berty 149 | 150 | 4. All the rest of the authors who worked on the dependencies used! Thanks a lot! 151 | 152 | 153 | 154 | ## Contributing 155 | 156 | 157 | 158 | Open an issue 159 | 160 | 161 | 162 | ## License 163 | 164 | 165 | 166 | Stateless Minds (c) 2022 and contributors 167 | 168 | 169 | 170 | MIT License 171 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= 2 | github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 3 | github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= 4 | github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 5 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 6 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 7 | github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764= 8 | github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= 9 | github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf h1:dwGgBWn84wUS1pVikGiruW+x5XM4amhjaZO20vCjay4= 10 | github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= 15 | github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 16 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= 17 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 18 | github.com/foolin/mixer v0.0.8 h1:uGd94/kdPkk+GxMkUdfbBfXsYLNY7OEmKer1xNsKRVI= 19 | github.com/foolin/mixer v0.0.8/go.mod h1:Hgm3f6NIGGVT+BM9L26yauJuFmZiTSxTqTXurFz0RDE= 20 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 21 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 22 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 23 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 24 | github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 25 | github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 26 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 27 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 28 | github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= 29 | github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 30 | github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= 31 | github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= 32 | github.com/libp2p/go-libp2p v0.37.0 h1:8K3mcZgwTldydMCNOiNi/ZJrOB9BY+GlI3UxYzxBi9A= 33 | github.com/libp2p/go-libp2p v0.37.0/go.mod h1:GOKmSN99scDuYGTwaTbQPR8Nt6dxrK3ue7OjW2NGDg4= 34 | github.com/maxence-charriere/go-app/v10 v10.0.8 h1:ZbHTaIN1nMTzMvWmx5/wAMioDxnftNmkYfcX3O4lleE= 35 | github.com/maxence-charriere/go-app/v10 v10.0.8/go.mod h1:VyjGLeTiK6hfAQ/Q5ZVcLbuJ9vOZhKcewSC/ZwI+6WM= 36 | github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 37 | github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 38 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 39 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 40 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 41 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 42 | github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 43 | github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 44 | github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 45 | github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 46 | github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 47 | github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 48 | github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ= 49 | github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII= 50 | github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 51 | github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 52 | github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= 53 | github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= 54 | github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 55 | github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 56 | github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= 57 | github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= 58 | github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 59 | github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 60 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 61 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 62 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 63 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 64 | github.com/stateless-minds/boxo v0.24.3 h1:7UU9NDYv8z7EAeH64Pw7TDEqslp2J4io2DJWRZcFeVU= 65 | github.com/stateless-minds/boxo v0.24.3/go.mod h1:vmyA+qT8lTQByYf63nGOn5d4mwjW9QkNYL9oIc3kGfI= 66 | github.com/stateless-minds/go-ipfs-api v0.7.5 h1:nAqpcnUoDp/ZcqxTs+m5gtCeCDKl8eb/6V7pYCl6Yx4= 67 | github.com/stateless-minds/go-ipfs-api v0.7.5/go.mod h1:hwH/Up3TNtuwnAXvcSUilOKCfgJ85+c9faWa0KkKwAA= 68 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 69 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 70 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 71 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 72 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 73 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 74 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= 75 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= 76 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 78 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 79 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 80 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 81 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 82 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 83 | lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= 84 | lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= 85 | -------------------------------------------------------------------------------- /cyber-acid.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "sort" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/NYTimes/gziphandler" 12 | "github.com/foolin/mixer" 13 | "github.com/maxence-charriere/go-app/v10/pkg/app" 14 | "github.com/mitchellh/mapstructure" 15 | shell "github.com/stateless-minds/go-ipfs-api" 16 | ) 17 | 18 | const dbNameIssue = "issue" 19 | 20 | const dbNameCitizenReputation = "citizen_reputation" 21 | 22 | const typeShortage = "shortage" 23 | 24 | const ( 25 | topicCritical = "critical" 26 | topicIssue = "issue" 27 | ) 28 | 29 | const ( 30 | NotificationSuccess NotificationStatus = "positive" 31 | NotificationInfo NotificationStatus = "info" 32 | NotificationWarning NotificationStatus = "warning" 33 | NotificationDanger NotificationStatus = "negative" 34 | SuccessHeader = "Success" 35 | ErrorHeader = "Error" 36 | ) 37 | 38 | const ( 39 | asideTitleCreate = "Suggest Solution" 40 | asideTitleList = "List Solutions" 41 | ) 42 | 43 | // pubsub is a component that does a simple pubsub on ipfs. A component is a 44 | // customizable, independent, and reusable UI element. It is created by 45 | // embedding app.Compo into a struct. 46 | type acid struct { 47 | app.Compo 48 | sh *shell.Shell 49 | sub *shell.PubSubSubscription 50 | citizenID string 51 | issues []Issue 52 | categoryIssues map[string][]Issue 53 | ranks []CitizenReputation 54 | delegates []Delegate 55 | currentIssueInSlice int 56 | Solutions []Solution 57 | currentSolutionDescription string 58 | notifications map[string]notification 59 | notificationID int 60 | AsideTitle string 61 | } 62 | 63 | type NotificationStatus string 64 | 65 | type notification struct { 66 | id int 67 | status string 68 | header string 69 | message string 70 | } 71 | 72 | type Issue struct { 73 | ID string `mapstructure:"_id" json:"_id" validate:"uuid_rfc4122"` 74 | Type string `mapstructure:"type" json:"type" validate:"uuid_rfc4122"` 75 | Category string `mapstructure:"category" json:"category" validate:"uuid_rfc4122"` 76 | Description string `mapstructure:"description" json:"description" validate:"uuid_rfc4122"` 77 | Delegates []Delegate `mapstructure:"delegates" json:"delegates" validate:"uuid_rfc4122"` 78 | Solutions []Solution `mapstructure:"solutions" json:"solutions" validate:"uuid_rfc4122"` 79 | Voters []string `mapstructure:"voters" json:"voters" validate:"uuid_rfc4122"` 80 | } 81 | 82 | type Solution struct { 83 | ID string `mapstructure:"_id" json:"_id" validate:"uuid_rfc4122"` 84 | Description string `mapstructure:"description" json:"description" validate:"uuid_rfc4122"` 85 | Votes int `mapstructure:"votes" json:"votes" validate:"uuid_rfc4122"` 86 | } 87 | 88 | type Delegate struct { 89 | CitizenID string `mapstructure:"citizenId" json:"citizenId" validate:"uuid_rfc4122"` 90 | Votes int `mapstructure:"votes" json:"votes" validate:"uuid_rfc4122"` 91 | Selected int `mapstructure:"selected" json:"selected" validate:"uuid_rfc4122"` 92 | OwnVote bool `mapstructure:"voted" json:"voted" validate:"uuid_rfc4122"` 93 | } 94 | 95 | type CitizenReputation struct { 96 | ID string `mapstructure:"_id" json:"_id" validate:"uuid_rfc4122"` 97 | Type string `mapstructure:"type" json:"type" validate:"uuid_rfc4122"` 98 | CitizenID string `mapstructure:"citizenId" json:"citizenId" validate:"uuid_rfc4122"` 99 | ReputationIndex float64 `mapstructure:"reputationIndex" json:"reputationIndex" validate:"uuid_rfc4122"` 100 | } 101 | 102 | func (a *acid) OnMount(ctx app.Context) { 103 | sh := shell.NewShell("localhost:5001") 104 | a.sh = sh 105 | myPeer, err := a.sh.ID() 106 | if err != nil { 107 | log.Fatal(err) 108 | } 109 | 110 | citizenID := myPeer.ID[len(myPeer.ID)-8:] 111 | // replace password with your own 112 | password := "mysecretpassword" 113 | 114 | a.citizenID = mixer.EncodeString(password, citizenID) 115 | a.subscribeToCriticalTopic(ctx) 116 | a.subscribeToIssueTopic(ctx) 117 | a.notifications = make(map[string]notification) 118 | a.categoryIssues = make(map[string][]Issue) 119 | ctx.Async(func() { 120 | // err := a.sh.OrbitDocsDelete(dbNameIssue, "all") 121 | // if err != nil { 122 | // log.Fatal(err) 123 | // } 124 | 125 | // err := a.sh.OrbitDocsDelete(dbNameCitizenReputation, "4") 126 | // if err != nil { 127 | // log.Fatal(err) 128 | // } 129 | 130 | cr, err := a.sh.OrbitDocsQuery(dbNameCitizenReputation, "type", "reputation") 131 | if err != nil { 132 | log.Fatal(err) 133 | } 134 | 135 | var cc []interface{} 136 | err = json.Unmarshal(cr, &cc) 137 | if err != nil { 138 | log.Fatal(err) 139 | } 140 | 141 | for _, zz := range cc { 142 | r := CitizenReputation{} 143 | err = mapstructure.Decode(zz, &r) 144 | if err != nil { 145 | log.Fatal(err) 146 | } 147 | ctx.Dispatch(func(ctx app.Context) { 148 | a.ranks = append(a.ranks, r) 149 | sort.SliceStable(a.ranks, func(i, j int) bool { 150 | return a.ranks[i].ID < a.ranks[j].ID 151 | }) 152 | }) 153 | } 154 | 155 | v, err := a.sh.OrbitDocsQuery(dbNameIssue, "type", "shortage") 156 | if err != nil { 157 | log.Fatal(err) 158 | } 159 | 160 | var vv []interface{} 161 | err = json.Unmarshal(v, &vv) 162 | if err != nil { 163 | log.Fatal(err) 164 | } 165 | 166 | for _, ii := range vv { 167 | i := Issue{} 168 | err = mapstructure.Decode(ii, &i) 169 | if err != nil { 170 | log.Fatal(err) 171 | } 172 | ctx.Dispatch(func(ctx app.Context) { 173 | a.categoryIssues[i.Category] = append(a.categoryIssues[i.Category], i) 174 | a.issues = append(a.issues, i) 175 | sort.SliceStable(a.issues, func(i, j int) bool { 176 | return a.issues[i].ID < a.issues[j].ID 177 | }) 178 | }) 179 | } 180 | }) 181 | } 182 | 183 | func (a *acid) subscribeToCriticalTopic(ctx app.Context) { 184 | ctx.Async(func() { 185 | topic := topicCritical 186 | subscription, err := a.sh.PubSubSubscribe(topic) 187 | if err != nil { 188 | log.Fatal(err) 189 | } 190 | a.sub = subscription 191 | a.subscriptionCritical(ctx) 192 | }) 193 | } 194 | 195 | func (a *acid) subscribeToIssueTopic(ctx app.Context) { 196 | ctx.Async(func() { 197 | topic := topicIssue 198 | subscription, err := a.sh.PubSubSubscribe(topic) 199 | if err != nil { 200 | log.Fatal(err) 201 | } 202 | a.sub = subscription 203 | a.subscriptionIssue(ctx) 204 | }) 205 | } 206 | 207 | func (a *acid) subscriptionIssue(ctx app.Context) { 208 | ctx.Async(func() { 209 | defer a.sub.Cancel() 210 | // wait on pubsub 211 | res, err := a.sub.Next() 212 | if err != nil { 213 | log.Fatal(err) 214 | } 215 | // Decode the string data. 216 | str := string(res.Data) 217 | log.Println("Subscriber of topic issue received message: " + str) 218 | ctx.Async(func() { 219 | a.subscribeToIssueTopic(ctx) 220 | }) 221 | 222 | s := Issue{} 223 | err = json.Unmarshal([]byte(str), &s) 224 | if err != nil { 225 | log.Fatal(err) 226 | } 227 | 228 | id, err := strconv.Atoi(s.ID) 229 | if err != nil { 230 | log.Fatal(err) 231 | } 232 | 233 | ctx.Dispatch(func(ctx app.Context) { 234 | a.issues[id-1] = s 235 | }) 236 | }) 237 | } 238 | 239 | func (a *acid) subscriptionCritical(ctx app.Context) { 240 | ctx.Async(func() { 241 | defer a.sub.Cancel() 242 | // wait on pubsub 243 | res, err := a.sub.Next() 244 | if err != nil { 245 | log.Fatal(err) 246 | } 247 | // Decode the string data. 248 | str := string(res.Data) 249 | log.Println("Subscriber of topic critical received message: " + str) 250 | ctx.Async(func() { 251 | a.subscribeToCriticalTopic(ctx) 252 | }) 253 | 254 | s := Issue{} 255 | err = json.Unmarshal([]byte(str), &s) 256 | if err != nil { 257 | log.Fatal(err) 258 | } 259 | 260 | var lastID int 261 | unique := true 262 | for n, i := range a.issues { 263 | a.categoryIssues[i.Category] = append(a.categoryIssues[i.Category], i) 264 | if s.Description == i.Description { 265 | unique = false 266 | } 267 | 268 | if n == 0 { 269 | lastID, err = strconv.Atoi(i.ID) 270 | if err != nil { 271 | log.Fatal(err) 272 | } 273 | } else { 274 | currentID, err := strconv.Atoi(i.ID) 275 | if err != nil { 276 | log.Fatal(err) 277 | } 278 | previousID, err := strconv.Atoi(a.issues[n-1].ID) 279 | if err != nil { 280 | log.Fatal(err) 281 | } 282 | if currentID > previousID { 283 | lastID = currentID 284 | } 285 | } 286 | 287 | } 288 | if unique { 289 | newID := lastID + 1 290 | issue := Issue{ 291 | ID: strconv.Itoa(newID), 292 | Type: typeShortage, 293 | Category: s.Category, 294 | Description: s.Description, 295 | Solutions: []Solution{}, 296 | } 297 | 298 | i, err := json.Marshal(issue) 299 | if err != nil { 300 | log.Fatal(err) 301 | } 302 | 303 | err = a.sh.OrbitDocsPut(dbNameIssue, i) 304 | if err != nil { 305 | log.Fatal(err) 306 | } 307 | 308 | err = a.sh.PubSubPublish(topicIssue, string(i)) 309 | if err != nil { 310 | log.Fatal(err) 311 | } 312 | 313 | ctx.Dispatch(func(ctx app.Context) { 314 | a.issues = append(a.issues, issue) 315 | }) 316 | } 317 | }) 318 | } 319 | 320 | // The Render method is where the component appearance is defined. Here, a 321 | // "pubsub World!" is displayed as a heading. 322 | func (a *acid) Render() app.UI { 323 | return app.Div().Class("l-application").Role("presentation").Body( 324 | app.Link().Rel("stylesheet").Href("https://assets.ubuntu.com/v1/vanilla-framework-version-3.8.0.min.css"), 325 | app.Link().Rel("stylesheet").Href("https://use.fontawesome.com/releases/v6.2.0/css/all.css"), 326 | app.Link().Rel("stylesheet").Href("/app.css"), 327 | app.Header().Class("l-navigation is-collapsed").Body( 328 | app.Div().Class("l-navigation__drawer").Body( 329 | app.Div().Class("p-panel is-dark").Body( 330 | app.Div().Class("p-panel__header is-sticky").Body( 331 | app.A().Class("p-panel__logo").Href("#").Body( 332 | app.H5().Class("p-heading--2").Text("Cyber Acid"), 333 | ), 334 | ), 335 | app.Hr(), 336 | app.P().Class("p-heading--6").Body( 337 | app.Text("Liquid democracy politics simulator based on the automated data feed from the moneyless economy simulator "), 338 | app.A().Href("https://github.com/stateless-minds/cyber-stasis").Text("Cyber Stasis"), 339 | ).Style("padding", "0 10%;"), 340 | app.Hr(), 341 | app.Div().Class("p-panel__content").Body( 342 | app.Div().Class("p-side-navigation--icons is-dark").ID("drawer-icons").Body( 343 | app.Nav().Aria("label", "Main"), 344 | app.Ul().Class("p-side-navigation__list").Body( 345 | app.Li().Class("p-side-navigation__item--title").Body( 346 | app.A().Class("p-side-navigation__link").Href("#").Body( 347 | app.I().Class("p-icon--help is-light p-side-navigation__icon"), 348 | app.Span().Class("p-side-navigation__label").Text("How to play"), 349 | ).OnClick(a.openHowToDialog), 350 | app.A().Class("p-side-navigation__link").Href("#").Body( 351 | app.I().Class("p-icon--warning is-light p-side-navigation__icon"), 352 | app.Span().Class("p-side-navigation__label").Text("Shortages"), 353 | ).Aria("current", "page"), 354 | app.A().Class("p-side-navigation__link").Href("#").Body( 355 | app.I().Class("p-icon--share is-light p-side-navigation__icon"), 356 | app.Span().Class("p-side-navigation__label").Text("Delegate rankings"), 357 | ).OnClick(a.openRankingsDialog), 358 | ), 359 | ), 360 | ), 361 | ), 362 | ), 363 | ), 364 | ), 365 | app.Main().Class("l-main").Body( 366 | app.Div().Class("p-panel").Body( 367 | app.If(len(a.notifications) > 0, func() app.UI { 368 | return app.Range(a.notifications).Map(func(s string) app.UI { 369 | return app.Div().Class("p-notification--" + a.notifications[s].status).Body( 370 | app.Div().Class("p-notification__content").Body( 371 | app.H5().Class("p-notification__title").Text(a.notifications[s].header), 372 | app.P().Class("p-notification__message").Text(a.notifications[s].message), 373 | ), 374 | ) 375 | }) 376 | }), 377 | app.Div().Class("p-panel__header").Body( 378 | app.H4().Class("p-panel__title").Text("Open Issues"), 379 | ), 380 | app.Div().Class("p-panel__content").Body( 381 | app.Div().Class("u-fixed-width").Body( 382 | app.If(len(a.categoryIssues) > 0, func() app.UI { 383 | return app.Range(a.categoryIssues).Map(func(s string) app.UI { 384 | return app.Table().Aria("label", "Issues table").Class("p-main-table").Body( 385 | app.THead().Body( 386 | app.Tr().Body( 387 | app.Th().Body( 388 | app.Span().Class("status-icon is-running").Text("Category "+s), 389 | ), 390 | app.Th().Text("Actions"), 391 | ), 392 | ), 393 | app.If(len(a.categoryIssues[s]) > 0, func() app.UI { 394 | return app.TBody().Body( 395 | app.Range(a.categoryIssues[s]).Slice(func(i int) app.UI { 396 | return app.Tr().DataSet("id", i).Body( 397 | app.Td().DataSet("column", "issue").Body( 398 | app.Div().Text(a.categoryIssues[s][i].Description), 399 | ), 400 | app.Td().DataSet("column", "action").Body( 401 | app.Div().Body( 402 | app.Button().Class("u-no-margin--bottom").Text("List Solutions").Value(a.categoryIssues[s][i].ID).OnMouseOver(a.asidePreloadList).OnClick(a.asideOpenList), 403 | app.Button().Class("u-no-margin--bottom").Text("Suggest Solution").Value(a.categoryIssues[s][i].ID).OnMouseOver(a.asidePreloadCreate).OnClick(a.asideOpenCreate), 404 | ), 405 | ), 406 | ) 407 | }), 408 | ) 409 | }), 410 | ) 411 | }) 412 | }), 413 | ), 414 | app.Div().Class("p-modal").ID("howto-modal").Style("display", "none").Body( 415 | app.Section().Class("p-modal__dialog").Role("dialog").Aria("modal", true).Aria("labelledby", "modal-title").Aria("describedby", "modal-description").Body( 416 | app.Header().Class("p-modal__header").Body( 417 | app.H2().Class("p-modal__title").ID("modal-title").Text("How to play"), 418 | app.Button().Class("p-modal__close").Aria("label", "Close active modal").Aria("controls", "modal").OnClick(a.closeHowToModal), 419 | ), 420 | app.Div().Class("p-heading-icon--small").Body( 421 | app.Aside().Class("p-accordion").Body( 422 | app.Ul().Class("p-accordion__list").Body( 423 | app.Li().Class("p-accordion__group").Body( 424 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 425 | app.Button().Type("button").Class("p-accordion__tab").ID("tab1").Aria("controls", "tab1-section").Aria("expanded", true).Text("What is Cyber Acid").Value("tab1-section").OnClick(a.toggleAccordion), 426 | ), 427 | app.Section().Class("p-accordion__panel").ID("tab1-section").Aria("hidden", false).Aria("labelledby", "tab1").Body( 428 | app.P().Text("Cyber Acid is a political simulator based on the liquid democracy concept. It is designed as an integration module that works with Cyber Stasis - the moneyless economy simulator."), 429 | ), 430 | ), 431 | app.Li().Class("p-accordion__group").Body( 432 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 433 | app.Button().Type("button").Class("p-accordion__tab").ID("tab2").Aria("controls", "tab2-section").Aria("expanded", true).Text("What is liquid democracy").Value("tab2-section").OnClick(a.toggleAccordion), 434 | ), 435 | app.Section().Class("p-accordion__panel").ID("tab2-section").Aria("hidden", true).Aria("labelledby", "tab1").Body( 436 | app.P().Text("Liquid democracy meets the transparency and accountability of direct democracy with the easy of use of representative democracy. Vote directly for what you want and delegate one-time voting rights per topic."), 437 | ), 438 | ), 439 | app.Li().Class("p-accordion__group").Body( 440 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 441 | app.Button().Type("button").Class("p-accordion__tab").ID("tab3").Aria("controls", "tab3-section").Aria("expanded", true).Text("How it works").Value("tab3-section").OnClick(a.toggleAccordion), 442 | ), 443 | app.Section().Class("p-accordion__panel").ID("tab3-section").Aria("hidden", true).Aria("labelledby", "tab3").Body( 444 | app.P().Text("The simulator receives live data from Cyber Stasis about critical shortages of production and resources. The goal of all participants is to suggest solutions to those issues. For example - replacing a resource with another one, researching new technologies etc."), 445 | ), 446 | ), 447 | app.Li().Class("p-accordion__group").Body( 448 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 449 | app.Button().Type("button").Class("p-accordion__tab").ID("tab4").Aria("controls", "tab4-section").Aria("expanded", true).Text("Features").Value("tab4-section").OnClick(a.toggleAccordion), 450 | ), 451 | app.Section().Class("p-accordion__panel").ID("tab4-section").Aria("hidden", true).Aria("labelledby", "tab3").Body( 452 | app.Ul().Class("p-matrix").Body( 453 | app.Li().Class("p-matrix__item").Body( 454 | app.Div().Class("p-matrix__content").Body( 455 | app.H3().Class("p-matrix__title").Text("Check shortages"), 456 | app.Div().Class("p-matrix__desc").Body( 457 | app.P().Text("Review pressing issues."), 458 | ), 459 | ), 460 | ), 461 | app.Li().Class("p-matrix__item").Body( 462 | app.Div().Class("p-matrix__content").Body( 463 | app.H3().Class("p-matrix__title").Text("Suggest a solution"), 464 | app.Div().Class("p-matrix__desc").Body( 465 | app.P().Text("Contribute with your expertise."), 466 | ), 467 | ), 468 | ), 469 | app.Li().Class("p-matrix__item").Body( 470 | app.Div().Class("p-matrix__content").Body( 471 | app.H3().Class("p-matrix__title").Text("Vote for solutions"), 472 | app.Div().Class("p-matrix__desc").Body( 473 | app.P().Text("Vote for the best solution."), 474 | ), 475 | ), 476 | ), 477 | app.Li().Class("p-matrix__item").Body( 478 | app.Div().Class("p-matrix__content").Body( 479 | app.H3().Class("p-matrix__title").Text("Delegate your vote"), 480 | app.Div().Class("p-matrix__desc").Body( 481 | app.P().Text("Not competent? Delegate your vote."), 482 | ), 483 | ), 484 | ), 485 | app.Li().Class("p-matrix__item").Body( 486 | app.Div().Class("p-matrix__content").Body( 487 | app.H3().Class("p-matrix__title").Text("Infinite delegation"), 488 | app.Div().Class("p-matrix__desc").Body( 489 | app.P().Text("Delegation can be chained for maximum participation."), 490 | ), 491 | ), 492 | ), 493 | app.Li().Class("p-matrix__item").Body( 494 | app.Div().Class("p-matrix__content").Body( 495 | app.H3().Class("p-matrix__title").Text("Cross delegation"), 496 | app.Div().Class("p-matrix__desc").Body( 497 | app.P().Text("Cross delegation is also supported."), 498 | ), 499 | ), 500 | ), 501 | ), 502 | ), 503 | ), 504 | app.Li().Class("p-accordion__group").Body( 505 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 506 | app.Button().Type("button").Class("p-accordion__tab").ID("tab5").Aria("controls", "tab5-section").Aria("expanded", true).Text("Support us").Value("tab5-section").OnClick(a.toggleAccordion), 507 | ), 508 | app.Section().Class("p-accordion__panel").ID("tab5-section").Aria("hidden", true).Aria("labelledby", "tab5").Body( 509 | app.A().Href("https://opencollective.com/stateless-minds-collective").Text("https://opencollective.com/stateless-minds-collective"), 510 | ), 511 | ), 512 | app.Li().Class("p-accordion__group").Body( 513 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 514 | app.Button().Type("button").Class("p-accordion__tab").ID("tab6").Aria("controls", "tab6-section").Aria("expanded", true).Text("Terms of service").Value("tab6-section").OnClick(a.toggleAccordion), 515 | ), 516 | app.Section().Class("p-accordion__panel").ID("tab6-section").Aria("hidden", true).Aria("labelledby", "tab6").Body( 517 | app.Div().Class("p-card").Body( 518 | app.H3().Text("Introduction"), 519 | app.P().Class("p-card__content").Text("Cyber Acid is a liquid democracy political simulator in the form of a fictional game based on real-time data from Cyber Stasis. By using the application you are implicitly agreeing to share your peer id with the IPFS public network."), 520 | ), 521 | app.Div().Class("p-card").Body( 522 | app.H3().Text("Application Hosting"), 523 | app.P().Class("p-card__content").Text("Cyber Acid is a decentralized application and is hosted on a public peer to peer network. By using the application you agree to host it on the public IPFS network free of charge for as long as your usage is."), 524 | ), 525 | app.Div().Class("p-card").Body( 526 | app.H3().Text("User-Generated Content"), 527 | app.P().Class("p-card__content").Text("All published content is user-generated, fictional and creators are not responsible for it."), 528 | ), 529 | ), 530 | ), 531 | app.Li().Class("p-accordion__group").Body( 532 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 533 | app.Button().Type("button").Class("p-accordion__tab").ID("tab7").Aria("controls", "tab7-section").Aria("expanded", true).Text("Privacy policy").Value("tab7-section").OnClick(a.toggleAccordion), 534 | ), 535 | app.Section().Class("p-accordion__panel").ID("tab7-section").Aria("hidden", true).Aria("labelledby", "tab7").Body( 536 | app.Div().Class("p-card").Body( 537 | app.H3().Text("Personal data"), 538 | app.P().Class("p-card__content").Text("There is no personal information collected within Cyber Acid. We store a small portion of your peer ID encrypted as a non-unique identifier which is used for displaying the ranks interface."), 539 | ), 540 | app.Div().Class("p-card").Body( 541 | app.H3().Text("Coookies"), 542 | app.P().Class("p-card__content").Text("Cyber Acid does not use cookies."), 543 | ), 544 | app.Div().Class("p-card").Body( 545 | app.H3().Text("Links to Cyber Stasis"), 546 | app.P().Class("p-card__content").Text("Cyber Acid contains links to its sister project Cyber Stasis and depends on its data to function properly."), 547 | ), 548 | app.Div().Class("p-card").Body( 549 | app.H3().Text("Changes to this privacy policy"), 550 | app.P().Class("p-card__content").Text("This Privacy Policy might be updated from time to time. Thus, it is advised to review this page periodically for any changes. You will be notified of any changes from this page. Changes are effective immediately after they are posted on this page."), 551 | ), 552 | ), 553 | ), 554 | ), 555 | ), 556 | ), 557 | ).Style("left", "10%").Style("width", "80%"), 558 | ), 559 | app.Div().Class("p-modal").ID("rankings-modal").Style("display", "none").Body( 560 | app.Section().Class("p-modal__dialog").Role("dialog").Aria("modal", true).Aria("labelledby", "modal-title").Aria("describedby", "modal-description").Body( 561 | app.Header().Class("p-modal__header").Body( 562 | app.H2().Class("p-modal__title").ID("modal-title").Text("Delegate rankings"), 563 | app.Button().Class("p-modal__close").Aria("label", "Close active modal").Aria("controls", "modal").OnClick(a.closeRankingsModal), 564 | ), 565 | app.Table().Aria("label", "Rankings table").Class("p-main-table").Body( 566 | app.THead().Body( 567 | app.Tr().Body( 568 | app.Th().Body( 569 | app.Span().Class("status-icon is-blocked").Text("Delegate ID"), 570 | ), 571 | app.Th().Text("Trust"), 572 | ), 573 | ), 574 | app.If(len(a.delegates) > 0, func() app.UI { 575 | return app.TBody().Body( 576 | app.Range(a.delegates).Slice(func(i int) app.UI { 577 | return app.Tr().DataSet("id", i).Body( 578 | app.Td().DataSet("column", "delegate").Body( 579 | app.Div().Text(a.delegates[i].CitizenID), 580 | ), 581 | app.Td().DataSet("column", "trust").Body( 582 | app.Div().Text(a.delegates[i].Selected), 583 | ), 584 | ) 585 | }), 586 | ) 587 | }), 588 | ), 589 | ).Style("left", "10%").Style("width", "80%"), 590 | ), 591 | ), 592 | ), 593 | ), 594 | app.Aside().Class("l-aside is-collapsed").ID("aside-panel").Body( 595 | app.Div().Class("p-panel").Body( 596 | app.Div().Class("p-panel__header").Body( 597 | app.H4().Class("p-panel__title").Text(a.AsideTitle), 598 | app.Div().Class("p-panel__controls").Body( 599 | app.Button().Class("p-button--base u-no-margin--bottom has-icon").Body(app.I().Class("p-icon--close")).OnClick(a.asideClose), 600 | ), 601 | ), 602 | app.If(a.AsideTitle == asideTitleCreate, func() app.UI { 603 | return app.Div().Class("p-panel__content").Body( 604 | app.Div().Class("p-form p-form--stacked").Body( 605 | app.Div().Class("p-form__group row").Body( 606 | app.Textarea().ID("solution").Name("solution").Rows(3).OnKeyUp(a.onSolution), 607 | ), 608 | ), 609 | app.Div().Class("row").Body( 610 | app.Div().Class("col-12").Body( 611 | app.Button().Class("p-button--positive u-float-right").Name("submit-solution").Text("Submit Solution").OnClick(a.submitSolution), 612 | ), 613 | ), 614 | ) 615 | }).ElseIf(a.AsideTitle == asideTitleList, func() app.UI { 616 | return app.Div().Class("p-panel__content").Body( 617 | app.Ul().Class("p-list-tree").Aria("multiselectable", true).Role("tree").Body( 618 | app.Li().Class("p-list-tree__item p-list-tree__item--group").Role("treeitem").Body( 619 | app.Button().Class("p-list-tree__toggle").ID("sub-1-btn").Aria("controls", "sub-1").Aria("expanded", true).Text("Suggested Solutions"), 620 | app.Ul().Class("p-list-tree").Role("group").ID("sub-1").Aria("hidden", false).Aria("labelledby", "sub-1-btn").Body( 621 | app.Range(a.Solutions).Slice(func(i int) app.UI { 622 | return app.Li().Class("p-list-tree__item").Role("treeitem").Body( 623 | app.P().Text(a.Solutions[i].Description), 624 | app.If(len(a.issues[a.currentIssueInSlice].Voters) > 0, func() app.UI { 625 | return app.If(sliceContains(a.issues[a.currentIssueInSlice].Voters, a.citizenID), func() app.UI { 626 | return app.Button(). 627 | Class("p-button is-small is-inline"). 628 | Text("Vote"). 629 | Value(a.Solutions[i].ID). 630 | OnClick(a.vote). 631 | Disabled(true). 632 | Body( 633 | app.I().Class("fa-solid fa-thumbs-up"), // Icon 634 | app.Span(). 635 | Class("p-badge"). 636 | Aria("label", strconv.Itoa(a.Solutions[i].Votes)+" votes"). 637 | Text(strconv.Itoa(a.Solutions[i].Votes)), // Votes count 638 | ) 639 | }).Else( func() app.UI { 640 | return app.Div().Class("vote-delegate-container").Body( 641 | // Vote Button with Icon 642 | app.Button(). 643 | Class("p-button is-small is-inline"). 644 | Text("Vote"). 645 | Value(a.Solutions[i].ID). 646 | OnClick(a.vote). 647 | Body( 648 | app.I().Class("fa-regular fa-thumbs-up"), // Icon for Vote 649 | ), 650 | 651 | // Delegate Button 652 | app.Button(). 653 | Class("p-button is-small is-inline"). 654 | ID("show-modal"). 655 | Text("Delegate..."). 656 | Aria("controls", "modal"). 657 | Value(a.Solutions[i].ID). 658 | OnClick(a.openDelegateDialog), 659 | 660 | // Modal for Delegation 661 | app.Div(). 662 | Class("p-modal"). 663 | ID("delegate-modal"). 664 | Style("display", "none"). // Initially hidden 665 | Body( 666 | app.Section(). 667 | Class("p-modal__dialog"). 668 | Role("dialog"). 669 | Aria("modal", true). 670 | Aria("labelledby", "modal-title"). 671 | Aria("describedby", "modal-description"). 672 | Body( 673 | app.Header(). 674 | Class("p-modal__header"). 675 | Body( 676 | app.H2(). 677 | Class("p-modal__title"). 678 | ID("modal-title"). 679 | Text("Delegate"), 680 | app.Button(). 681 | Class("p-modal__close"). 682 | Aria("label", "Close active modal"). 683 | Aria("controls", "modal"). 684 | Value(a.Solutions[i].ID). 685 | OnClick(a.closeDelegateModal), 686 | ), 687 | app.P(). 688 | ID("modal-description"). 689 | Text("Select a citizen to represent your vote for this issue:"), 690 | app.Div(). 691 | Class("p-heading-icon--small"). 692 | Body( 693 | app.Range(a.ranks).Slice(func(i int) app.UI { 694 | return app.If(a.ranks[i].CitizenID != a.citizenID, func() app.UI { 695 | return app.Div().Class("p-heading-icon__header").Body( 696 | app.Button(). 697 | Class("p-chip"). 698 | Aria("pressed", true). 699 | Disabled(true). 700 | Body( 701 | app.Span().Class("p-chip__value").Text("Citizen"), 702 | app.Span().Class("p-badge").Aria("label", "Citizen").Text(a.ranks[i].CitizenID), 703 | ), 704 | app.Button(). 705 | Class("p-chip"). 706 | Aria("pressed", true). 707 | Disabled(true). 708 | Body( 709 | app.Span().Class("p-chip__value").Text("Reputation"), 710 | app.Span().Class("p-badge").Aria("label", "Reputation").Text(a.ranks[i].ReputationIndex), 711 | ), 712 | app.Button(). 713 | Class("p-chip"). 714 | Body( 715 | app.Span().Class("p-chip__value").Text("Select"), 716 | ).Value(a.ranks[i].CitizenID).OnClick(a.delegate), 717 | ) 718 | }) 719 | }), 720 | ), 721 | ), 722 | ), 723 | ) 724 | }) 725 | }).Else( func() app.UI { 726 | return app.Div().Class("vote-delegate-container").Body( 727 | // Vote Button with Icon 728 | app.Button(). 729 | Class("p-button is-small is-inline"). 730 | Text("Vote"). 731 | Value(a.Solutions[i].ID). 732 | OnClick(a.vote). 733 | Body( 734 | app.I().Class("fa-regular fa-thumbs-up"), // Icon for Vote 735 | ), 736 | 737 | // Delegate Button 738 | app.Button(). 739 | Class("p-button is-small is-inline"). 740 | ID("show-modal"). 741 | Text("Delegate..."). 742 | Aria("controls", "modal"). 743 | Value(a.Solutions[i].ID). 744 | OnClick(a.openDelegateDialog), 745 | 746 | // Modal for Delegation 747 | app.Div(). 748 | Class("p-modal"). 749 | ID("delegate-modal"). 750 | Style("display", "none"). // Initially hidden 751 | Body( 752 | app.Section(). 753 | Class("p-modal__dialog"). 754 | Role("dialog"). 755 | Aria("modal", true). 756 | Aria("labelledby", "modal-title"). 757 | Aria("describedby", "modal-description"). 758 | Body( 759 | app.Header(). 760 | Class("p-modal__header"). 761 | Body( 762 | app.H2(). 763 | Class("p-modal__title"). 764 | ID("modal-title"). 765 | Text("Delegate"), 766 | app.Button(). 767 | Class("p-modal__close"). 768 | Aria("label", "Close active modal"). 769 | Aria("controls", "modal"). 770 | Value(a.Solutions[i].ID). 771 | OnClick(a.closeDelegateModal), 772 | ), 773 | app.P(). 774 | ID("modal-description"). 775 | Text("Select a citizen to represent your vote for this issue:"), 776 | app.Div(). 777 | Class("p-heading-icon--small"). 778 | Body( 779 | app.Range(a.ranks).Slice(func(i int) app.UI { 780 | return app.If(a.ranks[i].CitizenID != a.citizenID, func() app.UI { 781 | return app.Div().Class("p-heading-icon__header").Body( 782 | app.Button(). 783 | Class("p-chip"). 784 | Aria("pressed", true). 785 | Disabled(true). 786 | Body( 787 | app.Span().Class("p-chip__value").Text("Citizen"), 788 | app.Span().Class("p-badge").Aria("label", "Citizen").Text(a.ranks[i].CitizenID), 789 | ), 790 | app.Button(). 791 | Class("p-chip"). 792 | Aria("pressed", true). 793 | Disabled(true). 794 | Body( 795 | app.Span().Class("p-chip__value").Text("Reputation"), 796 | app.Span().Class("p-badge").Aria("label", "Reputation").Text(a.ranks[i].ReputationIndex), 797 | ), 798 | app.Button(). 799 | Class("p-chip"). 800 | Body( 801 | app.Span().Class("p-chip__value").Text("Select"), 802 | ).Value(a.ranks[i].CitizenID).OnClick(a.delegate), 803 | ) 804 | }) 805 | }), 806 | ), 807 | ), 808 | ), 809 | ) 810 | }), 811 | ) 812 | }), 813 | ), 814 | ), 815 | ), 816 | ) 817 | }), 818 | ), 819 | ), 820 | ) 821 | } 822 | 823 | func (a *acid) asidePreloadList(ctx app.Context, e app.Event) { 824 | issueID := ctx.JSSrc().Get("value").String() 825 | issueIDInt, err := strconv.Atoi(issueID) 826 | if err != nil { 827 | log.Fatal(err) 828 | } 829 | a.currentIssueInSlice = issueIDInt - 1 830 | a.Solutions = a.issues[issueIDInt-1].Solutions 831 | a.AsideTitle = asideTitleList 832 | } 833 | 834 | func (a *acid) asideOpenList(ctx app.Context, e app.Event) { 835 | app.Window().Get("document").Call("querySelector", ".l-aside").Get("classList").Call("remove", "is-collapsed") 836 | } 837 | 838 | func (a *acid) asidePreloadCreate(ctx app.Context, e app.Event) { 839 | a.AsideTitle = asideTitleCreate 840 | } 841 | 842 | func (a *acid) asideOpenCreate(ctx app.Context, e app.Event) { 843 | app.Window().Get("document").Call("querySelector", ".l-aside").Get("classList").Call("remove", "is-collapsed") 844 | app.Window().Get("document").Call("querySelector", ".p-button--positive").Call("setAttribute", "id", ctx.JSSrc().Get("value").String()) 845 | } 846 | 847 | func (a *acid) asideClose(ctx app.Context, e app.Event) { 848 | app.Window().Get("document").Call("querySelector", ".l-aside").Get("classList").Call("add", "is-collapsed") 849 | } 850 | 851 | func (a *acid) onSolution(ctx app.Context, e app.Event) { 852 | a.currentSolutionDescription = ctx.JSSrc().Get("value").String() 853 | } 854 | 855 | func (a *acid) vote(ctx app.Context, e app.Event) { 856 | ctx.JSSrc().Get("firstChild").Get("classList").Call("remove", "fa-regular") 857 | 858 | val := ctx.JSSrc().Get("value").String() 859 | solutionID, err := strconv.Atoi(val) 860 | if err != nil { 861 | log.Fatal(err) 862 | } 863 | 864 | currentIssue := a.issues[a.currentIssueInSlice] 865 | var delegate bool 866 | var delegatedVotes int 867 | var ownVote bool 868 | // delegated voting logic 869 | for i, d := range currentIssue.Delegates { 870 | if a.citizenID == d.CitizenID { 871 | delegate = true 872 | ownVote = currentIssue.Delegates[i].OwnVote 873 | if !d.OwnVote { 874 | currentIssue.Delegates[i].OwnVote = true 875 | 876 | } 877 | delegatedVotes = d.Votes 878 | currentIssue.Delegates[i].Votes = 0 879 | } 880 | } 881 | 882 | currentIssue.Voters = append(currentIssue.Voters, a.citizenID) 883 | 884 | if delegate { 885 | if !ownVote { 886 | currentIssue.Solutions[solutionID-1].Votes += delegatedVotes + 1 887 | } else { 888 | currentIssue.Solutions[solutionID-1].Votes += delegatedVotes 889 | } 890 | 891 | } else { 892 | currentIssue.Solutions[solutionID-1].Votes++ 893 | } 894 | 895 | i, err := json.Marshal(currentIssue) 896 | if err != nil { 897 | log.Fatal(err) 898 | } 899 | ctx.Async(func() { 900 | err = a.sh.OrbitDocsPut(dbNameIssue, i) 901 | if err != nil { 902 | ctx.Dispatch(func(ctx app.Context) { 903 | a.createNotification(ctx, NotificationDanger, ErrorHeader, "Could not vote for solution. Try again later.") 904 | log.Fatal(err) 905 | }) 906 | } 907 | err = a.sh.PubSubPublish(topicIssue, string(i)) 908 | if err != nil { 909 | log.Fatal(err) 910 | } 911 | ctx.Dispatch(func(ctx app.Context) { 912 | a.issues[a.currentIssueInSlice] = currentIssue 913 | a.createNotification(ctx, NotificationSuccess, SuccessHeader, "Vote accepted.") 914 | }) 915 | }) 916 | } 917 | 918 | func (a *acid) openRankingsDialog(ctx app.Context, e app.Event) { 919 | for _, i := range a.issues { 920 | a.delegates = append(a.delegates, i.Delegates...) 921 | } 922 | sort.SliceStable(a.delegates, func(i, j int) bool { 923 | return a.delegates[i].Selected > a.delegates[j].Selected 924 | }) 925 | app.Window().GetElementByID("rankings-modal").Set("style", "display:flex") 926 | } 927 | 928 | func (a *acid) openHowToDialog(ctx app.Context, e app.Event) { 929 | app.Window().GetElementByID("howto-modal").Set("style", "display:flex") 930 | } 931 | 932 | func (a *acid) openDelegateDialog(ctx app.Context, e app.Event) { 933 | app.Window().GetElementByID("delegate-modal").Set("style", "display:flex") 934 | } 935 | 936 | func (a *acid) delegate(ctx app.Context, e app.Event) { 937 | citizenID := ctx.JSSrc().Get("value").String() 938 | issue := a.issues[a.currentIssueInSlice] 939 | 940 | var delegateExists bool 941 | var delegate Delegate 942 | votesTransfer := 1 943 | if len(issue.Delegates) > 0 { 944 | for ii, dd := range issue.Delegates { 945 | // recursive delegation logic 946 | if dd.CitizenID == a.citizenID { 947 | // transfer origin votes to recipient plus own vote 948 | votesTransfer = dd.Votes + 1 949 | // set origin delegator's votes to zero 950 | issue.Delegates[ii].Votes = 0 951 | // set origin delegator as voted 952 | issue.Delegates[ii].OwnVote = true 953 | } 954 | } 955 | 956 | for i, d := range issue.Delegates { 957 | if d.CitizenID == citizenID { 958 | issue.Delegates[i].Votes += votesTransfer 959 | issue.Delegates[i].Selected++ 960 | delegateExists = true 961 | } 962 | } 963 | } 964 | 965 | if len(issue.Delegates) == 0 || !delegateExists { 966 | delegate = Delegate{ 967 | CitizenID: citizenID, 968 | Votes: votesTransfer, 969 | Selected: 1, 970 | } 971 | issue.Delegates = append(issue.Delegates, delegate) 972 | } 973 | issue.Voters = append(issue.Voters, a.citizenID) 974 | var voters []string 975 | for _, v := range issue.Voters { 976 | // if the delegate already voted previously remove from voters so he can vote again on new delegation 977 | if citizenID != v { 978 | voters = append(voters, v) 979 | } 980 | } 981 | issue.Voters = voters 982 | 983 | i, err := json.Marshal(issue) 984 | if err != nil { 985 | log.Fatal(err) 986 | } 987 | 988 | ctx.Async(func() { 989 | err = a.sh.OrbitDocsPut(dbNameIssue, i) 990 | if err != nil { 991 | log.Fatal(err) 992 | } 993 | 994 | err = a.sh.PubSubPublish(topicIssue, string(i)) 995 | if err != nil { 996 | log.Fatal(err) 997 | } 998 | 999 | ctx.Dispatch(func(ctx app.Context) { 1000 | a.issues[a.currentIssueInSlice] = issue 1001 | a.closeDelegateModal(ctx, e) 1002 | a.createNotification(ctx, NotificationSuccess, SuccessHeader, "Vote delegated.") 1003 | }) 1004 | }) 1005 | } 1006 | 1007 | func (a *acid) toggleAccordion(ctx app.Context, e app.Event) { 1008 | id := ctx.JSSrc().Get("value").String() 1009 | attr := app.Window().GetElementByID(id).Get("attributes") 1010 | aria := attr.Get("aria-hidden").Get("value").String() 1011 | if aria == "false" { 1012 | app.Window().GetElementByID(id).Call("setAttribute", "aria-hidden", "true") 1013 | } else { 1014 | app.Window().GetElementByID(id).Call("setAttribute", "aria-hidden", "false") 1015 | } 1016 | } 1017 | 1018 | func (a *acid) closeRankingsModal(ctx app.Context, e app.Event) { 1019 | app.Window().GetElementByID("rankings-modal").Set("style", "display:none") 1020 | } 1021 | 1022 | func (a *acid) closeHowToModal(ctx app.Context, e app.Event) { 1023 | app.Window().GetElementByID("howto-modal").Set("style", "display:none") 1024 | } 1025 | 1026 | func (a *acid) closeDelegateModal(ctx app.Context, e app.Event) { 1027 | app.Window().GetElementByID("delegate-modal").Set("style", "display:none") 1028 | } 1029 | 1030 | func (a *acid) submitSolution(ctx app.Context, e app.Event) { 1031 | idStr := ctx.JSSrc().Get("id").String() 1032 | id, err := strconv.Atoi(idStr) 1033 | if err != nil { 1034 | log.Fatal(err) 1035 | } 1036 | 1037 | lastSolutionID := 0 1038 | unique := true 1039 | if len(a.issues[id-1].Solutions) > 0 { 1040 | solutions := a.issues[id-1].Solutions 1041 | for n, s := range solutions { 1042 | if s.Description == a.currentSolutionDescription { 1043 | unique = false 1044 | } 1045 | if n > 0 { 1046 | currentID, err := strconv.Atoi(s.ID) 1047 | if err != nil { 1048 | log.Fatal(err) 1049 | } 1050 | previousID, err := strconv.Atoi(solutions[n-1].ID) 1051 | if err != nil { 1052 | log.Fatal(err) 1053 | } 1054 | if currentID > previousID { 1055 | lastSolutionID = currentID 1056 | } 1057 | } else { 1058 | lastSolutionID = 1 1059 | } 1060 | } 1061 | } 1062 | 1063 | if unique { 1064 | solution := Solution{ 1065 | ID: strconv.Itoa(lastSolutionID + 1), 1066 | Description: a.currentSolutionDescription, 1067 | Votes: 0, 1068 | } 1069 | 1070 | a.issues[id-1].Solutions = append(a.issues[id-1].Solutions, solution) 1071 | 1072 | i, err := json.Marshal(a.issues[id-1]) 1073 | if err != nil { 1074 | log.Fatal(err) 1075 | } 1076 | 1077 | ctx.Async(func() { 1078 | err = a.sh.OrbitDocsPut(dbNameIssue, i) 1079 | if err != nil { 1080 | ctx.Dispatch(func(ctx app.Context) { 1081 | a.createNotification(ctx, NotificationDanger, ErrorHeader, "Could not create solution. Try again later.") 1082 | log.Fatal(err) 1083 | }) 1084 | } 1085 | err = a.sh.PubSubPublish(topicIssue, string(i)) 1086 | if err != nil { 1087 | log.Fatal(err) 1088 | } 1089 | 1090 | ctx.Dispatch(func(ctx app.Context) { 1091 | app.Window().Get("document").Call("querySelector", ".l-aside").Get("classList").Call("add", "is-collapsed") 1092 | a.createNotification(ctx, NotificationSuccess, SuccessHeader, "Solution submited.") 1093 | }) 1094 | }) 1095 | } 1096 | } 1097 | 1098 | func (a *acid) createNotification(ctx app.Context, s NotificationStatus, h string, msg string) { 1099 | a.notificationID++ 1100 | a.notifications[strconv.Itoa(a.notificationID)] = notification{ 1101 | id: a.notificationID, 1102 | status: string(s), 1103 | header: h, 1104 | message: msg, 1105 | } 1106 | 1107 | ntfs := a.notifications 1108 | ctx.Async(func() { 1109 | for n := range ntfs { 1110 | time.Sleep(5 * time.Second) 1111 | delete(ntfs, n) 1112 | ctx.Async(func() { 1113 | ctx.Dispatch(func(ctx app.Context) { 1114 | a.notifications = ntfs 1115 | }) 1116 | }) 1117 | } 1118 | }) 1119 | } 1120 | 1121 | // https://play.golang.org/p/Qg_uv_inCek 1122 | // contains checks if a string is present in a slice 1123 | func sliceContains(s []string, str string) bool { 1124 | for _, v := range s { 1125 | if v == str { 1126 | return true 1127 | } 1128 | } 1129 | 1130 | return false 1131 | } 1132 | 1133 | // The main function is the entry point where the app is configured and started. 1134 | // It is executed in 2 different environments: A client (the web browser) and a 1135 | // server. 1136 | func main() { 1137 | // The first thing to do is to associate the hello component with a path. 1138 | // 1139 | // This is done by calling the Route() function, which tells go-app what 1140 | // component to display for a given path, on both client and server-side. 1141 | app.Route("/", func() app.Composer{ 1142 | return &acid{} 1143 | }) 1144 | 1145 | // Once the routes set up, the next thing to do is to either launch the app 1146 | // or the server that serves the app. 1147 | // 1148 | // When executed on the client-side, the RunWhenOnBrowser() function 1149 | // launches the app, starting a loop that listens for app events and 1150 | // executes client instructions. Since it is a blocking call, the code below 1151 | // it will never be executed. 1152 | // 1153 | // When executed on the server-side, RunWhenOnBrowser() does nothing, which 1154 | // lets room for server implementation without the need for precompiling 1155 | // instructions. 1156 | app.RunWhenOnBrowser() 1157 | 1158 | // Finally, launching the server that serves the app is done by using the Go 1159 | // standard HTTP package. 1160 | // 1161 | // The Handler is an HTTP handler that serves the client and all its 1162 | // required resources to make it work into a web browser. Here it is 1163 | // configured to handle requests with a path that starts with "/". 1164 | 1165 | withGz := gziphandler.GzipHandler(&app.Handler{ 1166 | Name: "cyber-acid", 1167 | Description: "Cyber Acid - Liquid democracy politics simulator based on personal reputation index", 1168 | Styles: []string{ 1169 | "https://assets.ubuntu.com/v1/vanilla-framework-version-3.8.0.min.css", 1170 | "https://use.fontawesome.com/releases/v6.2.0/css/all.css", 1171 | }, 1172 | Scripts: []string{}, 1173 | }) 1174 | http.Handle("/", withGz) 1175 | 1176 | if err := http.ListenAndServe(":7000", nil); err != nil { 1177 | log.Fatal(err) 1178 | } 1179 | } 1180 | --------------------------------------------------------------------------------