├── .gitignore
├── README.md
├── app
├── .env-sample
├── .eslintrc.cjs
├── README.md
├── assets
│ ├── add-article-form.png
│ ├── editor.png
│ └── home-page.png
├── index.html
├── lua
│ ├── cms.lua
│ └── proxy.lua
├── package-lock.json
├── package.json
├── public
│ ├── ao.svg
│ ├── arweave.png
│ ├── atomic-asset.lua
│ ├── atomic-note.lua
│ ├── atomic-notes.png
│ ├── collection.lua
│ ├── cover.png
│ ├── github.svg
│ ├── icon.png
│ ├── logo.png
│ ├── x.png
│ └── x.svg
├── src
│ ├── App.css
│ ├── App.jsx
│ ├── components
│ │ ├── Header.jsx
│ │ ├── NoteCard.jsx
│ │ └── NotebookCard.jsx
│ ├── github-markdown.css
│ ├── lib
│ │ ├── svgs.jsx
│ │ └── utils.js
│ ├── main.jsx
│ └── pages
│ │ ├── Book.jsx
│ │ ├── CreateBook.jsx
│ │ ├── CreateNote.jsx
│ │ ├── Note.jsx
│ │ └── User.jsx
├── vite.config.js
└── yarn.lock
├── assets
├── cover.png
└── logo.png
├── logo
├── .gitignore
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── public
│ ├── ao.svg
│ ├── cover.png
│ ├── icon.png
│ └── svg-export.min.js
├── src
│ ├── App.jsx
│ ├── font.js
│ └── main.jsx
├── vite.config.js
└── yarn.lock
└── sdk
├── .babelrc-cjs
├── .babelrc-esm
├── .gitignore
├── .npmignore
├── README.md
├── make.js
├── package.json
├── scripts
└── upload_library.js
├── src
├── asset.js
├── assets
│ ├── banner.png
│ └── thumbnail.png
├── collection.js
├── dirname.js
├── helpers.js
├── index.js
├── lua
│ ├── aos-sqlite.wasm
│ ├── aos.wasm
│ ├── aos2.lua
│ ├── aos2_0_1.wasm
│ ├── atomic-asset.lua
│ ├── atomic-note-library.lua
│ ├── atomic-note.lua
│ ├── collection-registry.lua
│ ├── collection.lua
│ ├── notebook.lua
│ ├── profile.lua
│ ├── profile000.lua
│ ├── proxy.lua
│ ├── registry.lua
│ └── registry000.lua
├── note.js
├── notebook.js
├── profile.js
└── utils.js
├── test
├── README.md
└── index.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Atomic Notes
2 |
3 | 
4 |
5 | Atomic Note ia a new social primitive on AO, and a building block for horizontally scalable fully decentralized social applications.
6 |
7 | - [aoNote SDK Reference](./sdk)
8 | - [Testing Framework](./sdk/test)
9 |
--------------------------------------------------------------------------------
/app/.env-sample:
--------------------------------------------------------------------------------
1 | VITE_PROCESS_ID=6Z6aOJ7N2IJsVd7yNJrdw5eH_Ccy06cc7lWtu3SvhSA
2 | VITE_TITLE=Tomo | Permaweb Hacker
3 | VITE_DESCRIPTION=I hack, therefore I am.
4 | VITE_IMAGE=https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/cover.png
5 | VITE_ICON=https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/tomo.png
6 | VITE_PROFILE_NAME=Tomo
7 | VITE_PROFILE_DESCRIPTION=Permaweb Hacker
8 | VITE_PROFILE_IMAGE=https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/tomo.png
9 | VITE_PROFILE_X=0xTomo
10 | VITE_PROFILE_GITHUB=ocrybit
--------------------------------------------------------------------------------
/app/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react/jsx-no-target-blank': 'off',
16 | 'react-refresh/only-export-components': [
17 | 'warn',
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | # PermaCMS on Arweave/AO
2 |
3 | This is a permanent CMS deployed on Arweave and managed by AO.
4 |
5 | The SPA instance needs to be deployed once and articles will be updated by the assigned AO process.
6 |
7 | ***This is the very first alpha release, please expect bugs and use it with caution and managed.****
8 |
9 | ## Clone the Repo
10 |
11 | ```bash
12 | git clone https://github.com/ocrybit/perma-cms.git
13 | cd perma-cms
14 | npm install
15 | ```
16 |
17 | ## Prepare AOS Process
18 |
19 | In your terminal, go to the `lua` directory and start AOS.
20 |
21 | ```bash
22 | cd lua
23 | aos cms
24 | ```
25 |
26 | Then load the lua script to add handlers to your process.
27 |
28 | ```bash
29 | .load cms.lua
30 | ```
31 | You need to take note of the `ao process` txid and you need to make the same Arweave owner account available in your browser. The keyfile should be located at `.aos.json` in your home directory. You can import this to the [Arconnect](https://www.arconnect.io/) browser extension.
32 |
33 | you can quit the process with `Ctrl-C`. Now you are all set to run the CMS.
34 |
35 | ## `.env`
36 |
37 | You could update the site owner profile later, but due to the static website nature, predefining site details in `.env` will allow social media / search engines to get and display the site information.
38 |
39 | ```bash
40 | VITE_PROCESS_ID=6Z6aOJ7N2IJsVd7yNJrdw5eH_Ccy06cc7lWtu3SvhSA
41 | VITE_TITLE=Tomo | Permaweb Hacker
42 | VITE_DESCRIPTION=I hack, therefore I am.
43 | VITE_IMAGE=https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/cover.png
44 | VITE_ICON=https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/tomo.png
45 | VITE_PROFILE_NAME=Tomo
46 | VITE_PROFILE_DESCRIPTION=Permaweb Hacker
47 | VITE_PROFILE_IMAGE=https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/tomo.png
48 | VITE_PROFILE_X=0xTomo
49 | VITE_PROFILE_GITHUB=ocrybit
50 | ```
51 |
52 | ## Deploy on Arweave
53 |
54 | Check if everything works.
55 |
56 | ```bash
57 | yarn dev
58 | ```
59 |
60 | You could create and update articles at [http://localhost:5173/#/admin](http://localhost:5173/#/admin), or you can do that later.
61 |
62 | If everything is fine, build and deploy the app on Arweave.
63 |
64 | ```bash
65 | yarn build
66 | arkb deploy dist -w path_to_keyfile --auto-confirm
67 | ```
68 |
69 | Now you get the app URL like [https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U](https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U).
70 |
71 | ## Manage Articles with Built-in Editor
72 |
73 | You can go to the admin page at [https://your.app/#/admin](https://your.app/#/admin), and connect the owner wallet, and start writing articles with the simple built-in editor. You can perform the following actions.
74 |
75 | - Download the MD file
76 | - Upload articles to Arweave
77 | - Add articles to AO (CMS)
78 | - [bonus] Update your profile
79 |
80 | 
81 |
82 | ## Upload Markdown Files to Arewave
83 |
84 | In case of using the built-in editor, just hit the `Upload to Arweave` button.
85 |
86 | You could also import markdown files created with external editors like [HackMD](https://hackmd.io), and upload them to Arweave.
87 |
88 | You can use `arkb` to do so.
89 |
90 | ```bash
91 | arkb deploy doc.md -w path_to_keyfile --auto-confirm
92 | ```
93 |
94 | ## Add Articles to AO
95 |
96 | Then go to the admin page at [https://your.app/#/admin](https://your.app/#/admin) and add the `TxID` to your AO process with a `Title` and an arbitrary `Page ID`.
97 |
98 | Note that only the AO process owner can update, so you need to connect the same account that deployed the process.
99 |
100 | 
101 |
102 | Now you can access your article at [https://arweave.net/3W4...6U/#/a/3](https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/#/a/3).
103 |
104 | Also, it will be listed on the top page at [https://arweave.net/3W4...6U](https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U).
105 |
106 | 
107 |
--------------------------------------------------------------------------------
/app/assets/add-article-form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/assets/add-article-form.png
--------------------------------------------------------------------------------
/app/assets/editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/assets/editor.png
--------------------------------------------------------------------------------
/app/assets/home-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/assets/home-page.png
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %VITE_TITLE%
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/lua/cms.lua:
--------------------------------------------------------------------------------
1 | local json = require("json")
2 | local ao = require('ao')
3 |
4 | Articles = Articles or {}
5 | Profile = Profile or nil
6 |
7 | Handlers.add(
8 | "Set-Profile",
9 | Handlers.utils.hasMatchingTag("Action", "Set-Profile"),
10 | function (msg)
11 | assert(msg.From == Owner, 'only owner can execute!')
12 | assert(type(msg.name) == 'string', 'name is required!')
13 | local profile = { name = msg.name }
14 | if msg.description then
15 | profile.description = msg.description
16 | end
17 | if msg.x then
18 | profile.x = msg.x
19 | end
20 | if msg.github then
21 | profile.github = msg.github
22 | end
23 | if msg.image then
24 | profile.image = msg.image
25 | end
26 | if msg.cover then
27 | profile.cover = msg.cover
28 | end
29 | Profile = profile
30 | Handlers.utils.reply("profile updated!")(msg)
31 | end
32 | )
33 |
34 | Handlers.add(
35 | "Get-Profile",
36 | Handlers.utils.hasMatchingTag("Action", "Get-Profile"),
37 | function (msg)
38 | ao.send({
39 | Target = msg.From,
40 | Article = json.encode(Profile)
41 | })
42 | end
43 | )
44 |
45 | Handlers.add(
46 | "Add",
47 | Handlers.utils.hasMatchingTag("Action", "Add"),
48 | function (msg)
49 | assert(msg.From == Owner, 'only owner can execute!')
50 | assert(type(msg.title) == 'string', 'title is required!')
51 | assert(type(msg.id) == 'string', 'id is required!')
52 | assert(type(msg.txid) == 'string', 'txid is required!')
53 | assert(0 < tonumber(msg.date), 'date must be greater than 0')
54 | assert(not Articles[msg.id], 'article exists!')
55 | local article = {
56 | title = msg.title,
57 | txid = msg.txid,
58 | id = msg.id,
59 | date = tonumber(msg.date)
60 | }
61 |
62 | Articles[msg.id] = article
63 | Handlers.utils.reply("article added!")(msg)
64 | end
65 | )
66 |
67 | Handlers.add(
68 | "Update",
69 | Handlers.utils.hasMatchingTag("Action", "Update"),
70 | function (msg)
71 | assert(msg.From == Owner, 'only owner can execute!')
72 | assert(type(msg.txid) == 'string', 'txid is required!')
73 | assert(type(msg.id) == 'string', 'id is required!')
74 | assert(Articles[msg.id], 'article does not exist!')
75 | assert(0 < tonumber(msg.date), 'date must be greater than 0')
76 | local article = Articles[msg.id]
77 | if msg.title then
78 | assert(type(msg.title) == 'string', 'title must be string!')
79 | article.title = msg.title
80 | end
81 | article.txid = msg.txid
82 | article.update = tonumber(msg.date)
83 | Articles[msg.id] = article
84 | Handlers.utils.reply("article updated!")(msg)
85 | end
86 | )
87 |
88 | Handlers.add(
89 | "Delete",
90 | Handlers.utils.hasMatchingTag("Action", "Delete"),
91 | function (msg)
92 | assert(msg.From == Owner, 'only owner can execute!')
93 | assert(type(msg.id) == 'string', 'id is required!')
94 | assert(Articles[msg.id], 'article doesn\'t exist!')
95 | Articles[msg.id] = nil
96 | Handlers.utils.reply("article deleted!")(msg)
97 | end
98 | )
99 |
100 | Handlers.add(
101 | "List",
102 | Handlers.utils.hasMatchingTag("Action", "List"),
103 | function (msg)
104 | local arr = {}
105 | for _, article in pairs(Articles) do
106 | table.insert(arr, article)
107 | end
108 |
109 | local order = msg.order or "desc"
110 | local limit = msg.limit or #arr
111 | local skip = msg.skip or 0
112 |
113 | table.sort(arr, function(a, b)
114 | if order == "asc" then
115 | return a.date < b.date
116 | else
117 | return a.date > b.date
118 | end
119 | end)
120 |
121 | local start = skip + 1
122 | local finish = start + limit - 1
123 | finish = math.min(finish, #arr)
124 |
125 | local slicedArr = {}
126 | for i = start, finish do
127 | table.insert(slicedArr, arr[i])
128 | end
129 | local isNext = (finish < #arr) and "true" or "false"
130 | ao.send({
131 | Target = msg.From,
132 | Articles = json.encode(slicedArr),
133 | Count = tostring(#arr),
134 | Next = isNext
135 | })
136 | end
137 | )
138 |
139 | Handlers.add(
140 | "Get",
141 | Handlers.utils.hasMatchingTag("Action", "Get"),
142 | function (msg)
143 | assert(type(msg.id) == 'string', 'id is required!')
144 | ao.send({
145 | Target = msg.From,
146 | Article = json.encode(Articles[msg.id])
147 | })
148 | end
149 | )
150 |
151 |
--------------------------------------------------------------------------------
/app/lua/proxy.lua:
--------------------------------------------------------------------------------
1 | local ao = require("ao")
2 | Handlers.add(
3 | "allow",
4 | Handlers.utils.hasMatchingTag("Action", "Allow"),
5 | function (msg)
6 | ao.addAssignable({ From = msg.From })
7 | Handlers.utils.reply("allowed!")(msg)
8 | end
9 | )
10 |
11 | Handlers.add(
12 | "assign",
13 | Handlers.utils.hasMatchingTag("Type", "Process"),
14 | function (msg)
15 | assert(msg.From == msg.Owner, 'only process owner can execute!')
16 | assert(msg.Tags["Content-Type"] == "text/markdown" or msg.Tags["Content-Type"] == "text/plain", 'only markdown and text are allowed!')
17 | ao.send({
18 | Target = msg.Id,
19 | Data = msg.Data,
20 | Tags = { Action = "Assigned" }
21 | })
22 | end
23 | )
24 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tomo",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@chakra-ui/icons": "^2.1.1",
14 | "@chakra-ui/react": "^2.8.2",
15 | "@emotion/react": "^11.11.4",
16 | "@emotion/styled": "^11.11.5",
17 | "@fortawesome/free-brands-svg-icons": "^6.6.0",
18 | "@fortawesome/react-fontawesome": "^0.2.2",
19 | "@mdxeditor/editor": "^3.8.0",
20 | "@permaweb/aoconnect": "^0.0.56",
21 | "@wooorm/starry-night": "^3.4.0",
22 | "aonote": "^0.8.1",
23 | "arweave": "^1.15.1",
24 | "dayjs": "^1.11.11",
25 | "framer-motion": "^11.3.0",
26 | "hast-util-to-html": "^9.0.1",
27 | "localforage": "^1.10.0",
28 | "markdown-it": "^14.1.0",
29 | "marked": "^13.0.2",
30 | "react": "^18.3.1",
31 | "react-dom": "^18.3.1",
32 | "react-helmet": "^6.1.0",
33 | "react-router-dom": "^6.24.1",
34 | "vite-plugin-node-polyfills": "^0.22.0"
35 | },
36 | "devDependencies": {
37 | "@types/react": "^18.3.3",
38 | "@types/react-dom": "^18.3.0",
39 | "@vitejs/plugin-react-swc": "^3.5.0",
40 | "eslint": "^8.57.0",
41 | "eslint-plugin-react": "^7.34.2",
42 | "eslint-plugin-react-hooks": "^4.6.2",
43 | "eslint-plugin-react-refresh": "^0.4.7",
44 | "vite": "^5.3.1"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/public/ao.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/public/arweave.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/public/arweave.png
--------------------------------------------------------------------------------
/app/public/atomic-asset.lua:
--------------------------------------------------------------------------------
1 | local bint = require('.bint')(256)
2 | local json = require('json')
3 |
4 | if Name ~= '' then Name = '' end
5 | if Description ~= '' then Description = '' end
6 | if Thumbnail ~= '' then Thumbnail = '' end
7 | --if Collection ~= '' then Collection = '' end
8 | if Creator ~= '' then Creator = '' end
9 | if Ticker ~= '' then Ticker = '' end
10 | if Denomination ~= '' then Denomination = '' end
11 | if not Balances then Balances = { [''] = '' } end
12 | if DateCreated ~= '' then DateCreated = '' end
13 | if not Collections then Collections = {} end
14 |
15 | Transferable = true
16 |
17 | local function checkValidAddress(address)
18 | if not address or type(address) ~= 'string' then
19 | return false
20 | end
21 |
22 | return string.match(address, "^[%w%-_]+$") ~= nil and #address == 43
23 | end
24 |
25 | local function checkValidAmount(data)
26 | return (math.type(tonumber(data)) == 'integer' or math.type(tonumber(data)) == 'float') and bint(data) > 0
27 | end
28 |
29 | local function decodeMessageData(data)
30 | local status, decodedData = pcall(json.decode, data)
31 |
32 | if not status or type(decodedData) ~= 'table' then
33 | return false, nil
34 | end
35 |
36 | return true, decodedData
37 | end
38 |
39 | -- Read process state
40 | Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), function(msg)
41 | ao.send({
42 | Target = msg.From,
43 | Action = 'Read-Success',
44 | Data = json.encode({
45 | Name = Name,
46 | Description = Description,
47 | Ticker = Ticker,
48 | Denomination = Denomination,
49 | Balances = Balances,
50 | Transferable = Transferable,
51 | Thumbnail = Thumbnail,
52 | Collections = Collections
53 | })
54 | })
55 | end)
56 |
57 | -- Transfer balance to recipient (Data - { Recipient, Quantity })
58 | Handlers.add('Transfer', Handlers.utils.hasMatchingTag('Action', 'Transfer'), function(msg)
59 | if not Transferable then
60 | ao.send({ Target = msg.From, Action = 'Validation-Error', Tags = { Status = 'Error', Message = 'Transfers are not allowed' } })
61 | return
62 | end
63 |
64 | local data = {
65 | Recipient = msg.Tags.Recipient,
66 | Quantity = msg.Tags.Quantity
67 | }
68 |
69 | if checkValidAddress(data.Recipient) and checkValidAmount(data.Quantity) then
70 | -- Transfer is valid, calculate balances
71 | if not Balances[data.Recipient] then
72 | Balances[data.Recipient] = '0'
73 | end
74 |
75 | Balances[msg.From] = tostring(bint(Balances[msg.From]) - bint(data.Quantity))
76 | Balances[data.Recipient] = tostring(bint(Balances[data.Recipient]) + bint(data.Quantity))
77 |
78 | -- If new balance zeroes out then remove it from the table
79 | if bint(Balances[msg.From]) <= 0 then
80 | Balances[msg.From] = nil
81 | end
82 | if bint(Balances[data.Recipient]) <= 0 then
83 | Balances[data.Recipient] = nil
84 | end
85 |
86 | local debitNoticeTags = {
87 | Status = 'Success',
88 | Message = 'Balance transferred, debit notice issued',
89 | Recipient = msg.Tags.Recipient,
90 | Quantity = msg.Tags.Quantity,
91 | }
92 |
93 | local creditNoticeTags = {
94 | Status = 'Success',
95 | Message = 'Balance transferred, credit notice issued',
96 | Sender = msg.From,
97 | Quantity = msg.Tags.Quantity,
98 | }
99 |
100 | for tagName, tagValue in pairs(msg) do
101 | if string.sub(tagName, 1, 2) == 'X-' then
102 | debitNoticeTags[tagName] = tagValue
103 | creditNoticeTags[tagName] = tagValue
104 | end
105 | end
106 |
107 | -- Send a debit notice to the sender
108 | ao.send({
109 | Target = msg.From,
110 | Action = 'Debit-Notice',
111 | Tags = debitNoticeTags,
112 | Data = json.encode({
113 | Recipient = data.Recipient,
114 | Quantity = tostring(data.Quantity)
115 | })
116 | })
117 |
118 | -- Send a credit notice to the recipient
119 | ao.send({
120 | Target = data.Recipient,
121 | Action = 'Credit-Notice',
122 | Tags = creditNoticeTags,
123 | Data = json.encode({
124 | Sender = msg.From,
125 | Quantity = tostring(data.Quantity)
126 | })
127 | })
128 | end
129 | end)
130 |
131 | -- Mint new tokens (Data - { Quantity })
132 | Handlers.add('Mint', Handlers.utils.hasMatchingTag('Action', 'Mint'), function(msg)
133 | local decodeCheck, data = decodeMessageData(msg.Data)
134 |
135 | if decodeCheck and data then
136 | -- Check if quantity is present
137 | if not data.Quantity then
138 | ao.send({ Target = msg.From, Action = 'Input-Error', Tags = { Status = 'Error', Message = 'Invalid arguments, required { Quantity }' } })
139 | return
140 | end
141 |
142 | -- Check if quantity is a valid integer greater than zero
143 | if not checkValidAmount(data.Quantity) then
144 | ao.send({ Target = msg.From, Action = 'Validation-Error', Tags = { Status = 'Error', Message = 'Quantity must be an integer greater than zero' } })
145 | return
146 | end
147 |
148 | -- Check if owner is sender
149 | if msg.From ~= Owner then
150 | ao.send({ Target = msg.From, Action = 'Validation-Error', Tags = { Status = 'Error', Message = 'Only the process owner can mint new tokens' } })
151 | return
152 | end
153 |
154 | -- Mint request is valid, add tokens to the pool
155 | if not Balances[Owner] then
156 | Balances[Owner] = '0'
157 | end
158 |
159 | Balances[Owner] = tostring(bint(Balances[Owner]) + bint(data.Quantity))
160 |
161 | ao.send({ Target = msg.From, Action = 'Mint-Success', Tags = { Status = 'Success', Message = 'Tokens minted' } })
162 | else
163 | ao.send({
164 | Target = msg.From,
165 | Action = 'Input-Error',
166 | Tags = {
167 | Status = 'Error',
168 | Message = string.format('Failed to parse data, received: %s. %s', msg.Data,
169 | 'Data must be an object - { Quantity }')
170 | }
171 | })
172 | end
173 | end)
174 |
175 | -- Read balance (Data - { Target })
176 | Handlers.add('Balance', Handlers.utils.hasMatchingTag('Action', 'Balance'), function(msg)
177 | local decodeCheck, data = decodeMessageData(msg.Data)
178 |
179 | if decodeCheck and data then
180 | -- Check if target is present
181 | if not data.Target then
182 | ao.send({ Target = msg.From, Action = 'Input-Error', Tags = { Status = 'Error', Message = 'Invalid arguments, required { Target }' } })
183 | return
184 | end
185 |
186 | -- Check if target is a valid address
187 | if not checkValidAddress(data.Target) then
188 | ao.send({ Target = msg.From, Action = 'Validation-Error', Tags = { Status = 'Error', Message = 'Target is not a valid address' } })
189 | return
190 | end
191 |
192 | -- Check if target has a balance
193 | if not Balances[data.Target] then
194 | ao.send({ Target = msg.From, Action = 'Read-Error', Tags = { Status = 'Error', Message = 'Target does not have a balance' } })
195 | return
196 | end
197 |
198 | ao.send({
199 | Target = msg.From,
200 | Action = 'Read-Success',
201 | Tags = { Status = 'Success', Message = 'Balance received' },
202 | Data =
203 | Balances[data.Target]
204 | })
205 | else
206 | ao.send({
207 | Target = msg.From,
208 | Action = 'Input-Error',
209 | Tags = {
210 | Status = 'Error',
211 | Message = string.format('Failed to parse data, received: %s. %s', msg.Data,
212 | 'Data must be an object - { Target }')
213 | }
214 | })
215 | end
216 | end)
217 |
218 | -- Read balances
219 | Handlers.add('Balances', Handlers.utils.hasMatchingTag('Action', 'Balances'),
220 | function(msg) ao.send({ Target = msg.From, Action = 'Read-Success', Data = json.encode(Balances) }) end)
221 |
222 | -- Initialize a request to add the uploaded asset to a profile
223 | Handlers.add('Add-Asset-To-Profile', Handlers.utils.hasMatchingTag('Action', 'Add-Asset-To-Profile'), function(msg)
224 | if checkValidAddress(msg.Tags.ProfileProcess) then
225 | -- ao.assign({ Processes = { msg.Tags.ProfileProcess }, Message = ao.id })
226 | ao.send({
227 | Target = msg.Tags.ProfileProcess,
228 | Action = 'Add-Uploaded-Asset',
229 | Data = json.encode({
230 | Id = ao.id,
231 | Quantity = msg.Tags.Quantity or '0'
232 | })
233 | })
234 | else
235 | ao.send({
236 | Target = msg.From,
237 | Action = 'Input-Error',
238 | Tags = {
239 | Status = 'Error',
240 | Message = 'ProfileProcess tag not specified or not a valid Process ID'
241 | }
242 | })
243 | end
244 | end)
245 |
246 | Handlers.add('Add-To-Collection-Success', Handlers.utils.hasMatchingTag('Action', 'Add-To-Collection-Success'), function(msg)
247 | local exists = false
248 | for i, id in ipairs(Collections) do
249 | if id == msg.From then
250 | exists = true
251 | break
252 | end
253 | end
254 |
255 | if not exists then
256 | table.insert(Collections, msg.From)
257 | end
258 | end)
259 |
260 | Handlers.add('Remove-From-Collection-Success', Handlers.utils.hasMatchingTag('Action', 'Remove-From-Collection-Success'), function(msg)
261 | for i, id in ipairs(Collections) do
262 | if id == msg.From then
263 | table.remove(Collections, i)
264 | break
265 | end
266 | end
267 | end)
268 |
--------------------------------------------------------------------------------
/app/public/atomic-notes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/public/atomic-notes.png
--------------------------------------------------------------------------------
/app/public/collection.lua:
--------------------------------------------------------------------------------
1 | local json = require('json')
2 |
3 | if Name ~= '' then Name = '' end
4 | if Description ~= '' then Description = '' end
5 | if Creator ~= '' then Creator = '' end
6 | if Banner ~= '' then Banner = '' end
7 | if Thumbnail ~= '' then Thumbnail = '' end
8 |
9 | if DateCreated ~= '' then DateCreated = '' end
10 | if LastUpdate ~= '' then LastUpdate = '' end
11 |
12 | -- Assets: Id[]
13 | if not Assets then Assets = {} end
14 |
15 | local function decodeMessageData(data)
16 | local status, decodedData = pcall(json.decode, data)
17 |
18 | if not status or type(decodedData) ~= 'table' then
19 | return false, nil
20 | end
21 |
22 | return true, decodedData
23 | end
24 |
25 | local function assetExists(assetId)
26 | for _, id in ipairs(Assets) do
27 | if id == assetId then
28 | return true
29 | end
30 | end
31 | return false
32 | end
33 |
34 | local function checkValidAddress(address)
35 | if not address or type(address) ~= 'string' then
36 | return false
37 | end
38 |
39 | return string.match(address, "^[%w%-_]+$") ~= nil and #address == 43
40 | end
41 |
42 | Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), function(msg)
43 | ao.send({
44 | Target = msg.From,
45 | Data = json.encode({
46 | Name = Name,
47 | Description = Description,
48 | Creator = Creator,
49 | Banner = Banner,
50 | Thumbnail = Thumbnail,
51 | DateCreated = DateCreated,
52 | Assets = Assets
53 | })
54 | })
55 | end)
56 |
57 | -- Add or remove assets
58 | Handlers.add('Update-Assets', Handlers.utils.hasMatchingTag('Action', 'Update-Assets'), function(msg)
59 | if msg.From ~= Owner and msg.From ~= ao.id and msg.From ~= Creator then
60 | ao.send({
61 | Target = msg.From,
62 | Action = 'Authorization-Error',
63 | Tags = {
64 | Status = 'Error',
65 | Message = 'Unauthorized to access this handler'
66 | }
67 | })
68 | return
69 | end
70 |
71 | local decodeCheck, data = decodeMessageData(msg.Data)
72 |
73 | if decodeCheck and data then
74 | if not data.AssetIds or type(data.AssetIds) ~= 'table' or #data.AssetIds == 0 then
75 | ao.send({
76 | Target = msg.From,
77 | Action = 'Action-Response',
78 | Tags = {
79 | Status = 'Error',
80 | Message = 'Invalid or empty AssetIds list'
81 | }
82 | })
83 | return
84 | end
85 |
86 | if not data.UpdateType or (data.UpdateType ~= 'Add' and data.UpdateType ~= 'Remove') then
87 | ao.send({
88 | Target = msg.From,
89 | Action = 'Action-Response',
90 | Tags = {
91 | Status = 'Error',
92 | Message = 'UpdateType argument required (Add | Remove)'
93 | }
94 | })
95 | return
96 | end
97 |
98 | if data.UpdateType == 'Add' then
99 | for _, assetId in ipairs(data.AssetIds) do
100 | if not assetExists(assetId) then
101 | table.insert(Assets, assetId)
102 | ao.send({
103 | Target = assetId,
104 | Action = 'Add-To-Collection-Success',
105 | Tags = {}
106 | })
107 | end
108 | end
109 | end
110 |
111 | if data.UpdateType == 'Remove' then
112 | for _, assetId in ipairs(data.AssetIds) do
113 | for i, id in ipairs(Assets) do
114 | if id == assetId then
115 | table.remove(Assets, i)
116 | ao.send({
117 | Target = assetId,
118 | Action = 'Remove-From-Collection-Success',
119 | Tags = {}
120 | })
121 | break
122 | end
123 | end
124 | end
125 | end
126 |
127 | LastUpdate = msg.Timestamp
128 |
129 | ao.send({
130 | Target = msg.From,
131 | Action = 'Action-Response',
132 | Tags = {
133 | Status = 'Success',
134 | Message = 'Assets updated successfully'
135 | }
136 | })
137 | else
138 | ao.send({
139 | Target = msg.From,
140 | Action = 'Input-Error',
141 | Tags = {
142 | Status = 'Error',
143 | Message = string.format('Failed to parse data, received: %s. %s',
144 | msg.Data,
145 | 'Data must be an object - { AssetIds: [], UpdateType }')
146 | }
147 | })
148 | end
149 | end)
150 |
151 | -- Initialize a request to add the uploaded asset to a profile
152 | Handlers.add('Add-Collection-To-Profile', Handlers.utils.hasMatchingTag('Action', 'Add-Collection-To-Profile'), function(msg)
153 | if checkValidAddress(msg.Tags.ProfileProcess) then
154 | -- ao.assign({Processes = {msg.Tags.ProfileProcess}, Message = ao.id})
155 | ao.send({
156 | Target = msg.Tags.ProfileProcess,
157 | Action = 'Add-Collection',
158 | Data = json.encode({
159 | Id = ao.id,
160 | Name = Name
161 | })
162 | })
163 | else
164 | ao.send({
165 | Target = msg.From,
166 | Action = 'Input-Error',
167 | Tags = {
168 | Status = 'Error',
169 | Message = 'ProfileProcess tag not specified or not a valid Process ID'
170 | }
171 | })
172 | end
173 | end)
174 |
--------------------------------------------------------------------------------
/app/public/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/public/cover.png
--------------------------------------------------------------------------------
/app/public/github.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/public/icon.png
--------------------------------------------------------------------------------
/app/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/public/logo.png
--------------------------------------------------------------------------------
/app/public/x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/public/x.png
--------------------------------------------------------------------------------
/app/public/x.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/App.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | height:100%;
3 | coloir:#222326;
4 | }
5 |
6 | #root {
7 | height:100%;
8 | min-width: 100%;
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/app/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import { Image, Button, Flex, Box, useToast } from "@chakra-ui/react"
3 | import { Link } from "react-router-dom"
4 | import { map } from "ramda"
5 | import { getAddr, getProf } from "./lib/utils"
6 |
7 | import Header from "./components/Header"
8 | const roboto = {
9 | fontFamily: `"Roboto Mono", monospace`,
10 | fontOpticalSizing: "auto",
11 | fontWeight: 600,
12 | fontStyle: "normal",
13 | }
14 | function App() {
15 | const [address, setAddress] = useState(null)
16 | const [profile, setProfile] = useState(null)
17 | const [init, setInit] = useState(false)
18 | const t = useToast()
19 | const boxW = ["100%", null, "50%", 1 / 3]
20 | useEffect(() => getAddr({ setAddress, setInit, t }), [])
21 | useEffect(
22 | () => getProf({ address, setProfile, setInit, setAddress, t }),
23 | [address],
24 | )
25 |
26 | return (
27 | <>
28 |
31 |
32 |
33 |
34 |
35 | Atomic Notes
36 |
37 | A New Decentralized Social Primitive.
38 |
39 |
40 |
41 |
52 |
53 |
57 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
82 |
83 | Atomic Assets
84 |
85 |
86 | Data, licenses and smart contracts are all stored together on
87 | Arweave as tradable atomic assets on UCMs like{" "}
88 |
89 |
90 | BazAR
91 |
92 |
93 | .
94 |
95 |
96 |
97 |
98 |
105 |
106 | Universal Data License
107 |
108 |
109 | Profit sharing and royalty distribution are to be automated by
110 | Universal Content Marketplaces with onchain UDLs.
111 |
112 |
113 |
114 |
115 |
122 |
123 | Editable Notes on AO
124 |
125 |
126 | AO enables delta updates on permanent data, co-authoring, and
127 | semantic version control with AO processes / smart contracts.
128 |
129 |
130 |
131 |
132 |
139 |
140 | Built for Developers
141 |
142 |
143 | Atomic notes introduce a new social primitive built with a
144 | developer-first approach, providing an SDK and APIs.
145 |
146 |
147 |
148 |
149 |
156 |
157 | ArNS Integration
158 |
159 |
160 |
161 |
162 | ArNS
163 |
164 |
165 | domains provide censorship-resistant access to your content
166 | through thousands of decentralized gateways.
167 |
168 |
169 |
170 |
171 |
178 |
179 | Atomic Timelines
180 |
181 |
182 | Atomic Timelines are horizontally scalable social timelines
183 | built with Atomic Notes, distributing rewards to content
184 | creators.
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
196 |
201 |
202 |
203 |
204 |
209 |
210 |
211 |
212 |
213 | ONLY POSSIBLE ON ⓐ ARWEAVE
214 |
215 |
216 |
217 |
218 |
219 | >
220 | )
221 | }
222 |
223 | export default App
224 |
--------------------------------------------------------------------------------
/app/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Spinner,
4 | Image,
5 | Flex,
6 | Box,
7 | Menu,
8 | MenuButton,
9 | MenuList,
10 | MenuItem,
11 | MenuItemOption,
12 | MenuGroup,
13 | MenuOptionGroup,
14 | MenuDivider,
15 | } from "@chakra-ui/react"
16 | import {
17 | msg,
18 | err,
19 | ao,
20 | getAoProfile,
21 | getAddr,
22 | getProf,
23 | getPFP,
24 | } from "../lib/utils"
25 | import { useParams } from "react-router-dom"
26 | import { Link } from "react-router-dom"
27 | import { useState, useEffect } from "react"
28 | import lf from "localforage"
29 |
30 | function Header({
31 | children,
32 | address,
33 | profile,
34 | init,
35 | setAddress,
36 | setProfile,
37 | setInit,
38 | t,
39 | }) {
40 | return (
41 |
54 |
58 |
59 |
67 | {children ? (
68 | children
69 | ) : (
70 |
71 |
72 |
73 | Atomic Notes
74 |
75 |
76 | )}
77 |
78 | {!init ? (
79 |
80 | ) : profile ? (
81 | <>
82 |
156 | >
157 | ) : (
158 |
182 | )}
183 |
184 |
185 | )
186 | }
187 |
188 | export default Header
189 |
--------------------------------------------------------------------------------
/app/src/components/NoteCard.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Tag,
3 | IconButton,
4 | Menu,
5 | MenuButton,
6 | MenuList,
7 | MenuItem,
8 | Button,
9 | Tabs,
10 | TabList,
11 | Tab,
12 | TabPanels,
13 | TabPanel,
14 | Text,
15 | Flex,
16 | Input,
17 | Box,
18 | Image,
19 | Card,
20 | CardHeader,
21 | Heading,
22 | } from "@chakra-ui/react"
23 | import { validAddress, getPFP, gateway_url } from "../lib/utils"
24 | import { Link } from "react-router-dom"
25 | import { DeleteIcon, AddIcon, EditIcon } from "@chakra-ui/icons"
26 | import dayjs from "dayjs"
27 | import { map } from "ramda"
28 |
29 | const NoteCard = ({
30 | note,
31 | bazar = false,
32 | addToNotebook = () => {},
33 | deleteFromNotebook,
34 | profile,
35 | notebooks = [],
36 | nolinks,
37 | variant = "enclosed",
38 | navigate,
39 | diff = [],
40 | isCreator,
41 | bookmap = {},
42 | fileInputRef,
43 | thumb64,
44 | onChange,
45 | }) => {
46 | const props =
47 | variant === "enclosed"
48 | ? {
49 | mt: 6,
50 | mb: 2,
51 | p: 4,
52 | sx: { border: "1px solid #222326", borderRadius: "5px" },
53 | }
54 | : {
55 | p: 4,
56 | sx: {
57 | borderBottom: "1px solid rgb(226,232,240)",
58 | cursor: "pointer",
59 | },
60 | }
61 | return (
62 | {
66 | if (!nolinks) navigate(`/n/${note.id}`)
67 | }}
68 | >
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {note.title ?? "No Title"}
77 |
78 | {note.description}
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | {dayjs((note.date ?? Date.now()) * 1).format("MMM DD")}
88 |
89 | {
94 | e.stopPropagation()
95 | if (!nolinks) navigate(`/u/${profile.ProfileId}`)
96 | }}
97 | >
98 |
99 |
100 | {profile.DisplayName}
101 |
102 |
103 |
104 | {map(v => (
105 | {
114 | e.stopPropagation()
115 | if (!nolinks) navigate(`/b/${v.id}`)
116 | }}
117 | >
118 | {v.Name}
119 |
120 | ))(notebooks)}
121 |
122 |
123 | {nolinks || !isCreator || diff.length === 0 ? null : (
124 |
153 | )}
154 | {!bazar ? null : (
155 | <>
156 |
160 |
179 |
180 |
181 |
200 |
201 |
205 |
224 |
225 | >
226 | )}
227 | {nolinks || !isCreator ? null : (
228 |
248 | )}
249 | {nolinks || !isCreator || !deleteFromNotebook ? null : (
250 |
267 | )}
268 |
269 |
270 | {onChange ? (
271 | fileInputRef.current.click()}
287 | size="xl"
288 | align="center"
289 | justify="center"
290 | >
291 |
298 | {note.thumb64 || validAddress(note.thumbnail) ? null : (
299 |
300 | )}
301 |
302 | ) : !note.thumbnail ? null : (
303 |
317 | )}
318 |
319 | )
320 | }
321 |
322 | export default NoteCard
323 |
--------------------------------------------------------------------------------
/app/src/components/NotebookCard.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Tag,
3 | IconButton,
4 | Menu,
5 | MenuButton,
6 | MenuList,
7 | MenuItem,
8 | Button,
9 | Tabs,
10 | TabList,
11 | Tab,
12 | TabPanels,
13 | TabPanel,
14 | Text,
15 | Flex,
16 | Input,
17 | Box,
18 | Image,
19 | Card,
20 | CardHeader,
21 | Avatar,
22 | Heading,
23 | } from "@chakra-ui/react"
24 | import { validAddress, gateway_url, getThumb } from "../lib/utils"
25 | import { Link } from "react-router-dom"
26 | import { DeleteIcon, AddIcon, EditIcon } from "@chakra-ui/icons"
27 | import dayjs from "dayjs"
28 | import { map } from "ramda"
29 |
30 | const NotebookCard = ({
31 | bmap,
32 | note,
33 | bazar = false,
34 | addToNotebook = () => {},
35 | deleteFromNotebook,
36 | profile,
37 | notebooks = [],
38 | nolinks,
39 | variant = "enclosed",
40 | navigate,
41 | diff = [],
42 | isCreator,
43 | bookmap = {},
44 | fileInputRef,
45 | thumb64,
46 | onChange,
47 | }) => {
48 | const props =
49 | variant === "enclosed"
50 | ? {
51 | mt: 6,
52 | mb: 2,
53 | p: 4,
54 | sx: { border: "1px solid #222326", borderRadius: "5px" },
55 | }
56 | : {
57 | p: 4,
58 | sx: {
59 | borderBottom: "1px solid rgb(226,232,240)",
60 | cursor: "pointer",
61 | },
62 | }
63 | return (
64 |
65 |
66 |
67 |
68 | {onChange ? (
69 | fileInputRef.current.click()}
86 | size="xl"
87 | align="center"
88 | justify="center"
89 | >
90 |
97 | {note.thumb64 || validAddress(note.thumbnail) ? null : (
98 |
99 | )}
100 |
101 | ) : !note.thumbnail ? null : (
102 |
103 | )}
104 |
105 |
106 |
107 | {note.title}
108 |
109 | {note.description}
110 |
111 |
112 | {dayjs((note.date ?? Date.now()) * 1).format("MMM DD")}
113 |
114 |
120 | {note.assets?.length ?? 0} Notes
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | )
130 | }
131 |
132 | export default NotebookCard
133 |
--------------------------------------------------------------------------------
/app/src/lib/svgs.jsx:
--------------------------------------------------------------------------------
1 | const circleNotch = (
2 |
28 | )
29 |
30 | export { circleNotch }
31 |
--------------------------------------------------------------------------------
/app/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | import { dryrun } from "@permaweb/aoconnect"
2 | import lf from "localforage"
3 | import { fromPairs, map, prop, includes, clone } from "ramda"
4 | import { AR, Profile, Notebook, Note } from "aonote"
5 |
6 | let graphql_url = "https://arweave.net/graphql"
7 | let default_thumbnail = "9v2GrtXpVpPWf9KBuTBdClARjjcDA3NqxFn8Kbn1f2M"
8 | let default_banner = "UuEwLRmuNmqLTDcKqgcxDEV1CWIR_uZ6rxzmKjODlrg"
9 | let gateway_url = "https://arweave.net"
10 |
11 | const genOpt = () => {
12 | let env = { ar: {}, ao: {}, profile: {}, note: {}, notebook: {} }
13 | for (const k in import.meta.env) {
14 | if (k.match(/^VITE_/)) {
15 | const k2 = k.replace(/^VITE_/, "").toLowerCase()
16 | const k3 = k2.split("_")
17 | if (
18 | includes(k3[0], ["ar", "ao", "profile", "note", "notebook"]) &&
19 | import.meta.env[k].match(/^\s*$/) === null
20 | ) {
21 | env[k3[0]][k3.slice(1).join("_")] = import.meta.env[k]
22 | }
23 | }
24 | }
25 | let link = {
26 | ar: [],
27 | ao: ["module", "scheduler"],
28 | profile: ["registry", "src"],
29 | note: ["proxy", "src", "lib_src"],
30 | notebook: ["registry", "src"],
31 | }
32 | let namemap = {
33 | ar: {},
34 | ao: {},
35 | profile: { src: "profile_src" },
36 | note: { src: "note_src", lib_src: "notelib_src" },
37 | notebook: { src: "notebook_src" },
38 | }
39 | let opt = { ar: {}, ao: {}, profile: {}, note: {}, notebook: {} }
40 | opt.ar = env.ar
41 | for (const k in link) {
42 | for (const v of link[k]) {
43 | if (env[k][v]) opt[k][namemap[k][v] ?? v] = env[k][v]
44 | }
45 | }
46 | if (env.ao.mu || env.ao.cu || env.ao.gateway) {
47 | opt.ao.aoconnect = {}
48 | if (env.ao.mu) opt.ao.aoconnect.MU_URL = env.ao.mu
49 | if (env.ao.cu) opt.ao.aoconnect.CU_URL = env.ao.cu
50 | if (env.ao.gateway) opt.ao.aoconnect.GATEWAY_URL = env.ao.gateway
51 | }
52 | if (import.meta.env.VITE_THUMBNAIL) {
53 | opt.note.thumbnail = import.meta.env.VITE_THUMBNAIL
54 | opt.notebook.thumbnail = import.meta.env.VITE_THUMBNAIL
55 | }
56 | if (import.meta.env.VITE_BANNER) {
57 | opt.note.banner = import.meta.env.VITE_BANNER
58 | opt.notebook.banner = import.meta.env.VITE_BANNER
59 | }
60 |
61 | const _ar = clone(opt.ar)
62 | const _ao = clone(opt.ao)
63 | const _profile = clone(opt.profile)
64 | const _note = clone(opt.ntoe)
65 | const _notebook = clone(opt.ntoebook)
66 |
67 | opt.ao.ar = _ar
68 |
69 | opt.profile.ao = _ao
70 | opt.profile.ar = _ar
71 |
72 | opt.notebook.ao = _ao
73 | opt.notebook.ar = _ar
74 | opt.notebook.profile = _profile
75 |
76 | opt.note.ao = _ao
77 | opt.note.ar = _ar
78 | opt.note.profile = _profile
79 | return opt
80 | }
81 |
82 | const opt = genOpt()
83 |
84 | const _nb = new Notebook(opt.notebook)
85 |
86 | graphql_url = `${_nb.ar.protocol}://${_nb.ar.host}:${_nb.ar.port}/graphql`
87 | gateway_url = `${_nb.ar.protocol}://${_nb.ar.host}:${_nb.ar.port}`
88 | default_thumbnail = _nb.thumbnail
89 | default_banner = _nb.banner
90 |
91 | const getArticles = async ({ limit, skip } = {}) => {
92 | let tags = [{ name: "Action", value: "List" }]
93 | if (limit) tags.push({ name: "limit", value: limit.toString() })
94 | if (skip) tags.push({ name: "skip", value: skip.toString() })
95 | const result = await dryrun({
96 | process: import.meta.env.VITE_PROCESS_ID,
97 | tags,
98 | })
99 | return {
100 | articles: JSON.parse(result.Messages[0].Tags[6].value),
101 | next: JSON.parse(result.Messages[0].Tags[7].value),
102 | count: JSON.parse(result.Messages[0].Tags[8].value),
103 | }
104 | }
105 |
106 | const getProfile = async () => {
107 | const result = await dryrun({
108 | process: import.meta.env.VITE_PROCESS_ID,
109 | tags: [{ name: "Action", value: "Get-Profile" }],
110 | })
111 | return JSON.parse(result.Messages[0].Tags[6].value)
112 | }
113 |
114 | const ao =
115 | "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTEuOTciIHZpZXdCb3g9IjAgMCA0MjkgMjE0IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMCAyMTRINzEuMzc2M0w4NS45NDI5IDE3NC42MUw1My4xNjgxIDEwNy41TDAgMjE0WiIgZmlsbD0iYmxhY2siLz4KPHBhdGggZD0iTTE4OS4zNjYgMTYwLjc1TDEwOS45NzggMUw4NS45NDI5IDU1LjcwODlMMTYwLjk2MSAyMTRIMjE1TDE4OS4zNjYgMTYwLjc1WiIgZmlsbD0iYmxhY2siLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zMjIgMjE0QzM4MS4wOTQgMjE0IDQyOSAxNjYuMDk0IDQyOSAxMDdDNDI5IDQ3LjkwNTUgMzgxLjA5NCAwIDMyMiAwQzI2Mi45MDYgMCAyMTUgNDcuOTA1NSAyMTUgMTA3QzIxNSAxNjYuMDk0IDI2Mi45MDYgMjE0IDMyMiAyMTRaTTMyMiAxNzJDMzU3Ljg5OSAxNzIgMzg3IDE0Mi44OTkgMzg3IDEwN0MzODcgNzEuMTAxNSAzNTcuODk5IDQyIDMyMiA0MkMyODYuMTAxIDQyIDI1NyA3MS4xMDE1IDI1NyAxMDdDMjU3IDE0Mi44OTkgMjg2LjEwMSAxNzIgMzIyIDE3MloiIGZpbGw9ImJsYWNrIi8+Cjwvc3ZnPg=="
116 |
117 | const arweave_logo =
118 | "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMzQuOSAxMzUuNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTM0LjkgMTM1LjQ7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojMjIyMzI2O30KCS5zdDF7ZmlsbDpub25lO3N0cm9rZTojMjIyMzI2O3N0cm9rZS13aWR0aDo5LjY1MjE7c3Ryb2tlLW1pdGVybGltaXQ6MTA7fQo8L3N0eWxlPgo8Zz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik03Ny42LDkxLjVjLTAuMy0wLjYtMC42LTEuMy0wLjgtMi4xYy0wLjItMC44LTAuNC0xLjYtMC41LTIuNWMtMC43LDAuOC0xLjUsMS41LTIuNCwyLjIKCQljLTAuOSwwLjctMS45LDEuMy0zLDEuOGMtMS4xLDAuNS0yLjMsMC45LTMuNywxLjJjLTEuMywwLjMtMi44LDAuNC00LjQsMC40Yy0yLjUsMC00LjktMC40LTctMS4xYy0yLjEtMC43LTMuOS0xLjgtNS41LTMuMQoJCWMtMS41LTEuMy0yLjctMi45LTMuNi00LjdjLTAuOS0xLjgtMS4zLTMuOC0xLjMtNS45YzAtNS4yLDEuOS05LjMsNS44LTEyLjFjMy45LTIuOSw5LjctNC4zLDE3LjQtNC4zaDcuMXYtMi45CgkJYzAtMi40LTAuOC00LjMtMi4zLTUuN2MtMS42LTEuNC0zLjgtMi4xLTYuNy0yLjFjLTIuNiwwLTQuNSwwLjYtNS43LDEuN2MtMS4yLDEuMS0xLjgsMi42LTEuOCw0LjVINDYuNWMwLTIuMSwwLjUtNC4xLDEuNC02CgkJYzAuOS0xLjksMi4zLTMuNiw0LjEtNWMxLjgtMS40LDQtMi42LDYuNi0zLjRjMi42LTAuOCw1LjUtMS4zLDguOS0xLjNjMywwLDUuOCwwLjQsOC40LDEuMWMyLjYsMC43LDQuOCwxLjgsNi43LDMuMwoJCWMxLjksMS40LDMuNCwzLjIsNC40LDUuNGMxLjEsMi4yLDEuNiw0LjcsMS42LDcuNnYyMS4zYzAsMi43LDAuMiw0LjksMC41LDYuNmMwLjMsMS43LDAuOCwzLjIsMS41LDQuNXYwLjhINzcuNnogTTY1LjUsODIuNgoJCWMxLjMsMCwyLjUtMC4yLDMuNi0wLjVjMS4xLTAuMywyLjEtMC43LDMtMS4yYzAuOS0wLjUsMS42LTEsMi4zLTEuN2MwLjYtMC42LDEuMS0xLjMsMS41LTEuOXYtOC41aC02LjVjLTIsMC0zLjcsMC4yLTUuMSwwLjYKCQljLTEuNCwwLjQtMi42LDAuOS0zLjQsMS42Yy0wLjksMC43LTEuNSwxLjUtMiwyLjVjLTAuNCwxLTAuNiwyLTAuNiwzLjFjMCwxLjcsMC42LDMuMSwxLjgsNC4zQzYxLjIsODIsNjMsODIuNiw2NS41LDgyLjZ6Ii8+CjwvZz4KPGNpcmNsZSBjbGFzcz0ic3QxIiBjeD0iNjcuMyIgY3k9IjY4LjEiIHI9IjYxLjciLz4KPC9zdmc+Cg=="
119 |
120 | const defaultProfile = profile => {
121 | return (
122 | profile ?? {
123 | name: import.meta.env.VITE_PROFILE_NAME ?? "John Doe",
124 | description:
125 | import.meta.env.VITE_PROFILE_DESCRIPTION ?? "Set up your profile",
126 | image: import.meta.env.VITE_PROFILE_IMAGE ?? arweave_logo,
127 | x: import.meta.env.VITE_PROFILE_X ?? null,
128 | github: import.meta.env.VITE_PROFILE_GITHUB ?? null,
129 | }
130 | )
131 | }
132 | const action = value => tag("Action", value)
133 | const tag = (name, value) => ({ name, value })
134 | const wait = ms => new Promise(res => setTimeout(() => res(), ms))
135 |
136 | const getInfo = async prid => {
137 | const prof = new Profile(opt.profile)
138 | return await prof.info({ id: prid })
139 | }
140 |
141 | const getBookInfo = async pid => {
142 | const book = new Notebook({ pid, ...opt.notebook })
143 | return await book.info()
144 | }
145 |
146 | const getAoProfile = async address => {
147 | let prof = null
148 | const prid = await getProfileId(address)
149 | if (prid) {
150 | const _prof = new Profile(opt.profile)
151 | prof = await _prof.profile({ id: prid })
152 | }
153 | return prof
154 | }
155 |
156 | const getProfileId = async address => {
157 | const prof = new Profile(opt.profile)
158 | return (await prof.ids({ addr: address }))[0] ?? null
159 | }
160 |
161 | const getAoProfiles = async ids => {
162 | const prof = new Profile({ ...opt.profile })
163 | return await prof.profiles({ ids })
164 | }
165 |
166 | const getProf = ({ address, setAddress, setProfile, setInit, t }) => {
167 | ;(async () => {
168 | if (address) {
169 | let _profile = await lf.getItem(`profile-${address}`)
170 | let isCache = false
171 | if (_profile) {
172 | setProfile(_profile)
173 | setInit(true)
174 | isCache = true
175 | }
176 | _profile = await getAoProfile(address)
177 | setProfile(_profile)
178 | if (_profile) {
179 | await lf.setItem(`profile-${address}`, _profile)
180 | if (!isCache) msg(t, "Wallet Connected!")
181 | } else {
182 | const prof = await new Profile(opt.profile).init()
183 | err(
184 | t,
185 | "No Profile Found!",
186 | "You have no AO profile. Create one on BazAR.",
187 | )
188 | await lf.removeItem(`address`)
189 | setAddress(null)
190 | }
191 | setInit(true)
192 | }
193 | })()
194 | }
195 | const getAddr = ({ setAddress, setInit }) => {
196 | ;(async () => {
197 | const userAddress = await lf.getItem("address")
198 | if (userAddress) {
199 | setAddress(userAddress)
200 | } else {
201 | setInit(true)
202 | }
203 | })()
204 | }
205 |
206 | const getNotes = async pids => {
207 | const query = `query {
208 | transactions(ids: [${map(v => `"${v}"`)(pids).join(",")}], tags: { name: "Asset-Type", values: ["Atomic-Note"]}) {
209 | edges {
210 | node {
211 | id
212 | owner { address }
213 | tags { name value }
214 | }
215 | }
216 | }
217 | }`
218 | const res = await fetch(graphql_url, {
219 | method: "POST",
220 | headers: { "Content-Type": "application/json" },
221 | body: JSON.stringify({ query }),
222 | }).then(r => r.json())
223 | return map(prop("node"))(res?.data?.transactions?.edges ?? [])
224 | }
225 | const getBooks = async pids => {
226 | const query = `query {
227 | transactions(ids: [${map(v => `"${v}"`)(pids).join(",")}], tags: { name: "Collection-Type", values: ["Atomic-Notes"]}) {
228 | edges {
229 | node {
230 | id
231 | owner { address }
232 | tags { name value }
233 | }
234 | }
235 | }
236 | }`
237 | const res = await fetch(graphql_url, {
238 | method: "POST",
239 | headers: { "Content-Type": "application/json" },
240 | body: JSON.stringify({ query }),
241 | }).then(r => r.json())
242 | return map(prop("node"))(res?.data?.transactions?.edges ?? [])
243 | }
244 |
245 | const tags = tags => fromPairs(map(v => [v.name, v.value])(tags))
246 | const ltags = tags => fromPairs(map(v => [v.name.toLowerCase(), v.value])(tags))
247 | const badWallet = async (t, addr) => {
248 | let isValid = false
249 | try {
250 | await window.arweaveWallet.connect(["ACCESS_ADDRESS", "SIGN_TRANSACTION"])
251 | const addr2 = await window.arweaveWallet.getActiveAddress()
252 | isValid = addr === addr2
253 | } catch (e) {}
254 | if (!isValid)
255 | err(t, "The wrong wallet", `use ${addr} or reconnect the wallet.`)
256 | return !isValid
257 | }
258 | const validAddress = addr => /^[a-zA-Z0-9_-]{43}$/.test(addr)
259 |
260 | const gTag = (_tags, name) => {
261 | const __tags = tags(_tags)
262 | return __tags[name] ?? null
263 | }
264 |
265 | const tagEq = (tags, name, val = null) => {
266 | const _tags = gTag(tags, name)
267 | return _tags === val
268 | }
269 |
270 | const searchTag = (res, name, val) => {
271 | for (let v of res.Messages || []) {
272 | if (tagEq(v.Tags || {}, name, val)) return v
273 | }
274 | return null
275 | }
276 |
277 | const msg = (t, title = "Success!", description, status = "success") => {
278 | t({
279 | title,
280 | description,
281 | status,
282 | isClosable: true,
283 | })
284 | }
285 |
286 | const err = (
287 | t,
288 | title = "something went wrong!",
289 | description,
290 | status = "success",
291 | ) => msg(t, title, description, "error")
292 |
293 | const getPFP = profile =>
294 | profile.ProfileImage === "None"
295 | ? "./logo.png"
296 | : `${gateway_url}/${profile.ProfileImage}`
297 | const getThumb = book =>
298 | book.Thumbnail === "None" || !book.Thumbnail === "" || !book.Thumbnail
299 | ? "/logo.png"
300 | : `${gateway_url}/${book.Thumbnail}`
301 |
302 | const note_src_data = `if Name ~= '' then Name = '' end
303 | if Description ~= '' then Description = '' end
304 | if Thumbnail ~= '' then Thumbnail = '' end
305 | if Creator ~= '' then Creator = '' end
306 | if Ticker ~= '' then Ticker = '' end
307 | if Denomination ~= '' then Denomination = '' end
308 | if not Balances then Balances = { [''] = '' } end
309 | if DateCreated ~= '' then DateCreated = '' end
310 | if not Collections then Collections = {} end
311 |
312 | ao.addAssignable("LIBRARY", { Id = '' })
313 | `
314 | export {
315 | note_src_data,
316 | getThumb,
317 | default_thumbnail,
318 | default_banner,
319 | opt,
320 | err,
321 | msg,
322 | gTag,
323 | tagEq,
324 | searchTag,
325 | validAddress,
326 | badWallet,
327 | getBooks,
328 | ltags,
329 | tags,
330 | getNotes,
331 | getAddr,
332 | getAoProfile,
333 | getProf,
334 | wait,
335 | getArticles,
336 | getProfile,
337 | getProfileId,
338 | defaultProfile,
339 | ao,
340 | arweave_logo,
341 | action,
342 | tag,
343 | getInfo,
344 | getBookInfo,
345 | getPFP,
346 | graphql_url,
347 | gateway_url,
348 | getAoProfiles,
349 | }
350 |
--------------------------------------------------------------------------------
/app/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { RouterProvider, createHashRouter } from "react-router-dom"
3 | import { ChakraProvider } from "@chakra-ui/react"
4 | import ReactDOM from "react-dom/client"
5 | import App from "./App.jsx"
6 | import Note from "./pages/Note"
7 | import User from "./pages/User"
8 | import CreateNote from "./pages/CreateNote"
9 | import Book from "./pages/Book"
10 | import CreateBook from "./pages/CreateBook"
11 |
12 | const router = createHashRouter([
13 | {
14 | path: "/",
15 | element: ,
16 | },
17 | {
18 | path: "/u/:id",
19 | element: ,
20 | },
21 | {
22 | path: "/n/:id",
23 | element: ,
24 | },
25 | {
26 | path: "/b/:id",
27 | element: ,
28 | },
29 | {
30 | path: "/n/:pid/edit",
31 | element: ,
32 | },
33 | {
34 | path: "/b/:pid/edit",
35 | element: ,
36 | },
37 | ])
38 |
39 | ReactDOM.createRoot(document.getElementById("root")).render(
40 |
41 |
42 |
43 |
44 | ,
45 | )
46 |
--------------------------------------------------------------------------------
/app/src/pages/Book.jsx:
--------------------------------------------------------------------------------
1 | import { useNavigate, useParams } from "react-router-dom"
2 | import { Link } from "react-router-dom"
3 | import { useState, useEffect } from "react"
4 | import { Notebook } from "aonote"
5 | import Header from "../components/Header"
6 | import NoteCard from "../components/NoteCard"
7 | import { Helmet } from "react-helmet"
8 | import {
9 | getInfo,
10 | getNotes,
11 | getBookInfo,
12 | getAddr,
13 | getProf,
14 | ltags,
15 | tags,
16 | badWallet,
17 | msg,
18 | err,
19 | getPFP,
20 | opt,
21 | gateway_url,
22 | getThumb,
23 | } from "../lib/utils"
24 | import dayjs from "dayjs"
25 | import {
26 | Button,
27 | Tabs,
28 | TabList,
29 | Tab,
30 | TabPanels,
31 | TabPanel,
32 | Text,
33 | Flex,
34 | Box,
35 | Image,
36 | Card,
37 | CardHeader,
38 | Avatar,
39 | Heading,
40 | useToast,
41 | } from "@chakra-ui/react"
42 | import { o, sortBy, map, pluck, fromPairs, clone, reject } from "ramda"
43 |
44 | function User({}) {
45 | const { id } = useParams()
46 | const navigate = useNavigate()
47 | const t = useToast()
48 | const [address, setAddress] = useState(null)
49 | const [profile, setProfile] = useState(null)
50 | const [init, setInit] = useState(false)
51 | const [user, setUser] = useState(null)
52 | const [notes, setNotes] = useState([])
53 | const [book, setBook] = useState(null)
54 | const [assetmap, setAssetMap] = useState({})
55 |
56 | useEffect(() => getAddr({ setAddress, setInit, t }), [])
57 | useEffect(
58 | () => getProf({ address, setProfile, setInit, setAddress, t }),
59 | [address],
60 | )
61 |
62 | useEffect(() => {
63 | ;(async () => {
64 | const info = await getBookInfo(id)
65 | if (info) {
66 | setBook(info)
67 | setNotes(
68 | o(
69 | sortBy(v => v["date-created"] * -1),
70 | map(v => {
71 | return { ...ltags(v.tags), id: v.id, owner: v.owner.address }
72 | }),
73 | )(await getNotes(info.Assets)),
74 | )
75 | let _user = await getInfo(info.Creator)
76 | if (_user) setUser({ ..._user.Profile, id: info.Creator })
77 | }
78 | })()
79 | }, [id])
80 | const isCreator = book && book?.Creator === profile?.ProfileId
81 | return (
82 | <>
83 |
86 | {!book ? null : (
87 |
88 |
89 | {book.Name}
90 | {!user ? "" : ` | ${user.DisplayName}`}
91 |
92 |
93 |
94 |
98 |
99 |
103 |
107 |
108 |
112 |
113 | )}
114 |
115 | {!book ? null : (
116 |
117 |
118 | <>
119 | {!book.Banner ? null : (
120 |
131 | )}
132 |
133 |
134 |
135 |
136 |
142 |
143 | {book.Name}
144 |
145 | {book.Description}
146 |
147 | {!user ? null : (
148 |
149 |
150 |
151 | {dayjs(book["DateCreated"] * 1).format(
152 | "MMM DD",
153 | )}
154 |
155 |
156 | {user.DisplayName}
157 |
158 |
159 | )}
160 |
161 |
162 |
166 |
185 |
186 | {!isCreator ? null : (
187 |
188 |
203 |
204 | )}
205 |
206 |
207 |
208 |
209 |
210 |
211 | Notes
212 |
213 |
214 |
215 | {map(v => {
216 | let _note = v
217 | if (assetmap[v.id]) {
218 | _note.thumbnail = assetmap[v.id].Thumbnail
219 | }
220 |
221 | const deleteFromNotebook = v => async e => {
222 | e.preventDefault()
223 | e.stopPropagation()
224 | if (await badWallet(t, address)) return
225 | if (
226 | confirm(
227 | "Would you like to remove the note from this notebook?",
228 | )
229 | ) {
230 | const book = await new Notebook({
231 | ...opt.notebook,
232 | pid: id,
233 | }).init()
234 | const { err: _err } = await book.removeNote(
235 | v.id,
236 | true,
237 | )
238 | if (!_err) {
239 | let _notes = clone(notes)
240 | setNotes(reject(v2 => v2.id === v.id)(_notes))
241 | msg(t, "Note removed!")
242 | } else {
243 | err(t, "something went wrong")
244 | }
245 | }
246 | }
247 | return (
248 |
257 | )
258 | })(notes)}
259 |
260 | {book.Description}
261 |
262 |
263 | >
264 |
265 |
266 | )}
267 | >
268 | )
269 | }
270 |
271 | export default User
272 |
--------------------------------------------------------------------------------
/app/src/pages/Note.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react"
2 | import { useNavigate, useParams } from "react-router-dom"
3 | import { useToast, Flex, Box, Image, Spinner } from "@chakra-ui/react"
4 | import markdownIt from "markdown-it"
5 | import { Profile, Note } from "aonote"
6 | import { toHtml } from "hast-util-to-html"
7 | import "../github-markdown.css"
8 | import { common, createStarryNight } from "@wooorm/starry-night"
9 | import { Link } from "react-router-dom"
10 | import Header from "../components/Header"
11 | import NoteCard from "../components/NoteCard"
12 | import { msg, err, getNotes, tags, getAddr, getProf, opt } from "../lib/utils"
13 | import { Helmet } from "react-helmet"
14 |
15 | function Article(a) {
16 | const { id } = useParams()
17 | const navigate = useNavigate()
18 | const t = useToast()
19 | const [address, setAddress] = useState(null)
20 | const [profile, setProfile] = useState(null)
21 | const [init, setInit] = useState(false)
22 | const [error, setError] = useState(false)
23 | const [md, setMD] = useState(null)
24 | const [note, setNote] = useState(null)
25 | const [user, setUser] = useState(null)
26 | const [initNote, setInitNote] = useState(false)
27 | const [pubmap, setPubmap] = useState({})
28 | useEffect(() => getAddr({ setAddress, setInit, t }), [])
29 | useEffect(
30 | () => getProf({ address, setProfile, setInit, setAddress, t }),
31 | [address],
32 | )
33 | useEffect(() => {
34 | ;(async () => {
35 | const _note = new Note({ ...opt.note, pid: id })
36 | const res = await _note.info()
37 | if (res) {
38 | setNote({
39 | id: id,
40 | title: res.Name,
41 | description: res.Description,
42 | thumbnail: res.Thumbnail,
43 | collections: res.Collections || [],
44 | })
45 | let pubmap = {}
46 | for (let v of res.Collections || []) {
47 | const nb = new Note({ ...opt.note, pid: v })
48 | const info = await nb.info()
49 | if (info) pubmap[v] = info
50 | }
51 | setPubmap(pubmap)
52 | }
53 | })()
54 | }, [md])
55 |
56 | useEffect(() => {
57 | ;(async () => {
58 | try {
59 | const note = new Note({ pid: id, ...opt.note })
60 | const out = await note.get()
61 | if (out) {
62 | const _article = out.data
63 | try {
64 | const creator = tags((await getNotes([id]))[0]?.tags).Creator
65 | const _prof = new Profile(opt.profile)
66 | setUser(await _prof.profile({ id: creator }))
67 | } catch (e) {}
68 | const text = _article
69 | const starryNight = await createStarryNight(common)
70 | const markdownItInstance = markdownIt({
71 | highlight(value, lang) {
72 | const scope = starryNight.flagToScope(lang)
73 | return toHtml({
74 | type: "element",
75 | tagName: "pre",
76 | properties: {
77 | className: scope
78 | ? [
79 | "highlight",
80 | "highlight-" +
81 | scope.replace(/^source\./, "").replace(/\./g, "-"),
82 | ]
83 | : undefined,
84 | },
85 | children: scope
86 | ? /** @type {Array} */ (
87 | starryNight.highlight(value, scope).children
88 | )
89 | : [{ type: "text", value }],
90 | })
91 | },
92 | })
93 | const html = markdownItInstance.render(text)
94 | setMD(html)
95 | setError(false)
96 | }
97 | } catch (e) {
98 | setError(true)
99 | console.log(e)
100 | }
101 | setInitNote(true)
102 | })()
103 | }, [id])
104 | const isCreator = user && user?.ProfileId === profile?.ProfileId
105 | let notebooks = []
106 | for (let v of note?.collections || []) {
107 | if (pubmap[v]) notebooks.push({ ...pubmap[v], id: v })
108 | }
109 | return (
110 | <>
111 |
114 | {!note ? null : (
115 |
116 |
117 | {note.title}
118 | {!user ? "" : ` | ${user.DisplayName}`}
119 |
120 |
121 |
122 |
126 |
127 |
131 |
135 |
136 |
140 |
141 | )}
142 |
150 |
151 | {error ? (
152 |
160 |
161 | something went wrong
162 |
163 | ) : !initNote ? (
164 |
172 |
173 | Fetching Note...
174 |
175 | ) : (
176 |
183 | )}
184 | {!note || !user ? null : (
185 |
186 |
194 |
195 | )}
196 |
197 |
198 | >
199 | )
200 | }
201 |
202 | export default Article
203 |
--------------------------------------------------------------------------------
/app/src/pages/User.jsx:
--------------------------------------------------------------------------------
1 | import { useNavigate, useParams } from "react-router-dom"
2 | import { Notebook } from "aonote"
3 | import { Link } from "react-router-dom"
4 | import { useState, useEffect } from "react"
5 | import Header from "../components/Header"
6 | import NoteCard from "../components/NoteCard"
7 | import lf from "localforage"
8 | import {
9 | getBooks,
10 | getNotes,
11 | getInfo,
12 | getBookInfo,
13 | getAddr,
14 | getProf,
15 | ltags,
16 | tags,
17 | badWallet,
18 | msg,
19 | err,
20 | getPFP,
21 | opt,
22 | gateway_url,
23 | getThumb,
24 | } from "../lib/utils"
25 | import dayjs from "dayjs"
26 | import {
27 | useToast,
28 | Tag,
29 | IconButton,
30 | Menu,
31 | MenuButton,
32 | MenuList,
33 | MenuItem,
34 | Button,
35 | Tabs,
36 | TabList,
37 | Tab,
38 | TabPanels,
39 | TabPanel,
40 | Text,
41 | Flex,
42 | Box,
43 | Image,
44 | Card,
45 | CardHeader,
46 | Avatar,
47 | Heading,
48 | } from "@chakra-ui/react"
49 | import {
50 | indexOf,
51 | without,
52 | o,
53 | sortBy,
54 | map,
55 | pluck,
56 | fromPairs,
57 | clone,
58 | difference,
59 | } from "ramda"
60 |
61 | function User({}) {
62 | const { id } = useParams()
63 | const navigate = useNavigate()
64 | const t = useToast()
65 | const [address, setAddress] = useState(null)
66 | const [profile, setProfile] = useState(null)
67 | const [init, setInit] = useState(false)
68 | const [user, setUser] = useState(null)
69 | const [notes, setNotes] = useState([])
70 | const [books, setBooks] = useState([])
71 | const [bookmap, setBookMap] = useState({})
72 | const [notemap, setNoteMap] = useState({})
73 | const [assetmap, setAssetMap] = useState({})
74 |
75 | useEffect(() => getAddr({ setAddress, setInit, t }), [])
76 | useEffect(
77 | () => getProf({ address, setProfile, setInit, setAddress, t }),
78 | [address],
79 | )
80 |
81 | useEffect(() => {
82 | ;(async () => {
83 | const info = await getInfo(id)
84 | if (info) {
85 | setUser(info.Profile)
86 | setNotes(
87 | o(
88 | sortBy(v => v["date-created"] * -1),
89 | map(v => {
90 | return { ...ltags(v.tags), id: v.id, owner: v.owner.address }
91 | }),
92 | )(await getNotes(pluck("Id", info.Assets))),
93 | )
94 | setBooks(
95 | o(
96 | sortBy(v => v["date-created"] * -1),
97 | map(v => {
98 | return { ...ltags(v.tags), id: v.id, owner: v.owner.address }
99 | }),
100 | )(await getBooks(pluck("Id", info.Collections))),
101 | )
102 | }
103 | })()
104 | }, [id])
105 |
106 | useEffect(() => {
107 | ;(async () => {
108 | let bookmap = {}
109 | let notemap = {}
110 | for (let v of books) {
111 | const _info = await lf.getItem(`notebook-${v.id}`)
112 | if (_info) {
113 | bookmap[v.id] = _info
114 | for (let v2 of bookmap[v.id].Assets || []) {
115 | notemap[v2] ??= []
116 | notemap[v2].push(v.id)
117 | setNoteMap(notemap)
118 | }
119 | }
120 | setBookMap(bookmap)
121 | }
122 | for (let v of books) {
123 | const info = await getBookInfo(v.id)
124 | if (info) await lf.setItem(`notebook-${v.id}`, info)
125 | let exists = []
126 | for (let k in notemap) {
127 | for (let v2 of notemap[k]) {
128 | if (v2 === v.id) exists.push(k)
129 | }
130 | }
131 | const diff = difference(exists, info.Assets)
132 | bookmap[v.id] = info
133 | for (let v2 of diff) {
134 | notemap[v2] ??= []
135 | notemap[v2] = without([v.id], notemap[v2])
136 | }
137 | for (let v2 of bookmap[v.id].Assets || []) {
138 | notemap[v2] ??= []
139 | if (indexOf(v.id, notemap[v2]) === -1) {
140 | notemap[v2].push(v.id)
141 | setNoteMap(notemap)
142 | }
143 | }
144 | setBookMap(bookmap)
145 | }
146 | })()
147 | }, [books])
148 | const isCreator = id === profile?.ProfileId
149 | return (
150 | <>
151 |
154 | {!user ? null : (
155 |
156 |
157 | <>
158 |
159 |
160 |
161 |
162 |
167 |
168 |
169 | {user.DisplayName}
170 | {user.Description}
171 |
172 |
173 | {!isCreator ? null : (
174 |
178 |
193 |
194 | )}
195 |
196 |
197 |
198 |
199 |
200 |
201 | Notes
202 | Notebooks
203 |
204 |
205 |
206 | {map(v => {
207 | const _books = notemap[v.id] ?? []
208 | const bids = pluck("id", books)
209 | const diff = difference(bids, _books)
210 | let notebooks = []
211 | for (let v2 of notemap[v.id] ?? []) {
212 | if (bookmap[v2])
213 | notebooks.push({ id: v2, ...bookmap[v2] })
214 | }
215 | let _note = v
216 | if (assetmap[v.id]) {
217 | _note.thumbnail = assetmap[v.id].Thumbnail
218 | }
219 | const addToNotebook = v3 => async () => {
220 | if (await badWallet(t, address)) return
221 | const book = await new Notebook({
222 | ...opt.notebook,
223 | pid: v3,
224 | }).init()
225 |
226 | const { err: _err } = await book.addNote(v.id)
227 | if (!_err) {
228 | let _map = clone(notemap)
229 | let _bmap = clone(bookmap)
230 | _map[v.id] ??= []
231 | _map[v.id].push(v3)
232 | setNoteMap(_map)
233 | if (_bmap[v3]) {
234 | _bmap[v3].Assets.push(v.id)
235 | setBookMap(_bmap)
236 | }
237 | msg(t, "Note added!")
238 | } else {
239 | err(t, "something went wrong")
240 | }
241 | }
242 | return (
243 | <>
244 |
255 | >
256 | )
257 | })(notes)}
258 |
259 |
260 | {map(v => {
261 | let bmap = bookmap[v.id]
262 | return (
263 |
264 |
272 |
273 |
274 |
275 | {!bmap ? null : (
276 |
281 | )}
282 |
288 |
289 |
290 | {v.title}
291 |
292 | {v.description}
293 |
294 |
295 | {dayjs(v["date-created"] * 1).format(
296 | "MMM DD",
297 | )}
298 |
299 |
300 | {!bmap ? null : (
301 |
307 | {bmap.Assets.length} Notes
308 |
309 | )}
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 | )
319 | })(books)}
320 |
321 |
322 |
323 | >
324 |
325 |
326 | )}
327 | >
328 | )
329 | }
330 |
331 | export default User
332 |
--------------------------------------------------------------------------------
/app/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite"
2 | import { nodePolyfills } from "vite-plugin-node-polyfills"
3 | import react from "@vitejs/plugin-react-swc"
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | base: "./",
8 | plugins: [nodePolyfills(), react()],
9 | build: { chunkSizeWarningLimit: 10000 },
10 | })
11 |
--------------------------------------------------------------------------------
/assets/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/assets/cover.png
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/assets/logo.png
--------------------------------------------------------------------------------
/logo/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .vercel
26 |
--------------------------------------------------------------------------------
/logo/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import react from 'eslint-plugin-react'
4 | import reactHooks from 'eslint-plugin-react-hooks'
5 | import reactRefresh from 'eslint-plugin-react-refresh'
6 |
7 | export default [
8 | {
9 | files: ['**/*.{js,jsx}'],
10 | ignores: ['dist'],
11 | languageOptions: {
12 | ecmaVersion: 2020,
13 | globals: globals.browser,
14 | parserOptions: {
15 | ecmaVersion: 'latest',
16 | ecmaFeatures: { jsx: true },
17 | sourceType: 'module',
18 | },
19 | },
20 | settings: { react: { version: '18.3' } },
21 | plugins: {
22 | react,
23 | 'react-hooks': reactHooks,
24 | 'react-refresh': reactRefresh,
25 | },
26 | rules: {
27 | ...js.configs.recommended.rules,
28 | ...react.configs.recommended.rules,
29 | ...react.configs['jsx-runtime'].rules,
30 | ...reactHooks.configs.recommended.rules,
31 | 'react/jsx-no-target-blank': 'off',
32 | 'react-refresh/only-export-components': [
33 | 'warn',
34 | { allowConstantExport: true },
35 | ],
36 | },
37 | },
38 | ]
39 |
--------------------------------------------------------------------------------
/logo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Logo | Arweave Japan
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/logo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "logo",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@chakra-ui/react": "^2.8.2",
14 | "@emotion/react": "^11.13.0",
15 | "@emotion/styled": "^11.13.0",
16 | "framer-motion": "^11.3.24",
17 | "ramda": "^0.30.1",
18 | "react": "^18.3.1",
19 | "react-dom": "^18.3.1",
20 | "react-router-dom": "^6.26.1"
21 | },
22 | "devDependencies": {
23 | "@eslint/js": "^9.8.0",
24 | "@types/react": "^18.3.3",
25 | "@types/react-dom": "^18.3.0",
26 | "@vitejs/plugin-react-swc": "^3.5.0",
27 | "eslint": "^9.8.0",
28 | "eslint-plugin-react": "^7.35.0",
29 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
30 | "eslint-plugin-react-refresh": "^0.4.9",
31 | "globals": "^15.9.0",
32 | "vite": "^5.4.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/logo/public/ao.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/logo/public/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/logo/public/cover.png
--------------------------------------------------------------------------------
/logo/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/logo/public/icon.png
--------------------------------------------------------------------------------
/logo/public/svg-export.min.js:
--------------------------------------------------------------------------------
1 | (function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):(t="undefined"!=typeof globalThis?globalThis:t||self,e(t.svgExport=t.svgExport||{}))})(this,function(t){"use strict";function e(t){void 0!==typeof console&&"function"==typeof console.warn&&console.warn(t)}function n(t){var n=document.createElement("div");if(n.className="tempdiv-svg-exportJS","string"==typeof t&&(n.insertAdjacentHTML("beforeend",t.trim()),t=n.firstChild),!t.nodeType||1!==t.nodeType)return e("Error svg-export: The input svg was not recognized"),null;var i=t.cloneNode(!0);return i.style.display=null,n.appendChild(i),n.style.visibility="hidden",n.style.display="table",n.style.position="absolute",document.body.appendChild(n),i}function i(t){t&&t.pdfOptions&&(Object.keys(y.pdfOptions).forEach(function(e){if(t.pdfOptions.hasOwnProperty(e)&&typeof t.pdfOptions[e]==typeof y.pdfOptions[e]){if(""===t.pdfOptions[e])return;y.pdfOptions[e]=t.pdfOptions[e]}}),y.pdfOptions.pageLayout.margin||(y.pdfOptions.pageLayout.margin=50),y.pdfOptions.pageLayout.margins||(y.pdfOptions.pageLayout.margins={})),y.pdfOptions.pageLayout.margins.top=y.pdfOptions.pageLayout.margins.top||y.pdfOptions.pageLayout.margin,y.pdfOptions.pageLayout.margins.bottom=y.pdfOptions.pageLayout.margins.bottom||y.pdfOptions.pageLayout.margin,y.pdfOptions.pageLayout.margins.left=y.pdfOptions.pageLayout.margins.left||y.pdfOptions.pageLayout.margin,y.pdfOptions.pageLayout.margins.right=y.pdfOptions.pageLayout.margins.top||y.pdfOptions.pageLayout.margin,delete y.pdfOptions.pageLayout.margin,t&&y.pdfOptions.pageLayout.size||(y.pdfOptions.pageLayout.size=[Math.max(300,y.width)+y.pdfOptions.pageLayout.margins.left+y.pdfOptions.pageLayout.margins.right,Math.max(300,y.height)+y.pdfOptions.pageLayout.margins.top+y.pdfOptions.pageLayout.margins.bottom+(y.pdfOptions.addTitleToPage?2*y.pdfOptions.pdfTitleFontSize+10:0)+(""!==y.pdfOptions.chartCaption?4*y.pdfOptions.pdfCaptionFontSize+10:0)])}function o(t,e){y={originalWidth:100,originalHeight:100,originalMinXViewBox:0,originalMinYViewBox:0,width:100,height:100,scale:1,useCSS:!0,transparentBackgroundReplace:"white",allowCrossOriginImages:!1,elementsToExclude:[],pdfOptions:{customFonts:[],pageLayout:{margin:50,margins:{}},addTitleToPage:!0,chartCaption:"",pdfTextFontFamily:"Helvetica",pdfTitleFontSize:20,pdfCaptionFontSize:14},onDone:null},y.originalHeight=-1!==t.style.getPropertyValue("height").indexOf("%")||t.getAttribute("height")&&-1!==t.getAttribute("height").indexOf("%")?t.getBBox().height*y.scale:t.getBoundingClientRect().height*y.scale,y.originalWidth=-1!==t.style.getPropertyValue("width").indexOf("%")||t.getAttribute("width")&&-1!==t.getAttribute("width").indexOf("%")?t.getBBox().width*y.scale:t.getBoundingClientRect().width*y.scale,y.originalMinXViewBox=t.getAttribute("viewBox")?t.getAttribute("viewBox").split(/\s/)[0]:0,y.originalMinYViewBox=t.getAttribute("viewBox")?t.getAttribute("viewBox").split(/\s/)[1]:0,e&&e.scale&&"number"==typeof e.scale&&(y.scale=e.scale),e&&e.height?"number"==typeof e.height&&(y.height=e.height*y.scale):y.height=y.originalHeight*y.scale,e&&e.width?"number"==typeof e.width&&(y.width=e.width*y.scale):y.width=y.originalWidth*y.scale,e&&!1===e.useCSS&&(y.useCSS=!1),e&&e.transparentBackgroundReplace&&(y.transparentBackgroundReplace=e.transparentBackgroundReplace),e&&e.allowCrossOriginImages&&(y.allowCrossOriginImages=e.allowCrossOriginImages),e&&e.excludeByCSSSelector&&"string"==typeof e.excludeByCSSSelector&&(y.elementsToExclude=t.querySelectorAll(e.excludeByCSSSelector)),e&&e.onDone&&"function"==typeof e.onDone&&(y.onDone=e.onDone),i(e)}function a(t,n){if("function"==typeof getComputedStyle){for(var i=0;i0)for(const t of o)-1===["width","height","inline-size","block-size"].indexOf(t)&&n.style.setProperty(t,o.getPropertyValue(t));t.childNodes.forEach(function(t,e){1===t.nodeType&&a(t,n.childNodes[parseInt(e,10)])})}else e("Warning svg-export: this browser is not able to get computed styles")}function r(t,e,n){void 0===n&&(n=!0),y.useCSS&&"object"==typeof e&&(a(e,t),t.style.display=null),y.elementsToExclude.forEach(function(t){t.remove()}),t.style.width=null,t.style.height=null,t.setAttribute("width",y.width),t.setAttribute("height",y.height),t.setAttribute("preserveAspectRatio","none"),t.setAttribute("viewBox",y.originalMinXViewBox+" "+y.originalMinYViewBox+" "+y.originalWidth+" "+y.originalHeight);for(var i=document.getElementsByClassName("tempdiv-svg-exportJS");i.length>0;)i[0].parentNode.removeChild(i[0]);if(n){var o=new XMLSerializer,r=o.serializeToString(t).replace(/currentColor/g,"black");return r.match(/^