├── site ├── public │ ├── logo.png │ ├── dream.jpg │ ├── favicon.png │ ├── logo-ao.png │ ├── logo-exp.png │ ├── logo-typr.png │ ├── logo-usda.png │ ├── logo-war.png │ ├── logo-0rbit.jpg │ ├── logo-trunk.png │ ├── banner-default.png │ ├── logo-ao-token.png │ ├── logo-trunk-black.png │ ├── index.html │ └── ar.svg ├── src │ ├── server │ │ ├── server.ts │ │ └── service.ts │ ├── index.tsx │ └── app │ │ ├── elements │ │ ├── externalEmbed.css │ │ ├── Loading.tsx │ │ ├── externalEmbed.tsx │ │ ├── NavBar.tsx │ │ ├── StoryCard.css │ │ ├── NavBar.css │ │ ├── Portrait.css │ │ ├── PostContent.css │ │ ├── NotiCard.css │ │ ├── StoryCard.tsx │ │ ├── NavBarButton.tsx │ │ ├── ActivityPost.css │ │ ├── SharedQuillEditor.tsx │ │ ├── NotiCard.tsx │ │ └── Portrait.tsx │ │ ├── pages │ │ ├── NotFoundPage.tsx │ │ ├── NotiPage.css │ │ ├── BookmarksPage.css │ │ ├── GamesPage.css │ │ ├── GamesPage.tsx │ │ ├── FollowPage.css │ │ ├── StoryPage.css │ │ ├── SitePage.css │ │ ├── ActivityPostPage.css │ │ ├── TokenPage.css │ │ ├── HomePage.css │ │ ├── BookmarksPage.tsx │ │ ├── ChatPage.css │ │ ├── TokenPage.tsx │ │ ├── ProfilePage.css │ │ ├── NotiPage.tsx │ │ ├── HomePage.tsx │ │ ├── SitePage.tsx │ │ ├── FollowPage.tsx │ │ ├── StoryPage.tsx │ │ ├── ChatPage.tsx │ │ └── ActivityPostPage.tsx │ │ ├── util │ │ ├── event.js │ │ └── consts.ts │ │ ├── modals │ │ ├── ViewImageModal.css │ │ ├── MessageModal.tsx │ │ ├── PostModal.tsx │ │ ├── AlertModal.tsx │ │ ├── BountyRecordsModal.css │ │ ├── QuestionModal.tsx │ │ ├── Modal.js │ │ ├── LoginModal.css │ │ ├── ViewImageModal.tsx │ │ ├── LoginModal.tsx │ │ ├── EditProfileModal.css │ │ ├── Modal.css │ │ ├── BountyRecordsModal.tsx │ │ ├── BountyModal.css │ │ ├── DMModal.tsx │ │ ├── BountyModal.tsx │ │ └── EditProfileModal.tsx │ │ ├── AppConfig.tsx │ │ ├── App.tsx │ │ └── App.css ├── tsconfig.json ├── config-overrides.js └── package.json ├── README.md ├── .gitignore ├── lua ├── twitter.lua └── ao-story.lua └── LICENSE /site/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/logo.png -------------------------------------------------------------------------------- /site/public/dream.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/dream.jpg -------------------------------------------------------------------------------- /site/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/favicon.png -------------------------------------------------------------------------------- /site/public/logo-ao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/logo-ao.png -------------------------------------------------------------------------------- /site/public/logo-exp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/logo-exp.png -------------------------------------------------------------------------------- /site/public/logo-typr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/logo-typr.png -------------------------------------------------------------------------------- /site/public/logo-usda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/logo-usda.png -------------------------------------------------------------------------------- /site/public/logo-war.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/logo-war.png -------------------------------------------------------------------------------- /site/public/logo-0rbit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/logo-0rbit.jpg -------------------------------------------------------------------------------- /site/public/logo-trunk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/logo-trunk.png -------------------------------------------------------------------------------- /site/public/banner-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/banner-default.png -------------------------------------------------------------------------------- /site/public/logo-ao-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/logo-ao-token.png -------------------------------------------------------------------------------- /site/public/logo-trunk-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamgamelover/typr/HEAD/site/public/logo-trunk-black.png -------------------------------------------------------------------------------- /site/src/server/server.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "./service"; 2 | 3 | export class Server { 4 | public static service: Service = new Service(); 5 | } -------------------------------------------------------------------------------- /site/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import App from './app/App'; 3 | 4 | const root = ReactDOM.createRoot( 5 | document.getElementById('root') as HTMLElement 6 | ); 7 | 8 | root.render( 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /site/src/app/elements/externalEmbed.css: -------------------------------------------------------------------------------- 1 | .embed-container { 2 | position: relative; 3 | overflow: hidden; 4 | padding-top: 56.25%; /* 16:9 Aspect Ratio */ 5 | width: 100%; 6 | height: 0; 7 | } 8 | 9 | .embed-container iframe.responsive-iframe { 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | width: 100%; 14 | height: 100%; 15 | border: 0; 16 | } -------------------------------------------------------------------------------- /site/src/app/pages/NotFoundPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './GamesPage.css'; 3 | 4 | class NotFoundPage extends React.Component { 5 | render() { 6 | return ( 7 |
8 |
9 | Stay tuned. 10 |
11 |
12 | ) 13 | } 14 | } 15 | 16 | export default NotFoundPage; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Self-Sustaining Community 2 | 3 | You can write stories. 4 | You can play games. 5 | 6 | These stories and games let you complete small or big things with people together. 7 | 8 | You do things you are interested in and get an income. 9 | 10 | Make your life better. 11 | Make the world better. 12 | 13 | ## Dev Workflow 14 | ### Install dependencies 15 | npm install 16 | 17 | ### Run on localhost 18 | npm start 19 | -------------------------------------------------------------------------------- /site/src/app/util/event.js: -------------------------------------------------------------------------------- 1 | function subscribe(eventName, listener) { 2 | document.addEventListener(eventName, listener); 3 | } 4 | 5 | function unsubscribe(eventName, listener) { 6 | document.removeEventListener(eventName, listener); 7 | } 8 | 9 | function publish(eventName, data) { 10 | const event = new CustomEvent(eventName, { detail: data }); 11 | document.dispatchEvent(event); 12 | } 13 | 14 | export { publish, subscribe, unsubscribe}; 15 | -------------------------------------------------------------------------------- /site/src/app/modals/ViewImageModal.css: -------------------------------------------------------------------------------- 1 | .view-image-modal-content { 2 | width: 100%; 3 | height: 100%; 4 | border: none; 5 | margin: auto; 6 | padding: 0; 7 | box-shadow: none; 8 | border-radius: 0; 9 | justify-content: center; 10 | align-items: center; 11 | cursor: auto; 12 | } 13 | 14 | .view-image-modal-image { 15 | max-width: 100%; 16 | } 17 | 18 | @media (max-width: 600px) { 19 | .view-image-modal-content { 20 | max-width: 100%; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /site/src/app/elements/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface LoadingProps { 4 | marginTop?: string; 5 | } 6 | 7 | class Loading extends React.Component { 8 | render() { 9 | return ( 10 |
14 |
15 |
16 | ); 17 | } 18 | } 19 | 20 | export default Loading; -------------------------------------------------------------------------------- /site/src/app/pages/NotiPage.css: -------------------------------------------------------------------------------- 1 | .noti-page { 2 | display: flex; 3 | flex-direction: column; 4 | max-width: var(--page-max-width); 5 | padding-left: 30px; 6 | padding-bottom: 30px; 7 | row-gap: 10px; 8 | line-height: 30px; 9 | } 10 | 11 | .noti-page-header { 12 | display: flex; 13 | align-items: center; 14 | justify-content: space-between; 15 | } 16 | 17 | .noti-page-title { 18 | font-size: 23px; 19 | margin-bottom: 20px; 20 | } 21 | 22 | @media (max-width: 650px) { 23 | .noti-page { 24 | padding: 10px; 25 | padding-bottom: 60px; 26 | } 27 | } -------------------------------------------------------------------------------- /site/src/app/modals/MessageModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Modal.css' 3 | 4 | interface MessageModalProps { 5 | message: string; 6 | } 7 | 8 | class MessageModal extends React.Component { 9 | render() { 10 | if(this.props.message == '') 11 | return (null); 12 | 13 | return ( 14 |
15 |
16 | {this.props.message} 17 |
18 |
19 | ) 20 | } 21 | } 22 | 23 | export default MessageModal; -------------------------------------------------------------------------------- /site/src/app/pages/BookmarksPage.css: -------------------------------------------------------------------------------- 1 | .bookmarks-page { 2 | display: flex; 3 | flex-direction: column; 4 | max-width: var(--page-max-width); 5 | margin: auto; 6 | padding-left: 30px; 7 | padding-bottom: 30px; 8 | /* row-gap: 20px; */ 9 | } 10 | 11 | .bookmarks-page-header { 12 | display: flex; 13 | align-items: flex-end; 14 | justify-content: space-between; 15 | margin-bottom: 20px; 16 | } 17 | 18 | .bookmarks-page-header-title { 19 | font-size: 23px; 20 | } 21 | 22 | @media (max-width: 650px) { 23 | .bookmarks-page { 24 | padding: 10px; 25 | padding-bottom: 60px; 26 | } 27 | } -------------------------------------------------------------------------------- /site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "types": ["node", "react", "react-dom", "react-redux"], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "strictNullChecks": false, 16 | "forceConsistentCasingInFileNames": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "module": "esnext", 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noEmit": true, 23 | "jsx": "react-jsx" 24 | }, 25 | "include": [ 26 | "src" 27 | ] 28 | } -------------------------------------------------------------------------------- /site/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Typr 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /site/src/app/pages/GamesPage.css: -------------------------------------------------------------------------------- 1 | .games-page { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | max-width: var(--page-max-width); 6 | margin: auto; 7 | padding-left: 30px; 8 | padding-bottom: 30px; 9 | row-gap: 30px; 10 | line-height: 35px; 11 | } 12 | 13 | .games-intro-line { 14 | text-align: center; 15 | width: 390px; 16 | padding: 20px; 17 | border-radius: 30px; 18 | font-size: 18px; 19 | color: white; 20 | background-color: var(--section-header-color); 21 | } 22 | 23 | .game { 24 | color: black; 25 | background-color: var(--section-color); 26 | } 27 | 28 | @media (max-width: 650px) { 29 | .games-page { 30 | padding: 10px; 31 | } 32 | 33 | .games-intro-line { 34 | width: unset; 35 | } 36 | } -------------------------------------------------------------------------------- /site/src/app/pages/GamesPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './GamesPage.css'; 3 | 4 | class GamesPage extends React.Component { 5 | render() { 6 | return ( 7 |
8 |
9 | We believe that games will shape our future.
10 | Span across entertainment, education, health, shopping, crypto, and fintech. 11 |
And that is where we will be.
12 | From https://twitter.com/jasonoliver 13 |
14 |
15 | There is a MUD game running on AO that can be played in the AOS terminal.
16 | Here is a place for GUI to play it.
17 | Stay tuned! 18 |
19 |
20 | ) 21 | } 22 | } 23 | 24 | export default GamesPage; -------------------------------------------------------------------------------- /site/src/app/modals/PostModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BsFillXCircleFill } from 'react-icons/bs'; 3 | import './Modal.css' 4 | import PostContent from '../elements/PostContent'; 5 | 6 | interface PostModalProps { 7 | open: boolean; 8 | onClose: Function; 9 | isStory?: boolean; 10 | } 11 | 12 | class PostModal extends React.Component { 13 | render() { 14 | if (!this.props.open) 15 | return (
); 16 | 17 | return ( 18 |
19 |
20 | 23 | 24 | 25 |
26 |
27 | ) 28 | } 29 | } 30 | 31 | export default PostModal; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Node artifact files 7 | node_modules/ 8 | dist/ 9 | build/ 10 | 11 | # Compiled Java class files 12 | *.class 13 | 14 | # Compiled Python bytecode 15 | *.py[cod] 16 | 17 | # Log files 18 | *.log 19 | 20 | # Package files 21 | *.jar 22 | 23 | # Maven 24 | target/ 25 | dist/ 26 | 27 | # JetBrains IDE 28 | .idea/ 29 | 30 | # Unit test reports 31 | TEST*.xml 32 | 33 | # Generated by MacOS 34 | .DS_Store 35 | 36 | # Generated by Windows 37 | Thumbs.db 38 | 39 | # Applications 40 | *.app 41 | *.exe 42 | *.war 43 | 44 | # Large media files 45 | *.mp4 46 | *.tiff 47 | *.avi 48 | *.flv 49 | *.mov 50 | *.wmv 51 | 52 | # vs code 53 | .env 54 | .vscode 55 | 56 | # Hardhat files 57 | cache 58 | artifacts 59 | typechain-types -------------------------------------------------------------------------------- /site/src/app/modals/AlertModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Modal.css' 3 | 4 | interface AlertModalProps { 5 | message: string; 6 | button: string; 7 | onClose: Function; 8 | } 9 | 10 | class AlertModal extends React.Component { 11 | constructor(props:AlertModalProps) { 12 | super(props); 13 | this.onClose = this.onClose.bind(this); 14 | } 15 | 16 | onClose(e: any) { 17 | e.stopPropagation(); 18 | this.props.onClose(); 19 | } 20 | 21 | render() { 22 | if(this.props.message == '') 23 | return (null); 24 | 25 | return ( 26 |
27 |
28 |
{this.props.message}
29 |
30 | 31 |
32 |
33 |
34 | ) 35 | } 36 | } 37 | 38 | export default AlertModal; -------------------------------------------------------------------------------- /site/src/app/modals/BountyRecordsModal.css: -------------------------------------------------------------------------------- 1 | .br-modal { 2 | display: flex; 3 | margin-bottom: 10px; 4 | align-items: center; 5 | } 6 | 7 | .br-modal-icon { 8 | width: 35px; 9 | height: 35px; 10 | border-radius: 50%; 11 | } 12 | 13 | .br-modal-user { 14 | display: flex; 15 | width: 200px; 16 | column-gap: 10px; 17 | align-items: center; 18 | text-decoration: none; 19 | color: black; 20 | margin-right: 5px; 21 | } 22 | 23 | .br-modal-user:hover { 24 | background-color: rgb(241, 232, 232); 25 | border-radius: 10px; 26 | } 27 | 28 | .br-modal-token { 29 | display: flex; 30 | width: 140px; 31 | column-gap: 10px; 32 | align-items: center; 33 | } 34 | 35 | .br-modal-address { 36 | font-size: 11px; 37 | color: gray; 38 | margin-top: -5px; 39 | } 40 | 41 | .br-modal-quantity { 42 | font-size: 16px; 43 | margin-top: -6px; 44 | } 45 | 46 | @media (max-width: 650px) { 47 | .br-modal-user { 48 | width: 190px; 49 | } 50 | 51 | .br-modal-token { 52 | width: 120px; 53 | } 54 | } -------------------------------------------------------------------------------- /site/config-overrides.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = function override(config) { 4 | const fallback = config.resolve.fallback || {}; 5 | 6 | Object.assign(fallback, { 7 | fs: false, 8 | net: false, 9 | tls: false, 10 | zlib: false, 11 | "child_process": false, 12 | "os": require.resolve("os-browserify/browser"), 13 | "path": require.resolve("path-browserify"), 14 | "stream": require.resolve("stream-browserify"), 15 | "crypto": require.resolve("crypto-browserify"), 16 | "http": require.resolve("stream-http"), 17 | "https": require.resolve("https-browserify"), 18 | "assert": require.resolve("assert"), 19 | "url": require.resolve("url"), 20 | "process": require.resolve("process/browser.js") 21 | }); 22 | 23 | config.resolve.fallback = fallback; 24 | 25 | config.plugins = (config.plugins || []).concat([ 26 | new webpack.ProvidePlugin({ 27 | Buffer: ['buffer', 'Buffer'], 28 | process: 'process/browser.js' 29 | }) 30 | ]); 31 | 32 | return config; 33 | }; 34 | -------------------------------------------------------------------------------- /site/src/app/pages/FollowPage.css: -------------------------------------------------------------------------------- 1 | .follow-page { 2 | display: flex; 3 | flex-direction: column; 4 | max-width: var(--page-max-width); 5 | margin: auto; 6 | padding-left: 30px; 7 | padding-bottom: 30px; 8 | } 9 | 10 | .follow-page-header { 11 | display: flex; 12 | column-gap: 15px; 13 | margin-bottom: 10px; 14 | } 15 | 16 | .follow-page-row { 17 | display: flex; 18 | column-gap: 15px; 19 | cursor: pointer; 20 | padding: 5px 10px; 21 | color: black; 22 | text-decoration: none; 23 | margin-bottom: 10px; 24 | } 25 | 26 | .follow-page-row:hover { 27 | background-color: rgb(238, 232, 232); 28 | } 29 | 30 | .follow-page-portrait { 31 | width: 40px; 32 | height: 40px; 33 | border-radius: 50%; 34 | } 35 | 36 | .follow-page-nickname { 37 | font-size: 17px; 38 | font-weight: 600; 39 | } 40 | 41 | .follow-page-addr { 42 | font-size: 12px; 43 | color: gray; 44 | margin-top: -5px; 45 | } 46 | 47 | .follow-page-bio { 48 | font-size: 15px; 49 | } 50 | 51 | @media (max-width: 650px) { 52 | .follow-page { 53 | padding: 10px; 54 | } 55 | } -------------------------------------------------------------------------------- /lua/twitter.lua: -------------------------------------------------------------------------------- 1 | Members = Members or {} 2 | Posts = Posts or {} 3 | Replies = Replies or {} 4 | 5 | -- Handlers.add( 6 | -- "Register", 7 | -- Handlers.utils.hasMatchingTag("Action", "Register"), 8 | -- function(msg) 9 | -- table.insert(Members, msg.From) 10 | -- Handlers.utils.reply("registered")(msg) 11 | -- end 12 | -- ) 13 | 14 | Handlers.add( 15 | "GetPosts", 16 | Handlers.utils.hasMatchingTag("Action", "GetPosts"), 17 | function(msg) 18 | Handlers.utils.reply(table.concat(Posts, "▲"))(msg) 19 | end 20 | ) 21 | 22 | Handlers.add( 23 | "SendPost", 24 | Handlers.utils.hasMatchingTag("Action", "SendPost"), 25 | function(msg) 26 | table.insert(Posts, msg.Data) 27 | end 28 | ) 29 | 30 | Handlers.add( 31 | "GetReplies", 32 | Handlers.utils.hasMatchingTag("Action", "GetReplies"), 33 | function(msg) 34 | Handlers.utils.reply(table.concat(Replies, "▲"))(msg) 35 | end 36 | ) 37 | 38 | Handlers.add( 39 | "SendReply", 40 | Handlers.utils.hasMatchingTag("Action", "SendReply"), 41 | function(msg) 42 | table.insert(Replies, msg.Data) 43 | end 44 | ) 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 gamelover 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 | -------------------------------------------------------------------------------- /site/src/app/pages/StoryPage.css: -------------------------------------------------------------------------------- 1 | .story-page { 2 | display: flex; 3 | flex-direction: column; 4 | max-width: var(--page-max-width); 5 | margin: auto; 6 | padding-left: 30px; 7 | padding-bottom: 30px; 8 | row-gap: 10px; 9 | line-height: 30px; 10 | } 11 | 12 | .story-page-header { 13 | display: flex; 14 | align-items: center; 15 | justify-content: space-between; 16 | } 17 | 18 | .story-page-title { 19 | font-size: 23px; 20 | } 21 | 22 | .story-page-filter-row { 23 | display: flex; 24 | align-items: center; 25 | } 26 | 27 | .story-page-filter-container { 28 | display: flex; 29 | padding: 10px 0; 30 | } 31 | 32 | .story-page-filter { 33 | margin-right: 25px; 34 | cursor: pointer; 35 | color: gray; 36 | } 37 | 38 | .story-page-filter.selected { 39 | color: black; 40 | cursor: default; 41 | border-bottom: 1px solid black; 42 | } 43 | 44 | .story-page-category { 45 | width: 100px; 46 | height: 35px; 47 | margin-left: 25px; 48 | outline: none; 49 | border: 1px solid lightgray; 50 | } 51 | 52 | @media (max-width: 650px) { 53 | .story-page { 54 | padding: 10px; 55 | padding-bottom: 50px; 56 | } 57 | } -------------------------------------------------------------------------------- /site/src/app/modals/QuestionModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Modal.css' 3 | 4 | interface QuestionModalProps { 5 | message: string; 6 | onYes: Function; 7 | onNo: Function; 8 | } 9 | 10 | class QuestionModal extends React.Component { 11 | constructor(props: QuestionModalProps) { 12 | super(props); 13 | } 14 | 15 | render() { 16 | if (this.props.message == '') 17 | return (null); 18 | 19 | return ( 20 |
21 |
22 |
{this.props.message}
23 |
24 | 25 |
       
26 | 27 |
28 |
29 |
30 | ) 31 | } 32 | } 33 | 34 | export default QuestionModal; -------------------------------------------------------------------------------- /site/src/app/elements/externalEmbed.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './externalEmbed.css'; 3 | import { regexPatterns } from '../util/consts'; 4 | 5 | interface ExternalEmbedProps { 6 | src: string; 7 | } 8 | 9 | const ExternalEmbed: React.FC = ({ src }) => { 10 | const { youtubeRegex } = regexPatterns; 11 | // console.log('ExternalEmbed:', src); 12 | const getVideoID = (src: string): string | null => { 13 | const match = src.match(youtubeRegex); 14 | return match ? match[1] : null; 15 | }; 16 | 17 | const videoId = getVideoID(src); 18 | 19 | if (!videoId) { 20 | return

Invalid video URL: ${src}

; 21 | } 22 | 23 | const embedUrl = `https://www.youtube.com/embed/${videoId}`; 24 | const iframeTitle = `YouTube video player`; 25 | 26 | return ( 27 |
28 | 35 |
36 | ); 37 | }; 38 | 39 | export default ExternalEmbed; -------------------------------------------------------------------------------- /site/src/app/AppConfig.tsx: -------------------------------------------------------------------------------- 1 | export class AppConfig { 2 | public static siteName = 'Up Up'; 3 | public static secretPassword = 'ploy'; 4 | 5 | public static menu = [ 6 | { 7 | text: 'Home', 8 | icon: 'home', 9 | to: '/', 10 | loggedIn: false 11 | }, 12 | { 13 | text: 'Story', 14 | icon: 'story', 15 | to: '/story', 16 | // beta: false, 17 | loggedIn: false 18 | }, 19 | { 20 | text: 'Games', 21 | icon: 'games', 22 | to: '/games', 23 | loggedIn: false 24 | }, 25 | { 26 | text: 'TokenEco', 27 | icon: 'token', 28 | to: '/token', 29 | // beta: true, 30 | loggedIn: true 31 | }, 32 | { 33 | text: 'Chatroom', 34 | icon: 'chatroom', 35 | to: '/chat', 36 | loggedIn: true 37 | }, 38 | { 39 | text: 'Notifications', 40 | icon: 'notifications', 41 | to: '/notifications', 42 | // new: true, 43 | loggedIn: true 44 | }, 45 | { 46 | text: 'Bookmarks', 47 | icon: 'bookmarks', 48 | to: '/bookmarks', 49 | loggedIn: true 50 | }, 51 | { 52 | text: 'Profile', 53 | icon: 'profile', 54 | to: '/profile', 55 | loggedIn: true 56 | }, 57 | ]; 58 | } -------------------------------------------------------------------------------- /site/src/app/modals/Modal.js: -------------------------------------------------------------------------------- 1 | import './Modal.css' 2 | 3 | const Modal = props => { 4 | // close the modal 5 | // if (!props.open) return null; 6 | 7 | return ( 8 |
9 |
e.stopPropagation()}> 10 | {(props.showHeader || props.showHeader == undefined) && 11 |
12 | × 13 | {props.header} 14 |
15 | } 16 | 17 |
18 | {props.children} 19 |
20 | 21 |
22 | {props.showBtnCancel && 23 | 26 | } 27 | 28 | {(props.showBtnOk || props.showBtnOk == undefined) && 29 | 32 | } 33 |
34 |
35 |
36 | ) 37 | } 38 | 39 | export default Modal -------------------------------------------------------------------------------- /site/src/app/elements/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './NavBar.css'; 3 | import { AppConfig } from '../AppConfig'; 4 | import { getMenuIcon } from '../util/util'; 5 | import NavBarButton from './NavBarButton'; 6 | import { Server } from '../../server/server'; 7 | import { subscribe } from '../util/event'; 8 | 9 | class NavBar extends React.Component { 10 | 11 | constructor(props: any) { 12 | super(props); 13 | subscribe('wallet-events', () => { 14 | this.forceUpdate(); 15 | }); 16 | } 17 | 18 | renderButton(menu: any) { 19 | return ( 20 | 28 | ) 29 | } 30 | 31 | render() { 32 | let buttons = []; 33 | let menu = AppConfig.menu; 34 | 35 | for (let i = 0; i < menu.length; i++) { 36 | if (menu[i].loggedIn) { 37 | if (Server.service.isLoggedIn()) 38 | buttons.push(this.renderButton(menu[i])); 39 | } 40 | else 41 | buttons.push(this.renderButton(menu[i])); 42 | } 43 | 44 | return ( 45 | 48 | ); 49 | } 50 | } 51 | 52 | export default NavBar; -------------------------------------------------------------------------------- /site/src/app/modals/LoginModal.css: -------------------------------------------------------------------------------- 1 | .login-modal-content { 2 | background-color: var(--page-color); 3 | margin: 100px auto; 4 | /* padding: 7px 10px 0px 10px; */ 5 | border: 1px solid #888; 6 | border-radius: 20px; 7 | width: 300px; 8 | text-align: center; 9 | box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); 10 | position: relative; 11 | color: #ffffff80; 12 | } 13 | 14 | .login-modal-container { 15 | display: flex; 16 | flex-direction: column; 17 | row-gap: 15px; 18 | padding: 15px; 19 | } 20 | 21 | .login-modal-cancel-button { 22 | background-color: #00000080; 23 | } 24 | 25 | .login-modal-error { 26 | background-color: darkred; 27 | padding: 5px 10px 5px 10px; 28 | border-radius: 10px; 29 | font-size: 0.9em; 30 | color: white; 31 | } 32 | 33 | .login-modal-link { 34 | text-decoration: underline; 35 | /* font-size: 0.9em; */ 36 | color: white; 37 | cursor: pointer; 38 | } 39 | 40 | .login-modal-header { 41 | font-size: 1.1em; 42 | color: white; 43 | } 44 | 45 | .login-modal-button-mm, .login-modal-button-wc { 46 | display: flex; 47 | align-items: center; 48 | justify-content: center; 49 | margin-top: 50px; 50 | } 51 | 52 | .login-modal-button-wc { 53 | margin-top: 20px; 54 | margin-bottom: 30px; 55 | } 56 | 57 | .login-modal-icon { 58 | width: 45px; 59 | margin-right: 10px; 60 | } -------------------------------------------------------------------------------- /site/public/ar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /site/src/app/modals/ViewImageModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Modal.css'; 3 | import './ViewImageModal.css'; 4 | import { BsFillXCircleFill } from 'react-icons/bs'; 5 | 6 | interface ViewImageModalProps { 7 | open: boolean; 8 | src: string; 9 | onClose: Function; 10 | } 11 | 12 | class ViewImageModal extends React.Component { 13 | constructor(props:ViewImageModalProps) { 14 | super(props); 15 | this.onClose = this.onClose.bind(this); 16 | } 17 | 18 | componentDidMount() { 19 | document.addEventListener('keydown', (e) => { 20 | if (e.key === 'Escape') // Esc key 21 | this.onClose(); 22 | }); 23 | } 24 | 25 | componentWillUnmount() { 26 | document.removeEventListener('keydown', (e) => { 27 | if (e.key === 'Escape') 28 | this.onClose(); 29 | }); 30 | } 31 | 32 | onClose() { 33 | this.props.onClose(); 34 | } 35 | 36 | render() { 37 | if(!this.props.open) 38 | return (
); 39 | 40 | return ( 41 |
e.stopPropagation()}> 42 |
43 | 46 | 47 |
48 | 49 |
50 |
51 |
52 | ) 53 | } 54 | } 55 | 56 | export default ViewImageModal; -------------------------------------------------------------------------------- /site/src/app/pages/SitePage.css: -------------------------------------------------------------------------------- 1 | .site-page-footer { 2 | display: none; 3 | position: fixed; 4 | left: 0; 5 | bottom: 0; 6 | width: 100%; 7 | background-color: white; 8 | /* padding: 10px; */ 9 | /* box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); */ 10 | border-top: 0.5px solid lightgray; 11 | opacity: 0.9; 12 | align-items: center; 13 | justify-content: space-around; 14 | } 15 | 16 | .site-page-icon-button { 17 | padding: 7px 15px 3px 15px; 18 | color: black; 19 | } 20 | 21 | .site-page-header-mobile { 22 | display: none; 23 | } 24 | 25 | .site-page-mobile-menu { 26 | display: flex; 27 | position: fixed; 28 | top: 0; 29 | left: 0; 30 | width: 100%; 31 | height: 93vh; 32 | align-items: flex-end; 33 | justify-content: flex-end; 34 | } 35 | 36 | .site-page-menu-container { 37 | display: flex; 38 | flex-direction: column; 39 | row-gap: 5px; 40 | background-color: white; 41 | border-radius: 10px; 42 | box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2); 43 | margin-bottom: 30px; 44 | } 45 | 46 | .site-page-menu-item { 47 | display: flex; 48 | align-items: center; 49 | color: black; 50 | column-gap: 10px; 51 | padding: 10px 15px; 52 | text-decoration: none; 53 | font-size: 18px; 54 | } 55 | 56 | @media (max-width: 650px) { 57 | .site-page-footer { 58 | display: flex; 59 | } 60 | 61 | .site-page-header-pc { 62 | display: none; 63 | } 64 | 65 | .site-page-header-mobile { 66 | display: flex; 67 | align-items: center; 68 | justify-content: space-between; 69 | padding: 10px; 70 | border-bottom: 0.5px solid lightgray; 71 | margin-bottom: 15px; 72 | } 73 | } -------------------------------------------------------------------------------- /site/src/app/elements/StoryCard.css: -------------------------------------------------------------------------------- 1 | 2 | .story-card { 3 | position: relative; 4 | display: grid; 5 | grid-template-columns: 0.25fr 1fr; 6 | align-items: center; 7 | padding: 10px; 8 | padding-right: 20px; 9 | border-radius: 10px; 10 | background-color: var(--section-header-color); 11 | cursor: pointer; 12 | color: white; 13 | text-decoration: none; 14 | } 15 | 16 | .story-card:hover { 17 | background-color: rgb(81, 83, 88); 18 | } 19 | 20 | .story-card-header { 21 | display: flex; 22 | align-items: center; 23 | column-gap: 10px; 24 | } 25 | 26 | .story-card-image-container { 27 | display: flex; 28 | align-items: center; 29 | justify-content: center; 30 | width: 100px; 31 | height: 100px; 32 | margin-right: 20px; 33 | border-radius: 10px; 34 | background-color: #00000040; 35 | } 36 | 37 | .story-card-image { 38 | max-width: 100px; 39 | max-height: 100px; 40 | border-radius: 10px; 41 | } 42 | 43 | .story-card-avatar { 44 | width: 25px; 45 | height: 25px; 46 | border-radius: 50%; 47 | } 48 | 49 | .story-card-publisher { 50 | color: lightgray; 51 | } 52 | 53 | .story-card-title { 54 | display: flex; 55 | font-size: 1.1em; 56 | word-wrap: break-word; 57 | } 58 | 59 | .story-card-summary { 60 | color: gray; 61 | font-size: 0.88em; 62 | line-height: 20px; 63 | } 64 | 65 | .story-card-state-row { 66 | display: flex; 67 | justify-content: space-around; 68 | margin-top: 8px; 69 | } 70 | 71 | .story-card-state { 72 | display: flex; 73 | align-items: center; 74 | } 75 | 76 | .story-card-state-number { 77 | margin-left: 5px; 78 | font-size: 15px; 79 | color: lightgray; 80 | } 81 | 82 | @media (max-width: 650px) { 83 | .story-card-publisher { 84 | font-size: 15px; 85 | } 86 | } -------------------------------------------------------------------------------- /site/src/app/elements/NavBar.css: -------------------------------------------------------------------------------- 1 | .navbar-container { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .navbar-button { 7 | display: flex; 8 | align-items: center; 9 | position: relative; 10 | } 11 | 12 | .navbar-button-icon { 13 | width: 30px; 14 | height: 30px; 15 | } 16 | 17 | .navbar-text { 18 | margin-left: 10px; 19 | } 20 | 21 | @media (max-width: 850px) { 22 | .navbar-text { 23 | display: none; 24 | } 25 | } 26 | 27 | @media (max-width: 710px) { 28 | .navbar-container { 29 | display: none; 30 | } 31 | .navbar-text { 32 | display: block; 33 | } 34 | } 35 | 36 | .navbar-link, .navbar-link-active { 37 | height: 30px; 38 | color: rgb(113, 105, 105); 39 | text-decoration: none; 40 | margin-bottom: 10px; 41 | border-radius: 30px; 42 | padding: 10px 20px; 43 | } 44 | 45 | .navbar-link:hover { 46 | color: white; 47 | background-color: var(--section-header-color); 48 | } 49 | 50 | .navbar-link-active { 51 | font-weight: 600; 52 | color: black; 53 | } 54 | 55 | .pop-navbar-link { 56 | height: 20px; 57 | padding-top: 10px; 58 | padding-bottom: 10px; 59 | color: black; 60 | text-decoration: none; 61 | } 62 | 63 | .navbar-red-circle { 64 | width: 10px; 65 | height: 10px; 66 | border-radius: 50%; 67 | background-color: red; 68 | position: absolute; 69 | right: -7px; 70 | } 71 | 72 | .navbar-label-beta { 73 | font-size: 13px; 74 | color: white; 75 | padding: 0px 5px; 76 | line-height: 17px; 77 | background-color: red; 78 | position: absolute; 79 | right: 0px; 80 | top: -15px; 81 | } 82 | 83 | .new { 84 | top: -12px; 85 | } 86 | 87 | .navbar-bottom { 88 | text-align: center; 89 | color: rgb(19, 71, 182); 90 | font-style: italic; 91 | font-size: 0.85em; 92 | margin-top: 50px; 93 | } -------------------------------------------------------------------------------- /site/src/app/elements/Portrait.css: -------------------------------------------------------------------------------- 1 | .portrait-div-container { 2 | display: flex; 3 | align-items: center; 4 | column-gap: 10px; 5 | cursor: pointer; 6 | padding: 5px 10px; 7 | border-radius: 30px; 8 | margin-top: 50px; 9 | } 10 | 11 | .portrait-div-container:hover { 12 | background-color: lightgray; 13 | } 14 | 15 | .site-page-portrait { 16 | width: 50px; 17 | height: 50px; 18 | border-radius: 50%; 19 | } 20 | 21 | .site-page-nickname { 22 | font-size: 16px; 23 | font-weight: 600; 24 | } 25 | 26 | .site-page-addr { 27 | font-size: 13px; 28 | color: gray; 29 | /* margin-top: 3px; */ 30 | } 31 | 32 | .connect { 33 | width: 110px; 34 | margin-top: 50px; 35 | margin-left: 10px; 36 | } 37 | 38 | .connect.othent { 39 | background-color: rgb(102, 102, 221); 40 | } 41 | 42 | .connect.othent:hover { 43 | background-color: rgb(119, 119, 228); 44 | } 45 | 46 | .portrait-div-or { 47 | text-align: center; 48 | margin-top: 20px; 49 | margin-bottom: -30px; 50 | } 51 | 52 | .portrait-label { 53 | font-size: 13px; 54 | color: rgb(237, 108, 108); 55 | margin-left: 12px; 56 | margin-top: 5px; 57 | } 58 | 59 | .portrait-conn-mobile { 60 | display: none; 61 | } 62 | 63 | @media (max-width: 650px) { 64 | .portrait-conn-pc { 65 | display: none; 66 | } 67 | 68 | .portrait-conn-mobile { 69 | display: flex; 70 | } 71 | 72 | .connect { 73 | width: 100px; 74 | margin-top: unset; 75 | } 76 | 77 | .portrait-div-container { 78 | margin: 0px; 79 | background-color: white; 80 | } 81 | 82 | .site-page-portrait { 83 | width: 40px; 84 | height: 40px; 85 | } 86 | 87 | .site-page-nickname { 88 | color: black; 89 | } 90 | 91 | .site-page-addr { 92 | font-size: 13px; 93 | color: gray; 94 | margin-top: -5px; 95 | } 96 | } -------------------------------------------------------------------------------- /site/src/app/modals/LoginModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BsFillXCircleFill } from 'react-icons/bs'; 3 | import AlertModal from './AlertModal'; 4 | import './Modal.css' 5 | import './LoginModal.css' 6 | import MessageModal from './MessageModal'; 7 | 8 | declare var window: any; 9 | 10 | interface LoginModalProps { 11 | open: boolean; 12 | onClose: Function; 13 | } 14 | 15 | interface LoginModalState { 16 | message: string; 17 | alert: string; 18 | } 19 | 20 | class LoginModal extends React.Component { 21 | constructor(props:LoginModalProps) { 22 | super(props); 23 | 24 | this.state = { 25 | message: '', 26 | alert: '', 27 | } 28 | } 29 | 30 | render() { 31 | if(!this.props.open) 32 | return (
); 33 | 34 | return ( 35 |
36 |
37 | 40 | 41 |
42 | 43 | {/* */} 44 |
45 |
46 | 47 |
48 | 49 | {/* */} 50 |
51 |
52 | 53 | 54 | this.setState({alert: ''})}/> 55 |
56 | ) 57 | } 58 | } 59 | 60 | export default LoginModal; -------------------------------------------------------------------------------- /site/src/app/pages/ActivityPostPage.css: -------------------------------------------------------------------------------- 1 | .activity-post-page { 2 | padding-left: 30px; 3 | padding-bottom: 30px; 4 | max-width: var(--page-max-width); 5 | margin: auto; 6 | display: flex; 7 | flex-direction: column; 8 | row-gap: 10px; 9 | } 10 | 11 | .activity-post-page-header { 12 | display: flex; 13 | column-gap: 10px; 14 | background-color: var(--section-header-color); 15 | border-radius: 10px; 16 | padding: 5px 10px; 17 | align-items: center; 18 | color: white; 19 | cursor: pointer; 20 | } 21 | 22 | .activity-post-page-header:hover { 23 | background-color: rgb(81, 83, 87); 24 | } 25 | 26 | .activity-post-time { 27 | color: lightgray; 28 | font-size: 14px; 29 | margin-left: 10px; 30 | } 31 | 32 | .activity-post-page-story-row { 33 | display: flex; 34 | align-items: flex-end; 35 | column-gap: 20px; 36 | margin-bottom: 10px; 37 | } 38 | 39 | .activity-post-page-back-button { 40 | width: 34px; 41 | height: 34px; 42 | font-size: 18pt; 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | } 47 | 48 | .activity-post-page-editor-header { 49 | display: flex; 50 | align-items: center; 51 | border-top-right-radius: 10px; 52 | border-top-left-radius: 10px; 53 | background-color: var(--section-header-color); 54 | padding-left: 15px; 55 | margin-top: 10px; 56 | height: 34px; 57 | } 58 | 59 | .activity-post-page-reply-container { 60 | border-radius: 10px; 61 | margin-top: 20px; 62 | } 63 | 64 | .activity-post-page-reply-header { 65 | font-size: 0.9em; 66 | color: gray; 67 | margin-top: 20px; 68 | } 69 | 70 | .activity-post-page-action { 71 | width: 80px; 72 | margin-top: 12px; 73 | } 74 | 75 | .activity-post-page-story-title { 76 | font-size: large; 77 | color: yellow; 78 | } 79 | 80 | @media (max-width: 650px) { 81 | .activity-post-page { 82 | padding: 10px; 83 | padding-bottom: 80px; 84 | } 85 | } -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ao-twitter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": ".", 6 | "dependencies": { 7 | "@dicebear/collection": "^8.0.0", 8 | "@dicebear/core": "^8.0.0", 9 | "@othent/kms": "^1.0.12", 10 | "@permaweb/aoconnect": "^0.0.62", 11 | "@types/node": "^20.11.1", 12 | "@types/react": "^18.0.9", 13 | "@types/react-dom": "^18.0.4", 14 | "arseeding-arbundles": "^0.6.27", 15 | "arweave": "^1.15.0", 16 | "arweavekit": "^1.5.1", 17 | "compressorjs": "^1.2.1", 18 | "ethers": "^5.7.2", 19 | "html-react-parser": "^3.0.4", 20 | "react": "^18.1.0", 21 | "react-dom": "^18.1.0", 22 | "react-icons": "^4.4.0", 23 | "react-quill": "^2.0.0", 24 | "react-router-dom": "^6.3.0", 25 | "react-scripts": "5.0.1", 26 | "react-tooltip": "^5.26.3", 27 | "typescript": "^4.6.4", 28 | "web3": "^1.10.0" 29 | }, 30 | "scripts": { 31 | "start": "react-app-rewired start", 32 | "build": "GENERATE_SOURCEMAP=false react-app-rewired build", 33 | "deploy": "irys upload-dir ./build -h https://node1.irys.xyz --wallet ./wallet.json -t arweave --index-file index.html" 34 | }, 35 | "eslintConfig": { 36 | "extends": [ 37 | "react-app", 38 | "react-app/jest" 39 | ] 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | "last 2 chrome version", 44 | "last 2 firefox version", 45 | "last 2 safari version", 46 | "last 2 edge version" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | }, 54 | "devDependencies": { 55 | "@types/react-redux": "^7.1.33", 56 | "react-app-rewired": "^2.2.1", 57 | "assert": "^2.0.0", 58 | "buffer": "^6.0.3", 59 | "crypto-browserify": "^3.12.0", 60 | "https-browserify": "^1.0.0", 61 | "os-browserify": "^0.3.0", 62 | "path-browserify": "^1.0.1", 63 | "stream-browserify": "^3.0.0", 64 | "stream-http": "^3.2.0", 65 | "url": "^0.11.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /site/src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {HashRouter, Route, Routes} from 'react-router-dom'; 3 | import SitePage from './pages/SitePage'; 4 | import NotFoundPage from './pages/NotFoundPage'; 5 | import './App.css'; 6 | import HomePage from './pages/HomePage'; 7 | import ActivityPostPage from './pages/ActivityPostPage'; 8 | import GamesPage from './pages/GamesPage'; 9 | import ChatPage from './pages/ChatPage'; 10 | import TokenPage from './pages/TokenPage'; 11 | import StoryPage from './pages/StoryPage'; 12 | import BookmarksPage from './pages/BookmarksPage'; 13 | import ProfilePage from './pages/ProfilePage'; 14 | import FollowPage from './pages/FollowPage'; 15 | import NotiPage from './pages/NotiPage'; 16 | 17 | class App extends React.Component<{}, {}> { 18 | constructor(props = {}) { 19 | super(props); 20 | } 21 | 22 | componentDidMount() { 23 | } 24 | 25 | render() { 26 | return ( 27 | 28 | 29 | }> 30 | } /> 31 | } /> 32 | } /> 33 | } /> 34 | } /> 35 | } /> 36 | } /> 37 | } /> 38 | } /> 39 | } /> 40 | } /> 41 | } /> 42 | } /> 43 | } /> 44 | 45 | 46 | 47 | ); 48 | } 49 | } 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /site/src/app/elements/PostContent.css: -------------------------------------------------------------------------------- 1 | .post-story-modal-story-title { 2 | width: 95%; 3 | margin-top: 10px; 4 | margin-bottom: 20px; 5 | } 6 | 7 | .post-story-modal-left-actions { 8 | display: flex; 9 | column-gap: 20px; 10 | } 11 | 12 | .post-story-modal-poll-icon { 13 | display: flex; 14 | align-items: center; 15 | cursor: pointer; 16 | } 17 | 18 | .post-story-modal-poll-container { 19 | display: flex; 20 | flex-direction: column; 21 | /* row-gap: 10px; */ 22 | padding: 10px; 23 | margin: 10px; 24 | margin-bottom: 20px; 25 | border-radius: 10px; 26 | border: 1px solid lightgray; 27 | color: gray; 28 | } 29 | 30 | .post-story-modal-poll-option { 31 | width: 83%; 32 | } 33 | 34 | .poll-option-container { 35 | display: flex; 36 | align-items: center; 37 | margin-bottom: 10px; 38 | } 39 | 40 | .add-poll-option-icon { 41 | display: flex; 42 | align-items: center; 43 | cursor: pointer; 44 | margin-left: 20px; 45 | } 46 | 47 | .poll-length-input { 48 | width: 120px; 49 | margin-right: 20px; 50 | margin-bottom: 10px; 51 | } 52 | 53 | .poll-token-process { 54 | width: 240px; 55 | margin-right: 20px; 56 | } 57 | 58 | .poll-token-amount { 59 | width: 95px; 60 | margin-right: 20px; 61 | } 62 | 63 | .poll-token-send { 64 | width: 100px; 65 | } 66 | 67 | .button-remove-poll { 68 | padding: 0px 10px 0px 10px; 69 | height: 30px; 70 | } 71 | 72 | .post-modal-content { 73 | padding-top: 50px; 74 | width: 500px; 75 | max-height: 600px; 76 | overflow-y: auto; 77 | } 78 | 79 | .post-modal-actions { 80 | display: flex; 81 | justify-content: space-between; 82 | margin: 10px; 83 | } 84 | 85 | .post-modal-header-row { 86 | display: flex; 87 | align-items: center; 88 | justify-content: space-between; 89 | margin-top: -30px; 90 | } 91 | 92 | .post-modal-header-title { 93 | font-size: 20px; 94 | margin-bottom: 5px; 95 | } 96 | 97 | .post-modal-header-balance { 98 | display: flex; 99 | align-items: center; 100 | column-gap: 5px; 101 | color: gray; 102 | margin-right: 200px; 103 | } 104 | 105 | @media (max-width: 650px) { 106 | .modal-content { 107 | /* max-width: 330px; */ 108 | max-width: 85%; 109 | } 110 | } -------------------------------------------------------------------------------- /site/src/app/pages/TokenPage.css: -------------------------------------------------------------------------------- 1 | .token-page { 2 | display: flex; 3 | flex-direction: column; 4 | max-width: var(--page-max-width); 5 | margin: auto; 6 | padding-left: 50px; 7 | padding-bottom: 30px; 8 | row-gap: 20px; 9 | } 10 | 11 | .token-page-card { 12 | display: flex; 13 | align-items: center; 14 | column-gap: 15px; 15 | padding: 8px 15px; 16 | border-radius: 15px; 17 | background-color: var(--section-color); 18 | } 19 | 20 | .token-page-card.process { 21 | flex-direction: column; 22 | align-items: flex-start; 23 | } 24 | 25 | .token-page-header { 26 | display: flex; 27 | column-gap: 30px; 28 | } 29 | 30 | .token-page-title { 31 | color: gray; 32 | } 33 | 34 | .token-page-text { 35 | margin-top: 5px; 36 | } 37 | 38 | .token-page-text.balance { 39 | font-size: 19px; 40 | margin-top: -1px; 41 | } 42 | 43 | .token-page-vote { 44 | color: white; 45 | margin-top: 10px; 46 | padding: 6px 15px; 47 | border-radius: 10px; 48 | background-color: var(--section-header-color); 49 | } 50 | 51 | .token-page-label { 52 | color: gray; 53 | } 54 | 55 | .token-page-input { 56 | width: 300px; 57 | } 58 | 59 | .token-page-icon { 60 | width: 50px; 61 | height: 50px; 62 | } 63 | 64 | .token-page-icon.circle { 65 | border-radius: 50%; 66 | } 67 | 68 | .token-page-icon.cred { 69 | width: 45px; 70 | height: unset; 71 | } 72 | 73 | .token-page-token-row { 74 | display: grid; 75 | grid-template-columns: 1fr 1fr; 76 | grid-gap: 15px; 77 | } 78 | 79 | .token-page-balance-title { 80 | font-size: 20px; 81 | margin-top: 30px; 82 | } 83 | 84 | .token-page-balance-line { 85 | width: 100px; 86 | height: 1px; 87 | margin-top: -17px; 88 | background-color: lightgray; 89 | } 90 | 91 | .token-page-prompt { 92 | font-size: 15px; 93 | color: rgb(229, 96, 96); 94 | margin-top: -15px; 95 | } 96 | 97 | .token-page-button-spawn { 98 | margin-left: 20px; 99 | background-color: green; 100 | } 101 | 102 | @media (max-width: 650px) { 103 | .token-page { 104 | padding: 10px; 105 | padding-bottom: 100px; 106 | } 107 | 108 | .token-page-button-spawn { 109 | margin-left: 15px; 110 | } 111 | 112 | .token-page-token-row { 113 | grid-template-columns: unset; 114 | } 115 | 116 | .token-page-text { 117 | word-break: break-all; 118 | } 119 | } -------------------------------------------------------------------------------- /site/src/app/modals/EditProfileModal.css: -------------------------------------------------------------------------------- 1 | .edit-profile-modal-content { 2 | width: 400px; 3 | text-align: center; 4 | } 5 | 6 | .edit-profile-modal-header { 7 | font-size: 1.1em; 8 | /* color: white; */ 9 | margin-bottom: 15px; 10 | } 11 | 12 | .edit-profile-banner-container { 13 | position: relative; 14 | } 15 | 16 | .edit-profile-banner { 17 | width: 100%; 18 | /* cursor: pointer; */ 19 | } 20 | 21 | /* .edit-profile-banner:hover { 22 | opacity: 0.8; 23 | } */ 24 | 25 | .edit-profile-portrait { 26 | position: absolute; 27 | width: 90px; 28 | height: 90px; 29 | left: 10px; 30 | bottom: -40px; 31 | border-radius: 50%; 32 | cursor: pointer; 33 | } 34 | 35 | .edit-profile-portrait:hover { 36 | opacity: 0.8; 37 | } 38 | 39 | .edit-profile-camera { 40 | position: absolute; 41 | width: 30px; 42 | height: 30px; 43 | left: 40px; 44 | bottom: -10px; 45 | color: var(--link-color); 46 | cursor: pointer; 47 | } 48 | 49 | .edit-profile-random-avatar { 50 | position: absolute; 51 | left: 130px; 52 | bottom: -40px; 53 | } 54 | 55 | .edit-profile-change-banner { 56 | position: absolute; 57 | top: 50%; 58 | left: 50%; 59 | font-size: 28pt; 60 | transform: translate(-20px, -22px); 61 | } 62 | 63 | .edit-profile-change-portrait { 64 | position: absolute; 65 | bottom: -38px; 66 | left: 57px; 67 | font-size: 24pt; 68 | transform: translate(-18px, -18px); 69 | } 70 | 71 | .edit-profile-input-container { 72 | margin-top: 70px; 73 | display: flex; 74 | flex-direction: column; 75 | row-gap: 15px; 76 | } 77 | 78 | .edit-profile-input-row { 79 | display: flex; 80 | flex-direction: row; 81 | } 82 | 83 | .edit-profile-label { 84 | width: 100px; 85 | /* text-align: right; */ 86 | margin-top: 7px; 87 | color: gray; 88 | } 89 | 90 | .edit-profile-input { 91 | width: 100%; 92 | } 93 | 94 | .edit-profile-textarea { 95 | width: 100%; 96 | height: 100px; 97 | border: 1px solid gray; 98 | resize: none; 99 | } 100 | 101 | .edit-profile-save-button-container { 102 | display: flex; 103 | justify-content: center; 104 | margin-top: 20px; 105 | margin-bottom: -15px; 106 | } 107 | 108 | .file-select { 109 | display: none; 110 | } 111 | 112 | @media (max-width: 650px) { 113 | .modal-content { 114 | max-width: 85%; 115 | } 116 | 117 | .edit-profile-modal-content { 118 | max-width: 85%; 119 | } 120 | } -------------------------------------------------------------------------------- /site/src/app/modals/Modal.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: fixed; 3 | z-index: 1; 4 | left: 0; 5 | top: 0; 6 | width: 100%; 7 | height: 100%; 8 | overflow: auto; /* Enable scroll if needed */ 9 | background-color: rgba(0,0,0,0.7); 10 | opacity: 0; 11 | /* transition: all 0.2s ease-in-out; */ 12 | pointer-events: none; 13 | cursor: auto; 14 | } 15 | 16 | .modal.open { 17 | opacity: 1; 18 | pointer-events: visible; 19 | } 20 | 21 | .close { 22 | color: #aaa; 23 | float: right; 24 | font-size: 28px; 25 | font-weight: bold; 26 | margin-top: -10px; 27 | } 28 | 29 | .close:hover, 30 | .close:focus { 31 | color: black; 32 | text-decoration: none; 33 | cursor: pointer; 34 | } 35 | 36 | .modal-content { 37 | display: flex; 38 | flex-direction: column; 39 | position: relative; 40 | background-color: var(--page-color); 41 | margin: 100px auto; 42 | padding: 15px; 43 | border: 1px solid #888; 44 | width: 400px; 45 | border-radius: 10px; 46 | box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); 47 | /* transform: translateY(-200px); */ 48 | /* transition: all 0.2s ease-in-out; */ 49 | } 50 | 51 | .modal.open .modal-content { 52 | transform: translateY(0); 53 | } 54 | 55 | .modal.open { 56 | transform: translateX(0); 57 | } 58 | 59 | @media (max-width: 600px) { 60 | .modal-content { 61 | max-width: 300px; 62 | margin: 70px auto; 63 | } 64 | } 65 | 66 | .modal-header { 67 | font-size: 1.1em; 68 | padding-bottom: 5px; 69 | border-bottom: 1px solid lightgray; 70 | } 71 | 72 | .modal-body { 73 | margin-top: 15px; 74 | } 75 | 76 | .modal-footer { 77 | display: flex; 78 | justify-content: flex-end; 79 | align-items: center; 80 | margin-top: 20px; 81 | } 82 | 83 | /* The Confirm Button */ 84 | .ok-button { 85 | width: 100px; 86 | } 87 | 88 | .ok-button:disabled { 89 | cursor: not-allowed; 90 | background-color: gray; 91 | } 92 | 93 | /* The Cancel Button */ 94 | .cancel-button { 95 | background-color: var(--body-color); 96 | width: 85px; 97 | margin-right: 10px; 98 | } 99 | 100 | .modal-close-button { 101 | position: absolute; 102 | right: 0; 103 | top: 0; 104 | background-color: transparent; 105 | width: 45px; 106 | height: 45px; 107 | font-size: 18pt; 108 | padding: 8px; 109 | color: black; 110 | min-width: 45px; 111 | } 112 | 113 | .question-modal-button-no { 114 | color: black; 115 | background-color: white; 116 | border: 1px solid gray; 117 | } -------------------------------------------------------------------------------- /site/src/app/modals/BountyRecordsModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BsFillXCircleFill } from 'react-icons/bs'; 3 | import './Modal.css' 4 | import './BountyRecordsModal.css' 5 | import { MdOutlineToken } from "react-icons/md"; 6 | import { formatTimestamp, shortAddr, shortStr } from '../util/util'; 7 | import { TOKEN_ICON } from '../util/consts'; 8 | import { NavLink } from 'react-router-dom'; 9 | 10 | interface BountyRecordsModalProps { 11 | open: boolean; 12 | onClose: Function; 13 | data: any; 14 | } 15 | 16 | class BountyRecordsModal extends React.Component { 17 | 18 | constructor(props: BountyRecordsModalProps) { 19 | super(props); 20 | this.onClose = this.onClose.bind(this); 21 | } 22 | 23 | onClose() { 24 | this.props.onClose(); 25 | } 26 | 27 | renderRecords() { 28 | let divs = []; 29 | let data = this.props.data; 30 | 31 | if (data.length == 0) 32 | return
No record.
33 | 34 | for (let i = 0; i < data.length; i++) { 35 | let formattedNum = data[i].quantity.toFixed(12).replace(/\.?0+$/, ""); 36 | 37 | divs.push( 38 |
39 | 40 | 41 |
42 |
{shortStr(data[i].nickname, 17)}
43 |
{shortAddr(data[i].address, 4)}
44 |
45 |
46 | 47 |
48 | 49 |
50 |
{data[i].token_name}
51 |
{formattedNum}
52 |
53 |
54 | 55 |
56 | {formatTimestamp(data[i].time)} 57 |
58 |
59 | ) 60 | } 61 | 62 | return divs; 63 | } 64 | 65 | render() { 66 | if (!this.props.open) 67 | return (
); 68 | 69 | return ( 70 |
71 |
72 | 75 |
76 |
Bounty Records
77 |
78 |
79 | {this.renderRecords()} 80 |
81 |
82 | ) 83 | } 84 | } 85 | 86 | export default BountyRecordsModal; -------------------------------------------------------------------------------- /site/src/app/pages/HomePage.css: -------------------------------------------------------------------------------- 1 | .home-page { 2 | display: flex; 3 | flex-direction: column; 4 | max-width: var(--page-max-width); 5 | margin: auto; 6 | padding-left: 30px; 7 | padding-bottom: 30px; 8 | row-gap: 20px; 9 | } 10 | 11 | .home-chat-container { 12 | margin-top: 30px; 13 | } 14 | 15 | .home-msg-line { 16 | padding: 10px; 17 | background-color: var(--section-color); 18 | border-bottom: 1px solid rgb(233, 229, 229); 19 | } 20 | 21 | .home-msg-line:hover { 22 | background-color: var(--section-hover-color); 23 | } 24 | 25 | .no_hover:hover { 26 | background-color: var(--section-color); 27 | } 28 | 29 | .home-msg-header { 30 | display: flex; 31 | column-gap: 10px; 32 | } 33 | 34 | .home-msg-portrait { 35 | width: 45px; 36 | height: 45px; 37 | cursor: pointer; 38 | border-radius: 50%; 39 | } 40 | 41 | .my-portrait { 42 | scale: 1.1; 43 | } 44 | 45 | .home-msg-text { 46 | display: flex; 47 | flex-direction: row; 48 | column-gap: 18px; 49 | align-items: center; 50 | } 51 | 52 | .home-msg-nickname { 53 | font-weight: 600; 54 | font-size: 17px; 55 | margin-top: -1px; 56 | } 57 | 58 | .home-msg-address { 59 | color: gray; 60 | font-size: 12px; 61 | } 62 | 63 | .other-line { 64 | align-self: flex-start; 65 | } 66 | 67 | .my-line { 68 | align-self: flex-end; 69 | justify-content: flex-end; 70 | } 71 | 72 | .home-msg-time { 73 | font-size: 13px; 74 | color: gray; 75 | } 76 | 77 | .home-input-message { 78 | width: 500px; 79 | border-radius: 10px; 80 | border: 1px solid rgb(85, 103, 169); 81 | padding: 5px 10px; 82 | margin-right: 20px; 83 | } 84 | 85 | .home-input-message.nickname { 86 | width: 189px; 87 | height: 35px; 88 | margin-left: 10px; 89 | font-size: 18px; 90 | } 91 | 92 | .home-input-message.process { 93 | height: 30px; 94 | } 95 | 96 | .home-input-container { 97 | border-radius: 10px; 98 | padding-bottom: 5px; 99 | background-color: var(--section-color); 100 | } 101 | 102 | .home-actions { 103 | display: flex; 104 | margin: 10px; 105 | justify-content: space-between; 106 | } 107 | 108 | .home-filter { 109 | width: 120px; 110 | border: none; 111 | outline: none; 112 | } 113 | 114 | .home-page-tip-new-posts { 115 | position: fixed; 116 | top: 20px; 117 | left: 50%; 118 | color: white; 119 | padding: 10px 20px; 120 | border-radius: 30px; 121 | background-color: rgb(114, 114, 234); 122 | cursor: pointer; 123 | } 124 | 125 | .home-page-tip-new-posts:hover { 126 | background-color: rgb(80, 80, 209); 127 | } 128 | 129 | .home-page-temp-tip { 130 | display: flex; 131 | flex-direction: column; 132 | row-gap: 5px; 133 | position: fixed; 134 | color: white; 135 | padding: 10px 20px; 136 | border-radius: 10px; 137 | background-color: rgb(114, 114, 234); 138 | } 139 | 140 | @media (max-width: 650px) { 141 | .home-page { 142 | padding: 10px; 143 | margin-top: -15px; 144 | } 145 | 146 | .home-chat-container { 147 | margin-top: 0px; 148 | } 149 | 150 | .home-msg-nickname { 151 | font-size: 15px; 152 | } 153 | } -------------------------------------------------------------------------------- /site/src/app/elements/NotiCard.css: -------------------------------------------------------------------------------- 1 | .activity-post-row { 2 | background-color: var(--section-color); 3 | border-bottom-right-radius: 10px; 4 | border-bottom-left-radius: 10px; 5 | padding: 10px; 6 | padding-bottom: 5px; 7 | } 8 | 9 | .activity-post-row-header { 10 | border-top-right-radius: 10px; 11 | border-top-left-radius: 10px; 12 | background-color: var(--section-header-color); 13 | font-size: 0.8em; 14 | color: lightgray; 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | padding: 0 10px; 19 | height: 35px; 20 | } 21 | 22 | .activity-post-navlink { 23 | display: flex; 24 | color: white; 25 | align-items: center; 26 | text-decoration: none; 27 | } 28 | 29 | .activity-post-profile { 30 | display: flex; 31 | align-items: center; 32 | margin-bottom: 15px; 33 | } 34 | 35 | .activity-post-portrait { 36 | width: 35px; 37 | height: 35px; 38 | border-radius: 50%; 39 | } 40 | 41 | .activity-post-portrait.clickable { 42 | cursor: pointer; 43 | } 44 | 45 | .activity-post-author { 46 | color: lightgray; 47 | padding-top: 0px; 48 | padding-left: 8px; 49 | } 50 | 51 | .activity-post-author.clickable { 52 | cursor: pointer; 53 | } 54 | 55 | .activity-post-ori-date { 56 | font-size: 0.8em; 57 | color: lightgray; 58 | } 59 | 60 | .activity-post-action-row { 61 | display: flex; 62 | justify-content: space-around; 63 | } 64 | 65 | .activity-post-action { 66 | display: flex; 67 | align-items: center; 68 | cursor: pointer; 69 | margin-top: 15px; 70 | padding: 5px 10px 0 10px; 71 | /* background-color: yellowgreen; */ 72 | } 73 | 74 | .activity-post-action:hover { 75 | border-radius: 20px; 76 | background-color: yellowgreen; 77 | } 78 | 79 | .activity-post-action-icon { 80 | margin-right: 7px; 81 | } 82 | 83 | .activity-post-arweave-icon { 84 | width: 20px; 85 | height: 20px; 86 | margin-top: 2px; 87 | margin-left: 5px; 88 | cursor: pointer; 89 | } 90 | 91 | .activity-post-action-number { 92 | font-size: 15px; 93 | margin-bottom: 3px; 94 | color: gray; 95 | } 96 | 97 | .activity-post-hashtag { 98 | display: flex; 99 | text-decoration: none; 100 | align-items: center; 101 | font-size: 16px; 102 | } 103 | 104 | .activity-post-topic-image { 105 | width: 28px; 106 | height: 28px; 107 | border-radius: 50%; 108 | margin-right: 8px; 109 | } 110 | 111 | .activity-post-date { 112 | display: flex; 113 | align-items: center; 114 | column-gap: 10px; 115 | } 116 | 117 | .activity-post-content { 118 | padding-left: 55px; 119 | padding-right: 15px; 120 | margin-bottom: 10px; 121 | color: rgb(78, 68, 68); 122 | overflow-wrap: break-word; 123 | } 124 | 125 | .noti-card-label-reply { 126 | display: flex; 127 | align-items: center; 128 | font-size: 12px; 129 | color: white; 130 | height: 13px; 131 | padding: 2px 5px; 132 | margin-left: 10px; 133 | margin-top: 5px; 134 | background-color: rgb(101, 101, 218); 135 | } 136 | 137 | @media (max-width: 650px) { 138 | .story-card-publisher { 139 | font-size: 15px; 140 | } 141 | } -------------------------------------------------------------------------------- /site/src/app/elements/StoryCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { formatTimestamp, getFirstImage, getFirstLine } from '../util/util'; 3 | import './StoryCard.css'; 4 | import { NavLink } from 'react-router-dom'; 5 | import { BsHeartFill, BsPersonFillLock } from 'react-icons/bs'; 6 | import { FaCoins } from "react-icons/fa"; 7 | import { IoMdChatbubbles } from "react-icons/io"; 8 | import parse from 'html-react-parser'; 9 | 10 | interface StoryCardProps { 11 | data: any; 12 | } 13 | 14 | interface StoryCardState { 15 | title: string; 16 | image: string; 17 | } 18 | 19 | class StoryCard extends React.Component { 20 | 21 | constructor(props: StoryCardProps) { 22 | super(props); 23 | this.state = { 24 | title: '', 25 | image: '' 26 | }; 27 | } 28 | 29 | componentDidMount() { 30 | this.getStoryInfo(); 31 | } 32 | 33 | async getStoryInfo() { 34 | let str = this.props.data.post; 35 | 36 | let image = getFirstImage(str); 37 | if (!image) image = './dream.jpg'; 38 | 39 | // To be compatible with the previous version. 40 | let title = this.props.data.title; 41 | if (!title) { 42 | title = getFirstLine(str); 43 | if (!title) title = 'A Fine Stroy!'; 44 | } 45 | 46 | this.setState({ title, image }); 47 | } 48 | 49 | render() { 50 | let data = this.props.data; 51 | let date = new Date(data.time * 1000); 52 | let time = date.toLocaleString(); 53 | 54 | return ( 55 | 56 |
57 | 58 |
59 |
60 |
61 | 62 |
{data.nickname}
63 | {/*
·
*/} 64 |
{formatTimestamp(data.time, true)}
65 | {data.range === 'private' && } 66 |
67 | 68 | {/*
{this.state.title}
*/} 69 |
70 | {parse(this.state.title)} 71 |
72 | 73 | {/*
{data.summary}
*/} 74 | 75 |
76 |
77 | 78 |
{data.coins}
79 |
80 | 81 |
82 | 83 |
{data.replies}
84 |
85 | 86 |
87 | 88 |
{data.likes}
89 |
90 |
91 |
92 |
93 | ) 94 | } 95 | } 96 | 97 | export default StoryCard; -------------------------------------------------------------------------------- /site/src/app/pages/BookmarksPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './BookmarksPage.css'; 3 | import { getDataFromAO, getDefaultProcess, isLoggedIn, messageToAO } from '../util/util'; 4 | import { Server } from '../../server/server'; 5 | import ActivityPost from '../elements/ActivityPost'; 6 | import { BsCloudUpload } from 'react-icons/bs'; 7 | import MessageModal from '../modals/MessageModal'; 8 | 9 | interface BookmarksPageState { 10 | bookmarks: any; 11 | question: string; 12 | alert: string; 13 | message: string; 14 | loading: boolean; 15 | isLoggedIn: string; 16 | address: string; 17 | } 18 | 19 | class BookmarksPage extends React.Component<{}, BookmarksPageState> { 20 | 21 | constructor(props: {}) { 22 | super(props); 23 | this.state = { 24 | bookmarks: [], 25 | question: '', 26 | alert: '', 27 | message: '', 28 | loading: true, 29 | isLoggedIn: '', 30 | address: '', 31 | }; 32 | } 33 | 34 | componentDidMount() { 35 | this.start(); 36 | } 37 | 38 | async start() { 39 | window.scrollTo(0, 0); 40 | let address = await isLoggedIn(); 41 | this.setState({ isLoggedIn: address, address }); 42 | this.getBookmarks(address); 43 | } 44 | 45 | async getBookmarks(address: string) { 46 | let bookmarks = []; 47 | let val = localStorage.getItem('bookmarks'); 48 | if (val) bookmarks = JSON.parse(val); 49 | 50 | this.setState({ bookmarks, loading: false }); 51 | } 52 | 53 | renderBookmarks() { 54 | if (this.state.loading) 55 | return (
Loading...
); 56 | 57 | let bookmarks = this.state.bookmarks; 58 | bookmarks.sort((a: any, b: any) => { 59 | return b.time - a.time; 60 | }); 61 | 62 | let divs = []; 63 | for (let i = 0; i < bookmarks.length; i++) { 64 | let data = bookmarks[i]; 65 | data.isBookmarked = true; 66 | Server.service.addPostToCache(data); 67 | 68 | divs.push( 69 | 73 | ) 74 | } 75 | 76 | return divs.length > 0 ? divs :
No bookmarks yet.
77 | } 78 | 79 | async upload2AO() { 80 | this.setState({ message: 'Upload bookmarks to AO...' }); 81 | 82 | let process = await getDefaultProcess(Server.service.getActiveAddress()); 83 | let resp = await messageToAO( 84 | process, 85 | this.state.bookmarks, 86 | 'AOTwitter.setBookmark' 87 | ); 88 | 89 | this.setState({ message: '' }); 90 | } 91 | 92 | render() { 93 | return ( 94 |
95 |
96 |
Bookmarks
97 | 98 | {this.state.bookmarks.length > 0 && 99 |
this.upload2AO()}> 100 | Upload to AO 101 |
102 | } 103 |
104 | 105 | {this.renderBookmarks()} 106 | 107 |
108 | ) 109 | } 110 | } 111 | 112 | export default BookmarksPage; -------------------------------------------------------------------------------- /site/src/app/elements/NavBarButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, NavLink } from 'react-router-dom'; 3 | import { publish } from '../util/event'; 4 | import './NavBar.css'; 5 | import { BsAward, BsBell, BsBookmark, BsChatText, BsController, BsHouse, BsPerson } from 'react-icons/bs'; 6 | import { ICON_SIZE } from '../util/consts'; 7 | import { AiOutlineFire } from 'react-icons/ai'; 8 | import { Server } from '../../server/server'; 9 | 10 | interface NavBarButtonProps { 11 | // icon:string, 12 | text: string, 13 | to: string, 14 | beta: string, 15 | new: string, 16 | align?: string 17 | } 18 | 19 | interface NavBarButtonState { 20 | isMessagesButton: boolean; 21 | isMessagesPage: boolean; 22 | isFriendUpdated: boolean; 23 | isFriendButton: boolean; 24 | } 25 | 26 | class NavBarButton extends React.Component { 27 | 28 | constructor(props: NavBarButtonProps) { 29 | super(props); 30 | this.state = { 31 | isFriendUpdated: false, 32 | isMessagesPage: false, 33 | isMessagesButton: (this.props.text == 'Messages'), 34 | isFriendButton: (this.props.text == 'Friends'), 35 | }; 36 | 37 | this.onChatUpdated = this.onChatUpdated.bind(this); 38 | this.onFriendUpdated = this.onFriendUpdated.bind(this); 39 | } 40 | 41 | componentDidMount() { 42 | } 43 | 44 | componentWillUnmount() { 45 | } 46 | 47 | onChatUpdated(data: any) { 48 | this.forceUpdate(); 49 | } 50 | 51 | onFriendUpdated(data: any) { 52 | if (data.action == 'connect' || data.action == 'disconnect' || data.action == 'presence') 53 | return; 54 | 55 | if (window.location.pathname != '/friends' && this.state.isFriendButton) 56 | this.setState({ isFriendUpdated: true }); 57 | } 58 | 59 | onClickButton() { 60 | if (this.props.text == 'Profile') { 61 | publish('click-profile-menu'); 62 | } 63 | } 64 | 65 | renderIcon() { 66 | if (this.props.text == 'Home') 67 | return 68 | else if (this.props.text == 'Story') 69 | return 70 | else if (this.props.text == 'Games') 71 | return 72 | else if (this.props.text == 'TokenEco') 73 | return 74 | else if (this.props.text == 'Notifications') 75 | return 76 | else if (this.props.text == 'Bookmarks') 77 | return 78 | else if (this.props.text == 'Chatroom') 79 | return 80 | else if (this.props.text == 'Profile') 81 | return 82 | } 83 | 84 | render() { 85 | return ( 86 | (isActive ? "navbar-link-active" : "navbar-link")} to={this.props.to}> 87 |
this.onClickButton()}> 88 | {this.renderIcon()} 89 |
{this.props.text}
90 | {this.props.beta && 91 |
beta
92 | } 93 | {this.props.new && 94 |
new
95 | } 96 |
97 |
98 | ); 99 | } 100 | } 101 | 102 | export default NavBarButton; -------------------------------------------------------------------------------- /site/src/app/modals/BountyModal.css: -------------------------------------------------------------------------------- 1 | .bounty-modal-content { 2 | padding-left: 25px; 3 | } 4 | 5 | .bounty-modal-header-row { 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | } 10 | 11 | .bounty-modal-header-title { 12 | font-size: 20px; 13 | margin-bottom: 5px; 14 | } 15 | 16 | .bounty-modal-header-balance { 17 | display: flex; 18 | align-items: center; 19 | column-gap: 5px; 20 | color: gray; 21 | margin-right: 105px; 22 | } 23 | 24 | .bounty-modal-header-line { 25 | width: 300px; 26 | height: 1px; 27 | margin-bottom: 20px; 28 | background-color: lightgray; 29 | } 30 | 31 | .bounty-modal-header-line.tokens { 32 | margin-bottom: unset; 33 | } 34 | 35 | .bounty-modal-tokens-title { 36 | color: gray; 37 | font-size: 17px; 38 | margin-top: 15px; 39 | } 40 | 41 | .bounty-modal-token-row { 42 | display: flex; 43 | column-gap: 15px; 44 | margin: 15px 0; 45 | } 46 | 47 | .bounty-modal-token-row.bounty { 48 | margin: 5px 0; 49 | } 50 | 51 | .bounty-modal-token-row.choose { 52 | display: grid; 53 | grid-template-columns: 150px 150px; 54 | grid-gap: 10px; 55 | margin-bottom: 5px; 56 | } 57 | 58 | .bounty-modal-token-row.message { 59 | margin-bottom: 10px; 60 | } 61 | 62 | .bounty-modal-token { 63 | display: flex; 64 | align-items: center; 65 | justify-content: center; 66 | column-gap: 5px; 67 | padding: 2px 15px; 68 | color: white; 69 | cursor: pointer; 70 | background-color: var(--section-header-color); 71 | } 72 | 73 | .bounty-modal-input { 74 | width: 180px; 75 | text-align: center; 76 | font-size: 20px; 77 | } 78 | 79 | .bounty-modal-input.message { 80 | text-align: left; 81 | font-size: unset; 82 | } 83 | 84 | 85 | .bounty-modal-label-unit { 86 | width: 60px; 87 | color: gray; 88 | margin-top: 15px; 89 | /* padding: 2px 5px; */ 90 | /* background-color: rgb(101, 101, 218); */ 91 | } 92 | 93 | .bounty-modal-token.bounty { 94 | border-radius: 5px; 95 | margin-left: 5px; 96 | background-color: var(--fire-color); 97 | } 98 | 99 | .bounty-modal-token.message { 100 | border-radius: 5px; 101 | padding: 2px 20px; 102 | } 103 | 104 | .bounty-modal-token-row { 105 | display: flex; 106 | column-gap: 10px; 107 | } 108 | 109 | .bounty-modal-token-card { 110 | display: flex; 111 | align-items: center; 112 | column-gap: 8px; 113 | padding: 1px 9px; 114 | border-radius: 5px; 115 | cursor: pointer; 116 | background-color: var(--section-color); 117 | } 118 | 119 | .bounty-modal-token-card.picked { 120 | border: 1px solid black; 121 | background-color: var(--section-hover-color); 122 | } 123 | 124 | .bounty-modal-token-name { 125 | font-size: 12px; 126 | color: gray; 127 | } 128 | 129 | .bounty-modal-token-balance { 130 | margin-top: -5px; 131 | } 132 | 133 | .bounty-modal-token-icon { 134 | width: 25px; 135 | height: 25px; 136 | } 137 | 138 | .bounty-modal-token-icon.circle { 139 | border-radius: 50%; 140 | } 141 | 142 | .bounty-modal-token-icon.cred { 143 | width: 25px; 144 | height: unset; 145 | } 146 | 147 | @media (max-width: 650px) { 148 | .modal-content { 149 | max-width: 85%; 150 | } 151 | 152 | .bounty-modal-content { 153 | padding-left: 15px; 154 | } 155 | 156 | .bounty-modal-token { 157 | column-gap: 5px; 158 | padding: 2px 15px; 159 | } 160 | 161 | .bounty-modal-input { 162 | width: 178px; 163 | } 164 | } -------------------------------------------------------------------------------- /site/src/app/pages/ChatPage.css: -------------------------------------------------------------------------------- 1 | .chat-page { 2 | display: flex; 3 | flex-direction: column; 4 | max-width: var(--page-max-width); 5 | margin: auto; 6 | padding-left: 30px; 7 | padding-bottom: 30px; 8 | row-gap: 20px; 9 | } 10 | 11 | .chat-page-messages-container { 12 | display: flex; 13 | flex-direction: column; 14 | align-items: flex-start; 15 | height: 450px; 16 | background-color: rgb(227, 248, 227); 17 | border-radius: 10px; 18 | border: 1px solid lightgray; 19 | padding: 20px 10px; 20 | overflow-y: auto; 21 | } 22 | 23 | .chat-msg-line { 24 | display: flex; 25 | column-gap: 10px; 26 | align-items: center; 27 | margin-bottom: 3px; 28 | } 29 | 30 | .chat-message { 31 | display: flex; 32 | align-items: center; 33 | column-gap: 10px; 34 | max-width: 380px; 35 | background-color: rgb(65, 67, 72); 36 | border-radius: 20px; 37 | padding: 5px 15px; 38 | color: white; 39 | font-size: 16px; 40 | } 41 | 42 | .chat-msg-header { 43 | display: flex; 44 | align-items: center; 45 | column-gap: 10px; 46 | } 47 | 48 | .chat-msg-portrait { 49 | width: 40px; 50 | height: 40px; 51 | border-radius: 50%; 52 | } 53 | 54 | .chat-msg-text { 55 | display: flex; 56 | flex-direction: row; 57 | column-gap: 18px; 58 | align-items: center; 59 | } 60 | 61 | .chat-msg-address { 62 | color: gray; 63 | font-size: 12px; 64 | } 65 | 66 | .other-line { 67 | align-self: flex-start; 68 | } 69 | 70 | .my-line { 71 | align-self: flex-end; 72 | justify-content: flex-end; 73 | } 74 | 75 | .other-message { 76 | border-bottom-left-radius: 0; 77 | } 78 | 79 | .my-message { 80 | border-bottom-right-radius: 0; 81 | background-color: green; 82 | } 83 | 84 | .chat-msg-time { 85 | display: flex; 86 | font-size: 13px; 87 | color: gray; 88 | margin-top: -3px; 89 | } 90 | 91 | .chat-page-send-container { 92 | display: flex; 93 | align-items: center; 94 | justify-content: space-between; 95 | } 96 | 97 | .chat-input-message { 98 | flex-grow: 1; 99 | border-radius: 10px; 100 | border: 1px solid rgb(85, 103, 169); 101 | padding: 5px 10px; 102 | margin-right: 20px; 103 | } 104 | 105 | .chat-send-button { 106 | width: 90px; 107 | height: 50px; 108 | font-size: 18px; 109 | color: black; 110 | background-color: rgb(189, 239, 189); 111 | border-radius: 10px; 112 | border: 2px solid rgb(46, 75, 180); 113 | } 114 | 115 | .chat-page-list-container { 116 | display: flex; 117 | column-gap: 15px; 118 | padding-bottom: 6px; 119 | overflow-x: auto; 120 | } 121 | 122 | .chat-page-list { 123 | display: flex; 124 | align-items: center; 125 | column-gap: 10px; 126 | cursor: pointer; 127 | padding: 5px 10px; 128 | border-radius: 10px; 129 | } 130 | 131 | .chat-page-list:hover { 132 | background-color: rgb(238, 232, 232); 133 | } 134 | 135 | .chat-page-list.selected { 136 | background-color: rgb(238, 232, 232); 137 | } 138 | 139 | .chat-page-list-portrait { 140 | width: 45px; 141 | height: 45px; 142 | border-radius: 50%; 143 | } 144 | 145 | .chat-page-list-nickname { 146 | font-size: 16px; 147 | font-weight: 600; 148 | } 149 | 150 | .chat-page-list-addr { 151 | font-size: 12px; 152 | color: gray; 153 | } 154 | 155 | @media (max-width: 650px) { 156 | .chat-page { 157 | padding: 10px; 158 | padding-bottom: 100px; 159 | } 160 | 161 | .chat-page-messages-container { 162 | height: 430px; 163 | } 164 | } -------------------------------------------------------------------------------- /site/src/app/pages/TokenPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './TokenPage.css'; 3 | import { getWalletAddress, updateTokenBalances } from '../util/util'; 4 | import { TIP_CONN, TOKEN_NAME, TOKEN_ICON } from '../util/consts'; 5 | import MessageModal from '../modals/MessageModal'; 6 | import { Server } from '../../server/server'; 7 | import { subscribe } from '../util/event'; 8 | import Loading from '../elements/Loading'; 9 | 10 | declare var window: any; 11 | 12 | interface TokenPageState { 13 | question: string; 14 | alert: string; 15 | message: string; 16 | loading: boolean; 17 | address: string; 18 | balOfAO: number; 19 | balOfTRUNK: number; 20 | balOfWAR: number; 21 | } 22 | 23 | class TokenPage extends React.Component<{}, TokenPageState> { 24 | 25 | constructor(props: {}) { 26 | super(props); 27 | this.state = { 28 | question: '', 29 | alert: '', 30 | message: '', 31 | loading: true, 32 | address: '', 33 | balOfAO: 0, 34 | balOfWAR: 0, 35 | balOfTRUNK: 0, 36 | }; 37 | 38 | subscribe('wallet-events', () => { 39 | this.start(); 40 | }); 41 | } 42 | 43 | componentDidMount() { 44 | this.start(); 45 | } 46 | 47 | async start() { 48 | let address = await getWalletAddress(); 49 | console.log("address:", address) 50 | this.setState({ address, loading: true }); 51 | 52 | if (!Server.service.getBalanceOfTRUNK()) { 53 | await updateTokenBalances(address); 54 | } 55 | 56 | let balOfAO = Number(Server.service.getBalanceOfAO().toFixed(5)); 57 | let balOfWAR = Number(Server.service.getBalanceOfWAR().toFixed(5)); 58 | let balOfTRUNK = Server.service.getBalanceOfTRUNK(); 59 | 60 | this.setState({ balOfAO, balOfWAR, balOfTRUNK, loading: false }); 61 | } 62 | 63 | renderTokens() { 64 | let divs = []; 65 | let balances = [this.state.balOfAO, this.state.balOfWAR, this.state.balOfTRUNK]; 66 | 67 | for (let i = 0; i < balances.length; i++) { 68 | let tokenName = TOKEN_NAME.get(i); 69 | let tokenIcon = TOKEN_ICON.get(tokenName); 70 | divs.push( 71 |
72 | 73 |
74 |
{tokenName}
75 | {this.state.loading 76 | ? 77 | :
{balances[i]}
78 | } 79 |
80 |
81 | ) 82 | } 83 | 84 | return divs; 85 | } 86 | 87 | render() { 88 | let isLoggedIn = Server.service.isLoggedIn(); 89 | let address = this.state.address; 90 | if (!isLoggedIn) address = TIP_CONN; 91 | 92 | return ( 93 |
94 |
95 |
Active wallet address
96 |
{address}
97 |
98 | 99 |
Balances
100 |
101 | 102 |
103 | {this.renderTokens()} 104 |
105 | 106 | 107 |
108 | ) 109 | } 110 | } 111 | 112 | export default TokenPage; -------------------------------------------------------------------------------- /site/src/app/modals/DMModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BsFillXCircleFill } from 'react-icons/bs'; 3 | import AlertModal from './AlertModal'; 4 | import './Modal.css' 5 | import './BountyModal.css' 6 | import MessageModal from './MessageModal'; 7 | import { messageToAO, timeOfNow } from '../util/util'; 8 | import { AiOutlineFire } from 'react-icons/ai'; 9 | import { Server } from '../../server/server'; 10 | import { AO_TWITTER } from '../util/consts'; 11 | import { CiMail } from 'react-icons/ci'; 12 | 13 | interface DMModalProps { 14 | open: boolean; 15 | onClose: Function; 16 | friend: string; 17 | } 18 | 19 | interface DMModalState { 20 | message: string; 21 | alert: string; 22 | msg: string; 23 | } 24 | 25 | class DMModal extends React.Component { 26 | 27 | constructor(props: DMModalProps) { 28 | super(props); 29 | 30 | this.state = { 31 | message: '', 32 | alert: '', 33 | msg: '', 34 | } 35 | 36 | this.onClose = this.onClose.bind(this); 37 | this.onChangeMessage = this.onChangeMessage.bind(this); 38 | } 39 | 40 | onChangeMessage(e: any) { 41 | this.setState({ msg: e.currentTarget.value }); 42 | } 43 | 44 | onClose(sent?:boolean) { 45 | this.props.onClose(sent); 46 | } 47 | 48 | async sendMessage() { 49 | let msg = this.state.msg.trim(); 50 | if (!msg) { 51 | this.setState({ alert: 'Please input a message.' }) 52 | return; 53 | } else if (msg.length > 500) { 54 | this.setState({ alert: 'Message can be up to 500 characters long.' }) 55 | return; 56 | } 57 | 58 | this.setState({ message: 'Message...' }); 59 | 60 | let data = { 61 | address: Server.service.getActiveAddress(), 62 | friend: this.props.friend, 63 | message: msg, 64 | time: timeOfNow() 65 | }; 66 | 67 | // console.log('dm data ',data) 68 | await messageToAO(AO_TWITTER, data, 'SendMessage'); 69 | 70 | this.setState({ message: '' }); 71 | this.onClose(true); 72 | } 73 | 74 | handleKeyDown = (e: any) => { 75 | if (e.key === 'Enter') { 76 | e.preventDefault(); 77 | this.sendMessage(); 78 | } 79 | } 80 | 81 | render() { 82 | if (!this.props.open) 83 | return (
); 84 | 85 | return ( 86 |
e.stopPropagation()}> 87 |
88 | 91 | 92 |
93 |
Message
94 |
95 | 96 |
97 |
Send a DM message to friend.
98 | 99 |
100 | 107 | 108 |
this.sendMessage()}> 109 |  Send 110 |
111 |
112 |
113 | 114 | 115 | this.setState({ alert: '' })} /> 116 |
117 | ) 118 | } 119 | } 120 | 121 | export default DMModal; -------------------------------------------------------------------------------- /site/src/app/pages/ProfilePage.css: -------------------------------------------------------------------------------- 1 | .profile-page { 2 | display: flex; 3 | flex-direction: column; 4 | max-width: var(--page-max-width); 5 | margin: auto; 6 | padding-left: 30px; 7 | padding-bottom: 30px; 8 | } 9 | 10 | .profile-page-header { 11 | position: relative; 12 | } 13 | 14 | .profile-page-banner { 15 | width: 100%; 16 | } 17 | 18 | .profile-page-portrait { 19 | width: 100px; 20 | height: 100px; 21 | position: absolute; 22 | bottom: -45px; 23 | left: 10px; 24 | border-radius: 50%; 25 | } 26 | 27 | .profile-page-user-online { 28 | width: 16px; 29 | height: 16px; 30 | border-radius: 50%; 31 | background-color: green; 32 | position: absolute; 33 | bottom: 32px; 34 | left: 87px; 35 | } 36 | 37 | .profile-page-button-container { 38 | display: flex; 39 | margin-top: 5px; 40 | margin-bottom: 10px; 41 | margin-right: 10px; 42 | justify-content: flex-end; 43 | } 44 | 45 | @media (min-width: 600px) { 46 | .profile-page-button-container { 47 | margin-right: 0px; 48 | } 49 | } 50 | 51 | .profile-page-action-button { 52 | width: 42px; 53 | height: 40px; 54 | font-size: 18pt; 55 | display: flex; 56 | justify-content: center; 57 | align-items: center; 58 | margin-right: 15px; 59 | border: 1px solid gray; 60 | border-radius: 50%; 61 | cursor: pointer; 62 | } 63 | 64 | .profile-page-name { 65 | /* color: white; */ 66 | font-size: 1.2em; 67 | margin-left: 20px; 68 | } 69 | 70 | .profile-page-id { 71 | margin-left: 20px; 72 | font-size: 0.8em; 73 | margin-top: 5px; 74 | color: gray; 75 | } 76 | 77 | .profile-page-slug { 78 | margin-left: 20px; 79 | } 80 | 81 | .profile-page-desc { 82 | /* color: white; */ 83 | margin-top: 10px; 84 | margin-left: 20px; 85 | margin-right: 20px; 86 | } 87 | 88 | .profile-page-joined-container { 89 | display: flex; 90 | align-items: center; 91 | margin-left: 20px; 92 | margin-top: 15px; 93 | color: gray; 94 | } 95 | 96 | .profile-page-joined { 97 | margin-left: 10px; 98 | font-size: 15px; 99 | } 100 | 101 | .profile-page-posts { 102 | display: flex; 103 | flex-direction: column; 104 | row-gap: 10px; 105 | margin-top: 10px; 106 | } 107 | 108 | .profile-page-navlink { 109 | cursor: pointer; 110 | /* color: #ffffffa0; */ 111 | } 112 | 113 | .profile-page-navlink:visited { 114 | color: #ffffffa0; 115 | } 116 | 117 | .profile-page-filter-container { 118 | display: flex; 119 | /* border-radius: 10px; */ 120 | padding: 8px 15px; 121 | margin: 15px 0px; 122 | background-color: var(--section-header-color); 123 | } 124 | 125 | .profile-page-filter { 126 | margin: 0 10px; 127 | cursor: pointer; 128 | color: lightgray; 129 | } 130 | 131 | .profile-page-filter.selected { 132 | border-bottom: 1px solid white; 133 | color: white; 134 | cursor: default; 135 | } 136 | 137 | .profile-page-follow-button { 138 | display: flex; 139 | align-items: center; 140 | justify-content: center; 141 | width: 70px; 142 | height: 20px; 143 | color: white; 144 | padding: 10px 20px; 145 | border-radius: 30px; 146 | background-color: black; 147 | cursor: pointer; 148 | } 149 | 150 | .profile-page-follow-button.following { 151 | color: black; 152 | background-color: white; 153 | border: 1px solid gray; 154 | } 155 | 156 | .profile-page-follow-button.unfollow { 157 | color: red; 158 | background-color: rgb(239, 233, 233); 159 | border: 1px solid gray; 160 | } 161 | 162 | .profile-page-follow-container { 163 | display: flex; 164 | column-gap: 20px; 165 | margin-left: 20px; 166 | margin-top: 15px; 167 | } 168 | 169 | .profile-page-follow-link { 170 | display: flex; 171 | column-gap: 5px; 172 | cursor: pointer; 173 | border-bottom: 1px solid transparent; 174 | text-decoration: none; 175 | } 176 | 177 | .profile-page-follow-link:hover { 178 | border-bottom: 1px solid lightgray; 179 | } 180 | 181 | .profile-page-follow-number { 182 | color: black; 183 | font-weight: 600; 184 | } 185 | 186 | .profile-page-follow-text { 187 | color: gray; 188 | } 189 | 190 | @media (max-width: 650px) { 191 | .profile-page { 192 | padding: 10px; 193 | padding-bottom: 60px; 194 | } 195 | } -------------------------------------------------------------------------------- /site/src/app/pages/NotiPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './NotiPage.css'; 3 | import { getDataFromAO, getDefaultProcess, getWalletAddress, isLoggedIn, uuid } from '../util/util'; 4 | import { PAGE_SIZE } from '../util/consts'; 5 | import Loading from '../elements/Loading'; 6 | import { Server } from '../../server/server'; 7 | import NotiCard from '../elements/NotiCard'; 8 | 9 | interface NotiPageState { 10 | notis: any; 11 | loading: boolean; 12 | loadNextPage: boolean; 13 | open: boolean; 14 | isAll: boolean; 15 | } 16 | 17 | class NotiPage extends React.Component<{}, NotiPageState> { 18 | 19 | constructor(props: {}) { 20 | super(props); 21 | this.state = { 22 | notis: [], 23 | loading: true, 24 | loadNextPage: false, 25 | open: false, 26 | isAll: false, 27 | }; 28 | 29 | 30 | this.onOpen = this.onOpen.bind(this); 31 | this.onClose = this.onClose.bind(this); 32 | this.atBottom = this.atBottom.bind(this); 33 | 34 | // subscribe('wallet-events', () => { 35 | // this.forceUpdate(); 36 | // }); 37 | } 38 | 39 | componentDidMount() { 40 | // if (!Server.service.getIsLoggedIn()) return; 41 | 42 | this.getNotis(); 43 | window.addEventListener('scroll', this.atBottom); 44 | } 45 | 46 | componentWillUnmount(): void { 47 | // clearInterval(this.refresh); 48 | window.removeEventListener('scroll', this.atBottom); 49 | } 50 | 51 | atBottom() { 52 | const scrollHeight = document.documentElement.scrollHeight; 53 | const scrollTop = document.documentElement.scrollTop; 54 | const clientHeight = document.documentElement.clientHeight; 55 | 56 | if (scrollTop + clientHeight + 300 >= scrollHeight) { 57 | setTimeout(() => { 58 | if (!this.state.loading && !this.state.loadNextPage && !this.state.isAll) 59 | this.nextPage(); 60 | }, 200); 61 | } 62 | } 63 | 64 | onOpen() { 65 | this.setState({ open: true }); 66 | } 67 | 68 | onClose(data: any) { 69 | // console.log("onClose:", data) 70 | this.setState({ open: false }); 71 | if (data) { 72 | this.getNotis(); 73 | // this.setState({ posts: [], loading: true, isAll: false }); 74 | this.setState({ isAll: false }); 75 | } 76 | } 77 | 78 | // async start() { 79 | // await this.getStory(); 80 | // } 81 | 82 | async getNotis() { 83 | let address = await isLoggedIn(); 84 | let process = await getDefaultProcess(address); 85 | 86 | let notis = await getDataFromAO(process, 'Get-Notis', { offset: 0 }); 87 | // console.log("notis:", notis) 88 | 89 | if (notis.length < PAGE_SIZE) 90 | this.setState({ isAll: true }) 91 | 92 | this.setState({ notis, loading: false }); 93 | } 94 | 95 | async nextPage() { 96 | let process = Server.service.getDefaultProcess(); 97 | // console.log("process:", process) 98 | 99 | this.setState({ loadNextPage: true }); 100 | 101 | let offset = this.state.notis.length.toString(); 102 | // console.log("offset:", offset) 103 | 104 | let notis = await getDataFromAO(process, 'Get-Notis', { offset }); 105 | // console.log("notis:", notis) 106 | if (notis.length < PAGE_SIZE) 107 | this.setState({ isAll: true }) 108 | 109 | let total = this.state.notis.concat(notis); 110 | this.setState({ notis: total, loadNextPage: false }); 111 | } 112 | 113 | renderNotis() { 114 | if (this.state.loading) return (); 115 | 116 | let divs = []; 117 | for (let i = 0; i < this.state.notis.length; i++) { 118 | divs.push( 119 | 120 | ) 121 | } 122 | 123 | return divs 124 | } 125 | 126 | render() { 127 | if (!Server.service.isLoggedIn()) 128 | return (
Please login first.
) 129 | 130 | return ( 131 |
132 |
133 |
Notifications
134 |
135 | 136 | {this.renderNotis()} 137 | 138 | {this.state.loadNextPage && } 139 | {this.state.isAll && 140 |
141 | No more notifications. 142 |
143 | } 144 |
145 | ) 146 | } 147 | } 148 | 149 | export default NotiPage; -------------------------------------------------------------------------------- /site/src/server/service.ts: -------------------------------------------------------------------------------- 1 | export class Service { 2 | protected profiles:any; 3 | protected posts:any; 4 | protected post:any; 5 | protected postsInProfile:any; 6 | protected position:number; 7 | protected positionInProfile:number; 8 | 9 | protected stories_all:any; 10 | protected stories_project:any; 11 | protected stories_top:any; 12 | protected story:any; 13 | 14 | protected addrLogIn:string; 15 | protected activeAddress:string; 16 | protected defaultProcess:string; 17 | 18 | protected balanceOfAO:number; 19 | protected balanceOfTRUNK:number; 20 | protected balanceOfWAR:number; 21 | protected balanceOf0rbit:number; 22 | protected balanceOfUSDA:number; 23 | 24 | protected storyTab:number = 0; 25 | 26 | constructor() { 27 | this.profiles = []; 28 | this.post = []; 29 | this.postsInProfile = []; 30 | this.story = []; 31 | } 32 | 33 | public getProfile(id:string) { 34 | return this.profiles[id]; 35 | } 36 | 37 | public addProfileToCache(profile:any) { 38 | this.profiles[profile.address] = profile; 39 | } 40 | 41 | public addPositionToCache(position:number) { 42 | this.position = position; 43 | } 44 | 45 | public getPositionFromCache() { 46 | return this.position; 47 | } 48 | 49 | public addPositionInProfileToCache(position:number) { 50 | this.positionInProfile = position; 51 | } 52 | 53 | public getPositionInProfileFromCache() { 54 | return this.positionInProfile; 55 | } 56 | 57 | public addPostsToCache(posts:any) { 58 | this.posts = posts; 59 | } 60 | 61 | public getPostsFromCache() { 62 | return this.posts; 63 | } 64 | 65 | public addPostToCache(post:any) { 66 | this.post[post.id] = post; 67 | } 68 | 69 | public getPostFromCache(id:string) { 70 | return this.post[id]; 71 | } 72 | 73 | public addPostsInProfileToCache(id: string, posts:any) { 74 | this.postsInProfile[id] = posts; 75 | } 76 | 77 | public getPostsInProfileFromCache(id:string) { 78 | return this.postsInProfile[id]; 79 | } 80 | 81 | public setIsLoggedIn(address:string) { 82 | this.addrLogIn = address; 83 | } 84 | 85 | public isLoggedIn() { 86 | return this.addrLogIn; 87 | } 88 | 89 | public setActiveAddress(activeAddress:string) { 90 | this.activeAddress = activeAddress; 91 | } 92 | 93 | public getActiveAddress() { 94 | return this.activeAddress; 95 | } 96 | 97 | public setDefaultProcess(process:string) { 98 | this.defaultProcess = process; 99 | } 100 | 101 | public getDefaultProcess() { 102 | return this.defaultProcess; 103 | } 104 | 105 | public setBalanceOfAO(bal:number) { 106 | this.balanceOfAO = bal; 107 | } 108 | 109 | public getBalanceOfAO() { 110 | return this.balanceOfAO; 111 | } 112 | 113 | public setBalanceOfTRUNK(bal:number) { 114 | this.balanceOfTRUNK = bal; 115 | } 116 | 117 | public getBalanceOfTRUNK() { 118 | return this.balanceOfTRUNK; 119 | } 120 | 121 | public setBalanceOfWAR(bal:number) { 122 | this.balanceOfWAR = bal; 123 | } 124 | 125 | public getBalanceOfWAR() { 126 | return this.balanceOfWAR; 127 | } 128 | 129 | public setBalanceOf0rbit(bal:number) { 130 | this.balanceOf0rbit = bal; 131 | } 132 | 133 | public getBalanceOf0rbit() { 134 | return this.balanceOf0rbit; 135 | } 136 | 137 | public setBalanceOfUSDA(bal:number) { 138 | this.balanceOfUSDA = bal; 139 | } 140 | 141 | public getBalanceOfUSDA() { 142 | return this.balanceOfUSDA; 143 | } 144 | 145 | public setStoryTab(tab:number) { 146 | this.storyTab = tab; 147 | } 148 | 149 | public getStoryTab() { 150 | return this.storyTab; 151 | } 152 | 153 | public addAllStoriesToCache(stories:any) { 154 | this.stories_all = stories; 155 | } 156 | 157 | public getAllStoriesFromCache() { 158 | return this.stories_all; 159 | } 160 | 161 | 162 | public addProjectStoriesToCache(stories:any) { 163 | this.stories_project = stories; 164 | } 165 | 166 | public getProjectStoriesFromCache() { 167 | return this.stories_project; 168 | } 169 | 170 | 171 | public addTopStoriesToCache(stories:any) { 172 | this.stories_top = stories; 173 | } 174 | 175 | public getTopStoriesFromCache() { 176 | return this.stories_top; 177 | } 178 | 179 | public addStoryToCache(story:any) { 180 | this.story[story.id] = story; 181 | } 182 | 183 | public getStoryFromCache(id:string) { 184 | return this.story[id]; 185 | } 186 | } -------------------------------------------------------------------------------- /site/src/app/elements/ActivityPost.css: -------------------------------------------------------------------------------- 1 | .activity-post-row { 2 | background-color: var(--section-color); 3 | border-bottom-right-radius: 10px; 4 | border-bottom-left-radius: 10px; 5 | padding: 10px; 6 | padding-bottom: 5px; 7 | } 8 | 9 | .activity-post-row-header { 10 | border-top-right-radius: 10px; 11 | border-top-left-radius: 10px; 12 | background-color: var(--section-header-color); 13 | font-size: 0.8em; 14 | color: lightgray; 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | padding: 0 10px; 19 | height: 35px; 20 | } 21 | 22 | .activity-post-navlink { 23 | display: flex; 24 | color: white; 25 | align-items: center; 26 | text-decoration: none; 27 | } 28 | 29 | .activity-post-profile { 30 | display: flex; 31 | align-items: center; 32 | margin-bottom: 15px; 33 | } 34 | 35 | .activity-post-portrait { 36 | width: 35px; 37 | height: 35px; 38 | border-radius: 50%; 39 | } 40 | 41 | .activity-post-portrait.clickable { 42 | cursor: pointer; 43 | } 44 | 45 | .activity-post-author { 46 | color: lightgray; 47 | padding-top: 0px; 48 | padding-left: 8px; 49 | } 50 | 51 | .activity-post-author.clickable { 52 | cursor: pointer; 53 | } 54 | 55 | .activity-post-ori-date { 56 | font-size: 0.8em; 57 | color: lightgray; 58 | } 59 | 60 | .activity-post-action-row { 61 | display: flex; 62 | justify-content: space-around; 63 | } 64 | 65 | .activity-post-action { 66 | display: flex; 67 | align-items: center; 68 | cursor: pointer; 69 | margin-top: 15px; 70 | padding: 5px 10px 0 10px; 71 | /* background-color: yellowgreen; */ 72 | } 73 | 74 | .activity-post-action:hover { 75 | border-radius: 20px; 76 | background-color: yellowgreen; 77 | } 78 | 79 | .activity-post-action-icon { 80 | margin-right: 7px; 81 | } 82 | 83 | .activity-post-arweave-icon { 84 | width: 20px; 85 | height: 20px; 86 | margin-top: 2px; 87 | margin-left: 5px; 88 | cursor: pointer; 89 | } 90 | 91 | .activity-post-action-number { 92 | font-size: 15px; 93 | margin-bottom: 3px; 94 | color: gray; 95 | } 96 | 97 | .activity-post-hashtag { 98 | display: flex; 99 | text-decoration: none; 100 | align-items: center; 101 | font-size: 16px; 102 | } 103 | 104 | .activity-post-topic-image { 105 | width: 28px; 106 | height: 28px; 107 | border-radius: 50%; 108 | margin-right: 8px; 109 | } 110 | 111 | .activity-post-date { 112 | display: flex; 113 | align-items: center; 114 | column-gap: 10px; 115 | } 116 | 117 | .activity-post-content { 118 | display: flex; 119 | flex-direction: column; 120 | /* row-gap: 15px; */ 121 | padding-left: 55px; 122 | padding-right: 15px; 123 | margin-bottom: 10px; 124 | color: rgb(78, 68, 68); 125 | overflow-wrap: break-word; 126 | } 127 | 128 | .activity-post-poll-container { 129 | display: flex; 130 | flex-direction: column; 131 | row-gap: 10px; 132 | margin-top: 10px; 133 | } 134 | 135 | .activity-post-nickname { 136 | font-weight: 600; 137 | font-size: 17px; 138 | margin-top: -1px; 139 | } 140 | 141 | .poll-option-row { 142 | display: flex; 143 | position: relative; 144 | justify-content: space-between; 145 | padding: 2px 15px; 146 | } 147 | 148 | .poll-option-text { 149 | display: flex; 150 | align-items: center; 151 | column-gap: 15px; 152 | position: relative; 153 | z-index: 1; 154 | } 155 | 156 | .poll-option-progress { 157 | position: absolute; 158 | top: 0; 159 | left: 0; 160 | height: 100%; 161 | border-radius: 5px; 162 | background-color: #85c8ff; 163 | z-index: 0; 164 | } 165 | 166 | .poll-option-progress.zero { 167 | background-color: lightgray; 168 | } 169 | 170 | .poll-option-percentage-area { 171 | display: flex; 172 | column-gap: 5px; 173 | } 174 | 175 | .poll-option-percentage { 176 | position: relative; 177 | /* font-weight: 600; */ 178 | font-size: 15px; 179 | } 180 | 181 | .poll-option-count { 182 | position: relative; 183 | color: gray; 184 | font-size: 13px; 185 | } 186 | 187 | .poll-option-bottom { 188 | color: gray; 189 | font-size: 13px; 190 | } 191 | 192 | .poll-token-row { 193 | display: flex; 194 | /* color: gray; */ 195 | font-style: italic; 196 | font-size: 13px; 197 | column-gap: 10px; 198 | padding: 3px 8px; 199 | border-radius: 5px; 200 | background-color: #71c094; 201 | } 202 | 203 | .poll-token-logo { 204 | width: 25px; 205 | height: 25px; 206 | border-radius: 50%; 207 | } 208 | 209 | .poll-token-name { 210 | font-weight: 600; 211 | } 212 | 213 | @media (max-width: 650px) { 214 | .activity-post-nickname { 215 | font-size: 15px; 216 | } 217 | } -------------------------------------------------------------------------------- /site/src/app/elements/SharedQuillEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactQuill from 'react-quill'; 3 | import 'react-quill/dist/quill.snow.css'; 4 | 5 | //-------------------------- 6 | // --> SETTINGS FOR QUILL 7 | // Add the audio supporting 8 | 9 | const Quill = ReactQuill.Quill; 10 | const BlockEmbed = Quill.import('blots/block/embed'); 11 | 12 | class AudioBlot extends BlockEmbed { 13 | static create(url: any) { 14 | let node = super.create(); 15 | node.setAttribute('src', url); 16 | node.setAttribute('controls', ''); 17 | return node; 18 | } 19 | 20 | static value(node: any) { 21 | return node.getAttribute('src'); 22 | } 23 | } 24 | 25 | AudioBlot.blotName = 'audio'; 26 | AudioBlot.tagName = 'audio'; 27 | Quill.register(AudioBlot); 28 | 29 | // <-- SETTINGS FOR QUILL 30 | //-------------------------- 31 | 32 | interface SharedQuillEditorProps { 33 | placeholder?: string; 34 | isActivity?: boolean; 35 | hasFontSize?: boolean; 36 | onChange?: Function; 37 | getRef: Function; 38 | } 39 | 40 | interface SharedQuillEditorState { 41 | content: string; 42 | openAddMedia: boolean; 43 | } 44 | 45 | class SharedQuillEditor extends React.Component { 46 | 47 | quillRef: any; 48 | quillPosition: number; 49 | reactQuillRef: any; 50 | quillModules: any; 51 | quillFormats: any; 52 | 53 | constructor(props: SharedQuillEditorProps) { 54 | super(props); 55 | 56 | this.state = { 57 | content: '', 58 | openAddMedia: false 59 | } 60 | 61 | this.quillRef = null; 62 | this.reactQuillRef = null; 63 | this.quillPosition = 0; 64 | 65 | this.onContentChange = this.onContentChange.bind(this); 66 | this.onContentChangeSelection = this.onContentChangeSelection.bind(this); 67 | this.onInsertMedia = this.onInsertMedia.bind(this); 68 | this.onQuillImage = this.onQuillImage.bind(this); 69 | } 70 | 71 | componentDidMount() { 72 | this.attachQuillRefs(); 73 | } 74 | 75 | attachQuillRefs() { 76 | // Ensure React-Quill reference is available: 77 | if (typeof this.reactQuillRef.getEditor !== 'function') return; 78 | 79 | // Skip if Quill reference is defined: 80 | if (this.quillRef != null) return; 81 | 82 | const quillRef = this.reactQuillRef.getEditor(); 83 | if (quillRef != null) { 84 | this.quillRef = quillRef; 85 | this.props.getRef(quillRef); 86 | } 87 | } 88 | 89 | onContentChange(value: any) { 90 | this.setState({content: value}); 91 | 92 | if (this.props.onChange) { 93 | let text = this.quillRef.getText().trim().replaceAll(' ', ''); 94 | text = text.replace(/[\r\n]/g, ''); // remove \n (enter) 95 | this.props.onChange(text.length); 96 | } 97 | }; 98 | 99 | onContentChangeSelection() { 100 | let range = this.quillRef.getSelection(); 101 | if (range) { 102 | this.quillPosition = range.index; 103 | } 104 | }; 105 | 106 | onQuillImage() { 107 | this.setState({openAddMedia: true}); 108 | } 109 | 110 | onInsertMedia(data: any) { 111 | this.setState({openAddMedia: false}); 112 | this.quillRef.insertEmbed(this.quillPosition, data.category, data.url); 113 | } 114 | 115 | setQuill() { 116 | let container = [ 117 | this.props.hasFontSize && {'header': [1, 2, 3, false]}, 118 | 'bold', 'italic', 'underline', 'strike', 119 | {'align': ''}, {'align': 'center'}, {'align': 'right'}, 120 | 'link', 'image', 'video' 121 | ]; 122 | 123 | // if (this.props.isActivity) 124 | // container = [ 125 | // 'bold', 'italic', 'underline', 'strike', 126 | // {'align': ''}, {'align': 'center'}, {'align': 'right'}, 127 | // {'list': 'ordered'}, {'list': 'bullet'}, {'indent': '-1'}, {'indent': '+1'}, 'link', 'image' 128 | // ]; 129 | 130 | this.quillModules = { 131 | toolbar: { 132 | // handlers: { 133 | // image: this.onQuillImage, 134 | // }, 135 | container: [container] 136 | } 137 | }; 138 | 139 | // if (this.props.isActivity) 140 | // this.quillModules = {toolbar: null}; 141 | 142 | this.quillFormats = ['header', 'bold', 'italic', 'underline', 'strike', 'list', 'bullet', 'indent', 'align', 'link', 'image', 'video', 'audio']; 143 | // if (this.props.isActivity) 144 | // this.quillFormats = ['image', 'audio']; 145 | } 146 | 147 | render() { 148 | this.setQuill(); 149 | 150 | return ( 151 |
152 | { this.reactQuillRef = el }} 161 | /> 162 |
163 | ) 164 | } 165 | } 166 | 167 | export default SharedQuillEditor; -------------------------------------------------------------------------------- /site/src/app/elements/NotiCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { convertUrlsToLinks, shortAddr } from '../util/util'; 3 | import { formatTimestamp } from '../util/util'; 4 | import './NotiCard.css'; 5 | import parse, { attributesToProps } from 'html-react-parser'; 6 | import { Navigate } from 'react-router-dom'; 7 | import ViewImageModal from '../modals/ViewImageModal'; 8 | import AlertModal from '../modals/AlertModal'; 9 | import { Service } from '../../server/service'; 10 | import { subscribe } from '../util/event'; 11 | import { Tooltip } from 'react-tooltip' 12 | import MessageModal from '../modals/MessageModal'; 13 | import ExternalEmbed from './externalEmbed'; 14 | 15 | interface NotiCardProps { 16 | data: any; 17 | } 18 | 19 | interface NotiCardState { 20 | openImage: boolean; 21 | navigate: string; 22 | content: string; 23 | message: string; 24 | alert: string; 25 | } 26 | 27 | class NotiCard extends React.Component { 28 | 29 | id: string; 30 | imgUrl: string; 31 | loading: boolean = false; 32 | 33 | static service: Service = new Service(); 34 | 35 | parseOptions = { 36 | replace: (domNode: any) => { 37 | if (domNode.attribs && domNode.name === 'img') { 38 | const props = attributesToProps(domNode.attribs); 39 | return this.tapImage(e, props.src)} {...props} />; 40 | } else if (domNode.name === 'span' && domNode.attribs) { 41 | if (domNode.attribs.class === 'youtube-url') { 42 | return ; 43 | } 44 | } 45 | } 46 | }; 47 | 48 | constructor(props: NotiCardProps) { 49 | super(props); 50 | this.state = { 51 | openImage: false, 52 | navigate: '', 53 | content: '', 54 | message: '', 55 | alert: '', 56 | }; 57 | 58 | this.onClose = this.onClose.bind(this); 59 | 60 | subscribe('wallet-events', () => { 61 | this.forceUpdate(); 62 | }); 63 | } 64 | 65 | componentDidMount() { 66 | this.start(); 67 | 68 | const links = document.querySelectorAll("[id^='url']"); 69 | for (let i = 0; i < links.length; i++) { 70 | links[i].addEventListener('click', function (e) { 71 | e.stopPropagation(); 72 | }); 73 | } 74 | } 75 | 76 | componentWillUnmount() { 77 | const links = document.querySelectorAll("[id^='url']"); 78 | for (let i = 0; i < links.length; i++) { 79 | links[i].removeEventListener('click', function (e) { 80 | e.stopPropagation(); 81 | }); 82 | } 83 | } 84 | 85 | async start() { 86 | this.getPostContent(); 87 | } 88 | 89 | async getPostContent() { 90 | let content = this.props.data.post; 91 | // content = convertHashTag(content); 92 | content = convertUrlsToLinks(content); 93 | this.setState({ content }); 94 | } 95 | 96 | tapImage(e: any, src: string) { 97 | e.stopPropagation(); 98 | this.imgUrl = src; 99 | this.setState({ openImage: true }) 100 | } 101 | 102 | goProfilePage(e: any, id: string) { 103 | e.stopPropagation(); 104 | this.setState({ navigate: '/user/' + id }); 105 | } 106 | 107 | goPostPage(id: string) { 108 | this.setState({ navigate: "/post/" + id }); 109 | } 110 | 111 | onClose() { 112 | this.setState({ openImage: false }); 113 | } 114 | 115 | render() { 116 | let data = this.props.data; 117 | 118 | if (this.state.navigate) 119 | return ; 120 | 121 | return ( 122 |
this.goPostPage(data.post_id)} 126 | > 127 |
128 | this.goProfilePage(e, data.address)} 134 | title='Show Profile' 135 | // onMouseEnter={()=>this.openPopup()} 136 | // onMouseLeave={(e)=>this.closePopup(e)} 137 | /> 138 |
139 | {data.nickname} 140 |
141 | 142 |
{shortAddr(data.address, 4)}
143 |
144 | ·  {formatTimestamp(data.time)} 145 |
146 |
REPLY
147 |
148 | 149 |
150 | {parse(this.state.content, this.parseOptions)} 151 |
152 | 153 | 154 | 155 | this.setState({ alert: '' })} /> 156 | 157 |
158 | ) 159 | } 160 | } 161 | 162 | export default NotiCard; -------------------------------------------------------------------------------- /site/src/app/util/consts.ts: -------------------------------------------------------------------------------- 1 | export const regexPatterns = { 2 | urlRegex: /(\b(https?:\/\/|www\.)[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig, 3 | imageRegex: /\.(jpeg|jpg|gif|png|svg)$/i, 4 | hrefRegex: /]*?href\s*=\s*(['"])(.*?)\1/g, 5 | youtubeRegex: /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/ 6 | }; 7 | 8 | export const AO_TWITTER = "8s1ZpAx_NueKS4N2ZOMYWCkl5qVcGkgnBFSnqSVX9Fo"; 9 | export const AO_STORY = "AAwa2zqVLSMvxOPMjhUVtPHS_SN1ObbsaY27X9OPCbw"; 10 | // export const AO_STORY = "HvIVoTF2Z-UaIYxHaFLWnMj5qXcwD3pKS2YI74xwZR0"; 11 | export const STORY_INCOME = "LsNy8F1GSkGvE0IJ6g1RFpHHjKE6tmtXUT91WIv3PMQ"; 12 | 13 | export const AO = "0syT13r0s0tgPmIed95bJnuSqaD29HQNN8D3ElLSrsc"; 14 | export const TRUNK = "wOrb8b_V8QixWyXZub48Ki5B6OIDyf_p1ngoonsaRpQ"; 15 | export const WAR = "xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10"; 16 | export const WUSDC = "7zH9dlMNoxprab9loshv3Y7WG45DOny_Vrq9KrXObdQ"; 17 | export const BP = "_HbnZH5blAZH0CNT1k_dpRrGXWCzBg34hjMUkoDrXr0"; 18 | export const ORBT = "BUhZLMwQ6yZHguLtJYA5lLUa9LQzLXMXRfaq9FVcPJc"; 19 | export const USDA = "GcFxqTQnKHcr304qnOcq00ZqbaYGDn4Wbb0DHAM-wvU"; 20 | 21 | export const CHATROOM = "F__i_YGIUOGw43zyqLY9dEKNNEhB_uTqzL9tOTWJ-KA"; 22 | 23 | export const TIP_IMG = "Got an issue, images in the post can be up to ~500KB."; 24 | export const TIP_VOTE = "Vote failed. Please try again." 25 | export const ICON_SIZE = 28; 26 | export const PAGE_SIZE = "10"; 27 | export const TIP_CONN = "You should connect to wallet first."; 28 | 29 | export const AR_DEC = 1000000000000; // For Wrapped AR 30 | 31 | // Supporting the AO SQLite 32 | export const MODULE = "GYrbbe0VbHim_7Hi6zrOpHQXrSQz07XNtwCnfbFo2I0" 33 | export const AOS_V2_MODULE = "GuzQrkf50rBUqz3uUgjOIFOL1XmW9nSNysTBC-wyiWM" 34 | export const MU = "fcoN_xJeisVsPXA-trzVAuIiqO3ydLQxM-L4XbrQKzY" 35 | export const SCHEDULER = "_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA" 36 | 37 | export const ARWEAVE_GATEWAY = "https://arweave.net/"; 38 | 39 | // Denomination: AO is 12, wAR is 12, TRUNK is 3. 40 | export const TOKEN_DENO = new Map([[0, 12], [1, 12], [2, 3]]); 41 | export const TOKEN_PID = new Map([[0, AO], [1, WAR], [2, TRUNK]]); 42 | export const TOKEN_NAME = new Map([[0, 'AO'], [1, 'wAR'], [2, 'TRUNK']]); 43 | export const TOKEN_ICON = new Map([ 44 | ['AO', './logo-ao-token.png'], 45 | ['wAR', './logo-war.png'], 46 | ['WAR', './logo-war.png'], 47 | ['TRUNK', './logo-trunk.png'], 48 | ['TYPR', './logo-typr.png'], 49 | ['0RBT', './logo-0rbit.jpg'], 50 | ['USDA-TST', './logo-usda.png'], 51 | ]); 52 | 53 | export const LUA = 54 | ` 55 | local json = require("json") 56 | local sqlite3 = require("lsqlite3") 57 | 58 | DB = DB or sqlite3.open_memory() 59 | 60 | DB:exec [[ 61 | CREATE TABLE IF NOT EXISTS notis ( 62 | reply_id TEXT PRIMARY KEY, 63 | post_id TEXT, 64 | noti_type TEXT, 65 | address TEXT, 66 | avatar TEXT, 67 | nickname TEXT, 68 | post TEXT, 69 | bounty INT, 70 | bounty_type TEXT, 71 | time INT 72 | ); 73 | ]] 74 | 75 | AOT_TEST = "UabERwDSwechOsHg9M1N6qTk2O7EXPf63qABDTAj_Vs"; 76 | AO_TWITTER = "8s1ZpAx_NueKS4N2ZOMYWCkl5qVcGkgnBFSnqSVX9Fo"; 77 | 78 | local function query(stmt) 79 | local rows = {} 80 | for row in stmt:nrows() do 81 | table.insert(rows, row) 82 | end 83 | stmt:reset() 84 | return rows 85 | end 86 | 87 | Handlers.add( 88 | "TransferToken", 89 | Handlers.utils.hasMatchingTag("Action", "TransferToken"), 90 | function(msg) 91 | if msg.From == Owner then 92 | local target = msg.Tags.Target 93 | if target == '' then 94 | target = AOT_TEST 95 | end 96 | 97 | ao.send({ 98 | Target = target, 99 | Action = "Transfer", 100 | Recipient = msg.Tags.Recipient, 101 | Quantity = msg.Tags.Quantity 102 | }) 103 | end 104 | end 105 | ) 106 | 107 | Handlers.add( 108 | "Record-Noti", 109 | Handlers.utils.hasMatchingTag("Action", "Record-Noti"), 110 | function(msg) 111 | local data = json.decode(msg.Data) 112 | 113 | local stmt = DB:prepare [[ 114 | REPLACE INTO notis (reply_id, post_id, noti_type, address, avatar, nickname, post, bounty, bounty_type, time) 115 | VALUES (:reply_id, :post_id, :noti_type, :address, :avatar, :nickname, :post, :bounty, :bounty_type, :time); 116 | ]] 117 | 118 | if not stmt then 119 | error("Failed to prepare SQL statement: " .. DB:errmsg()) 120 | end 121 | 122 | stmt:bind_names({ 123 | reply_id = data.reply_id, 124 | post_id = data.post_id, 125 | noti_type = data.noti_type, 126 | address = data.address, 127 | avatar = data.avatar, 128 | nickname = data.nickname, 129 | post = data.post, 130 | bounty = data.bounty, 131 | bounty_type = data.bounty_type, 132 | time = data.time 133 | }) 134 | 135 | stmt:step() 136 | stmt:reset() 137 | end 138 | ) 139 | 140 | Handlers.add( 141 | "Get-Notis", 142 | Handlers.utils.hasMatchingTag("Action", "Get-Notis"), 143 | function(msg) 144 | local data = json.decode(msg.Data) 145 | 146 | local stmt = DB:prepare [[ 147 | SELECT * FROM notis 148 | ORDER BY time DESC LIMIT 10 OFFSET :offset; 149 | ]] 150 | 151 | if not stmt then 152 | error("Failed to prepare SQL statement: " .. DB:errmsg()) 153 | end 154 | 155 | stmt:bind_names({ 156 | offset = data.offset 157 | }) 158 | 159 | local rows = query(stmt) 160 | Handlers.utils.reply(json.encode(rows))(msg) 161 | end 162 | ) 163 | `; -------------------------------------------------------------------------------- /site/src/app/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './HomePage.css'; 3 | import { subscribe } from '../util/event'; 4 | import AlertModal from '../modals/AlertModal'; 5 | import MessageModal from '../modals/MessageModal'; 6 | import { 7 | checkContent, getDataFromAO, getWalletAddress, timeOfNow, 8 | messageToAO, uuid, isBookmarked, 9 | createArweaveWallet 10 | } from '../util/util'; 11 | import ActivityPost from '../elements/ActivityPost'; 12 | import { AO_TWITTER, PAGE_SIZE, TIP_CONN, TIP_IMG } from '../util/consts'; 13 | import { Server } from '../../server/server'; 14 | import Loading from '../elements/Loading'; 15 | import PostContent from '../elements/PostContent'; 16 | 17 | declare var window: any; 18 | 19 | interface HomePageState { 20 | posts: any; 21 | question: string; 22 | alert: string; 23 | message: string; 24 | loading: boolean; 25 | loadNextPage: boolean; 26 | range: string; 27 | process: string; 28 | newPosts: number; 29 | isAll: boolean; 30 | } 31 | 32 | class HomePage extends React.Component<{}, HomePageState> { 33 | 34 | quillRef: any; 35 | wordCount = 0; 36 | refresh: any; 37 | 38 | constructor(props: {}) { 39 | super(props); 40 | this.state = { 41 | posts: [], 42 | question: '', 43 | alert: '', 44 | message: '', 45 | loading: true, 46 | loadNextPage: false, 47 | range: 'everyone', 48 | process: '', 49 | newPosts: 0, 50 | isAll: false, 51 | }; 52 | 53 | this.getPosts = this.getPosts.bind(this); 54 | this.onContentChange = this.onContentChange.bind(this); 55 | this.onRangeChange = this.onRangeChange.bind(this); 56 | this.atBottom = this.atBottom.bind(this); 57 | 58 | subscribe('wallet-events', () => { 59 | this.forceUpdate(); 60 | }); 61 | 62 | subscribe('new-post', () => { 63 | this.getPosts(true); 64 | }); 65 | } 66 | 67 | componentDidMount() { 68 | this.start(); 69 | window.addEventListener('scroll', this.atBottom); 70 | } 71 | 72 | componentWillUnmount(): void { 73 | // clearInterval(this.refresh); 74 | window.removeEventListener('scroll', this.atBottom); 75 | Server.service.addPositionToCache(window.pageYOffset); 76 | } 77 | 78 | onContentChange(length: number) { 79 | this.wordCount = length; 80 | }; 81 | 82 | atBottom() { 83 | const scrollHeight = document.documentElement.scrollHeight; 84 | const scrollTop = document.documentElement.scrollTop; 85 | const clientHeight = document.documentElement.clientHeight; 86 | 87 | if (scrollTop + clientHeight + 300 >= scrollHeight) 88 | setTimeout(() => { 89 | if (!this.state.loading && !this.state.loadNextPage && !this.state.isAll) 90 | this.nextPage(); 91 | }, 200); 92 | } 93 | 94 | onRangeChange(e: React.FormEvent) { 95 | const element = e.target as HTMLSelectElement; 96 | this.setState({ range: element.value }); 97 | } 98 | 99 | async start() { 100 | this.getPosts(); 101 | 102 | // check the new post every 10 seconds. 103 | // this.refresh = setInterval(() => this.refreshPosts(), 10000); 104 | 105 | createArweaveWallet(); 106 | } 107 | 108 | async getTokens() { 109 | // the ID of the token 110 | const tokenID = "rN1B9kLV3ilqMQSd0bqc-sjrvMXzQkKB-JtfUeUUnl8"; 111 | 112 | // check if the token has been added 113 | // const isAdded = await window.arweaveWallet.isTokenAdded(tokenID); 114 | // console.log("isAdded:", isAdded) 115 | 116 | // add token if it hasn't been added yet 117 | // if (!isAdded) { 118 | // await window.arweaveWallet.addToken(tokenID); 119 | // } 120 | } 121 | 122 | async refreshPosts() { 123 | let posts_amt = localStorage.getItem('posts_amt'); 124 | if (posts_amt) { 125 | let posts = await getDataFromAO(AO_TWITTER, 'GetPosts', { id: '0' }); 126 | let newPosts = posts.length - Number(posts_amt); 127 | localStorage.setItem('posts_amt', posts.length.toString()); 128 | // console.log("newPosts amt:", newPosts) 129 | if (newPosts > 0) 130 | this.setState({ newPosts }); 131 | } 132 | } 133 | 134 | async showNewPosts() { 135 | // let posts = await getDataFromAO(AO_TWITTER, 'GetPosts', null); 136 | // // let final = parsePosts(posts); 137 | // // let total = final.concat(this.state.posts); 138 | // this.setState({ posts: [] }); 139 | 140 | // setTimeout(() => { 141 | // window.scrollTo(0, 0); 142 | // this.setState({ posts: total, newPosts: 0 }); 143 | // Server.service.addPostsToCache(total); 144 | // }, 10); 145 | } 146 | 147 | async getPosts(new_post?: boolean) { 148 | let posts = Server.service.getPostsFromCache(); 149 | let position = Server.service.getPositionFromCache(); 150 | 151 | if (!posts || new_post) { 152 | posts = await getDataFromAO(AO_TWITTER, 'GetPosts', { offset: 0 }); 153 | if (posts.length < PAGE_SIZE) 154 | this.setState({ isAll: true }) 155 | else 156 | this.setState({ isAll: false }) 157 | } 158 | 159 | // console.log("posts:", posts) 160 | this.checkBookmarks(posts); 161 | 162 | setTimeout(() => { 163 | window.scrollTo(0, position); 164 | }, 10); 165 | } 166 | 167 | async nextPage() { 168 | this.setState({ loadNextPage: true }); 169 | 170 | let offset = this.state.posts.length.toString(); 171 | let posts = await getDataFromAO(AO_TWITTER, 'GetPosts', { offset }); 172 | 173 | if (posts.length < PAGE_SIZE) 174 | this.setState({ isAll: true }) 175 | 176 | let total = this.state.posts.concat(posts); 177 | this.checkBookmarks(total); 178 | } 179 | 180 | checkBookmarks(posts: any) { 181 | let bookmarks = []; 182 | let val = localStorage.getItem('bookmarks'); 183 | if (val) bookmarks = JSON.parse(val); 184 | 185 | for (let i = 0; i < posts.length; i++) { 186 | let resp = isBookmarked(bookmarks, posts[i].id); 187 | posts[i].isBookmarked = resp; 188 | } 189 | 190 | Server.service.addPostsToCache(posts); 191 | this.setState({ posts, loading: false, loadNextPage: false }); 192 | } 193 | 194 | renderPosts() { 195 | if (this.state.loading) return (); 196 | 197 | let divs = []; 198 | let address = Server.service.getActiveAddress(); 199 | 200 | for (let i = 0; i < this.state.posts.length; i++) { 201 | let data = this.state.posts[i]; 202 | if (data.range === 'everyone' || data.address === address) { 203 | divs.push( 204 | 205 | ) 206 | } 207 | } 208 | 209 | return divs; 210 | } 211 | 212 | postDone() { 213 | this.getPosts(true); 214 | } 215 | 216 | render() { 217 | return ( 218 |
219 | {Server.service.isLoggedIn() && 220 | this.postDone()} /> 221 | } 222 | 223 |
224 | {this.renderPosts()} 225 |
226 | 227 | {this.state.newPosts > 0 && 228 |
this.showNewPosts()}> 229 | {this.state.newPosts}  New Posts 230 |
231 | } 232 | 233 | {this.state.loadNextPage && } 234 | {this.state.isAll && 235 |
236 | No more posts. 237 |
238 | } 239 | 240 | 241 | this.setState({ alert: '' })} /> 242 |
243 | ) 244 | } 245 | } 246 | 247 | export default HomePage; -------------------------------------------------------------------------------- /site/src/app/modals/BountyModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BsFillXCircleFill } from 'react-icons/bs'; 3 | import AlertModal from './AlertModal'; 4 | import './Modal.css' 5 | import './BountyModal.css' 6 | import MessageModal from './MessageModal'; 7 | import { messageToAO, updateTokenBalances, timeOfNow, transferTokenAward, trimDecimal, isValidPositiveNumber } from '../util/util'; 8 | import { AiOutlineFire } from 'react-icons/ai'; 9 | import { Server } from '../../server/server'; 10 | import { AO_STORY, AO_TWITTER, TOKEN_DENO, TOKEN_ICON, TOKEN_NAME, TOKEN_PID } from '../util/consts'; 11 | import Loading from '../elements/Loading'; 12 | import { subscribe } from '../util/event'; 13 | 14 | interface BountyModalProps { 15 | open: boolean; 16 | onClose: Function; 17 | onBounty: Function; 18 | data: any; 19 | isReply?: boolean; 20 | isStory?: boolean; 21 | } 22 | 23 | interface BountyModalState { 24 | message: string; 25 | alert: string; 26 | bounty: number; 27 | loading: boolean; 28 | } 29 | 30 | class BountyModal extends React.Component { 31 | 32 | tokenPicked = 0; 33 | balances: any[] = []; 34 | 35 | constructor(props: BountyModalProps) { 36 | super(props); 37 | this.state = { 38 | message: '', 39 | alert: '', 40 | bounty: 1, 41 | loading: false, 42 | } 43 | 44 | this.onClose = this.onClose.bind(this); 45 | this.onChangeBounty = this.onChangeBounty.bind(this); 46 | 47 | subscribe('get-bal-done', () => { 48 | this.setState({ loading: false }); 49 | }); 50 | } 51 | 52 | componentDidMount() { 53 | if (!Server.service.getBalanceOfTRUNK()) 54 | this.setState({ loading: true }); 55 | } 56 | 57 | onChangeBounty(e: any) { 58 | this.setState({ bounty: e.currentTarget.value }); 59 | } 60 | 61 | onClose() { 62 | this.props.onClose(); 63 | } 64 | 65 | fillQty(qty: number) { 66 | this.setState({ bounty: qty }); 67 | } 68 | 69 | onFilter(index: number) { 70 | if (this.tokenPicked === index) return; 71 | this.tokenPicked = index; 72 | // this.renderTokens(); 73 | this.forceUpdate(); 74 | } 75 | 76 | renderTokens() { 77 | let bal_ao = Server.service.getBalanceOfAO(); 78 | let bal_war = Server.service.getBalanceOfWAR(); 79 | let bal_trunk = Server.service.getBalanceOfTRUNK(); 80 | 81 | if (!this.state.loading) { 82 | bal_ao = Number(trimDecimal(bal_ao, 5)); 83 | bal_war = Number(trimDecimal(bal_war, 5)); 84 | bal_trunk = Number(trimDecimal(bal_trunk, 5)); 85 | } 86 | 87 | this.balances = [bal_ao, bal_war, bal_trunk]; 88 | 89 | let divs = []; 90 | for (let i = 0; i < this.balances.length; i++) { 91 | let tokenName = TOKEN_NAME.get(i); 92 | let tokenIcon = TOKEN_ICON.get(tokenName); 93 | 94 | divs.push( 95 |
this.onFilter(i)} 99 | > 100 | 101 |
102 |
{tokenName}
103 | {this.state.loading 104 | ? 105 | :
{this.balances[i]}
106 | } 107 |
108 |
109 | ) 110 | } 111 | 112 | return divs 113 | } 114 | 115 | renderTokenLabel() { 116 | let divs = []; 117 | let qty = [0.1, 0.5, 1, 2, 5, 10]; 118 | 119 | for (let i = 0; i < qty.length; i++) { 120 | divs.push( 121 |
this.fillQty(qty[i])}> 122 | {/* {qty[i]} */} 123 | {qty[i]} 124 |
125 | ) 126 | } 127 | 128 | return divs; 129 | } 130 | 131 | async onBounty() { 132 | this.setState({ message: 'Bounty...' }); 133 | 134 | // your own active address 135 | let from = Server.service.getActiveAddress(); 136 | console.log("from:", from) 137 | 138 | // the wallet address of post to tranfer a bounty 139 | let to = this.props.data.address; 140 | console.log("to:", to) 141 | 142 | let alert; 143 | let bal = this.balances[this.tokenPicked]; 144 | console.log("bal:", bal) 145 | if (!bal) bal = 0; 146 | 147 | let qty = this.state.bounty; 148 | console.log("qty:", qty) 149 | 150 | if (!isValidPositiveNumber(qty)) 151 | alert = 'Bounty must be a valid positive non-zero number.'; 152 | if (qty == 0) 153 | alert = 'Bounty is zero.'; 154 | if (qty > bal) 155 | alert = 'Insufficient balance.'; 156 | 157 | if (alert) { 158 | this.setState({ alert, message: '' }); 159 | return; 160 | } 161 | 162 | let target = TOKEN_PID.get(this.tokenPicked); 163 | console.log("target:", target) 164 | 165 | // formating the qty 166 | let fQty = qty * 10 ** TOKEN_DENO.get(this.tokenPicked); 167 | console.log("formating qty:", fQty) 168 | 169 | // await transferToken(from, to, qty, target); 170 | let response = await transferTokenAward(target, to, fQty.toString()); 171 | if (!response) { 172 | this.setState({ alert: 'You cancelled the bounty.', message: '' }); 173 | return; 174 | } 175 | 176 | this.onClose(); 177 | this.setState({ message: '' }); 178 | updateTokenBalances(from); 179 | 180 | // refreshing the number that displayed on the post. 181 | let quantity = Number(this.props.data.coins) + qty; 182 | this.props.onBounty(quantity.toFixed(3)); 183 | 184 | // update the bounty (coins) 185 | let data = { id: this.props.data.id, coins: qty }; 186 | let action = 'UpdateBounty'; 187 | if (this.props.isReply) action = 'UpdateBountyForReply'; 188 | 189 | if (this.props.isStory) 190 | await messageToAO(AO_STORY, data, action); 191 | else 192 | await messageToAO(AO_TWITTER, data, action); 193 | 194 | // add the record of a bounty 195 | let records = { 196 | id: data.id, 197 | address: from, 198 | token_name: TOKEN_NAME.get(this.tokenPicked), 199 | quantity: qty, 200 | time: timeOfNow() 201 | }; 202 | // console.log("records:", records) 203 | messageToAO(AO_TWITTER, records, 'Records-Bounty'); 204 | } 205 | 206 | render() { 207 | if (!this.props.open) 208 | return (
); 209 | 210 | return ( 211 |
e.stopPropagation()}> 212 |
213 | 216 | 217 |
218 |
Bounty
219 |
220 | 221 |
222 |
If you like these words.
223 |
Do a bounty to inspire more good words.
224 | 225 |
Pick a token
226 |
227 |
228 | {this.renderTokens()} 229 |
230 | 231 |
232 | {this.renderTokenLabel()} 233 |
234 | 235 |
236 | 244 | 245 | {!this.state.loading && 246 |
this.onBounty()}> 247 | Bounty 248 |
249 | } 250 |
251 |
252 | 253 | 254 | this.setState({ alert: '' })} /> 255 |
256 | ) 257 | } 258 | } 259 | 260 | export default BountyModal; -------------------------------------------------------------------------------- /site/src/app/pages/SitePage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink, Outlet } from 'react-router-dom'; 3 | import NavBar from '../elements/NavBar'; 4 | import { 5 | BsAward, BsBell, BsBookmark, BsChatText, BsController, BsHouse, 6 | BsPeopleFill, BsPerson, BsReplyFill, BsSend, BsSendFill 7 | } from 'react-icons/bs'; 8 | import { 9 | getDataFromAO, getDefaultProcess, 10 | isLoggedIn, 11 | messageToAO, 12 | updateTokenBalances} from '../util/util'; 13 | import { AO_TWITTER, ICON_SIZE } from '../util/consts'; 14 | import { Server } from '../../server/server'; 15 | import Portrait from '../elements/Portrait'; 16 | import { publish, subscribe } from '../util/event'; 17 | import './SitePage.css'; 18 | import { AiOutlineFire } from 'react-icons/ai'; 19 | import { RiQuillPenLine } from "react-icons/ri"; 20 | import { CgMoreO } from "react-icons/cg"; 21 | import PostModal from '../modals/PostModal'; 22 | 23 | interface SitePageState { 24 | users: number; 25 | posts: number; 26 | replies: number; 27 | open: boolean; 28 | address: string; 29 | openMenu: boolean; 30 | } 31 | 32 | class SitePage extends React.Component<{}, SitePageState> { 33 | 34 | constructor(props: {}) { 35 | super(props); 36 | this.state = { 37 | users: 0, 38 | posts: 0, 39 | replies: 0, 40 | open: false, 41 | address: '', 42 | openMenu: false, 43 | }; 44 | 45 | this.onOpen = this.onOpen.bind(this); 46 | this.onClose = this.onClose.bind(this); 47 | 48 | subscribe('wallet-events', () => { 49 | // let address = Server.service.isLoggedIn(); 50 | // this.setState({ address }) 51 | this.start(); 52 | }); 53 | } 54 | 55 | componentDidMount() { 56 | this.start(); 57 | } 58 | 59 | async start() { 60 | let address = await isLoggedIn(); 61 | // console.log("site page -> address:", address) 62 | 63 | Server.service.setIsLoggedIn(address); 64 | Server.service.setActiveAddress(address); 65 | this.setState({ address }) 66 | 67 | // let process = await getDefaultProcess(address); 68 | // Server.service.setDefaultProcess(process); 69 | 70 | // this.getStatus(); 71 | // setInterval(() => this.getStatus(), 60000); // 1 min 72 | 73 | // getting notifications. 74 | // setInterval(() => this.getNotis(), 20000); // 20 seconds 75 | 76 | // updateTokenBalances(address); 77 | 78 | window.addEventListener("walletSwitch", (e: any) => { 79 | const newAddress = e.detail.address; 80 | console.log("walletSwitch --> newAddress:", newAddress) 81 | Server.service.setIsLoggedIn(newAddress); 82 | Server.service.setActiveAddress(newAddress); 83 | localStorage.setItem('owner', newAddress); 84 | publish('wallet-events'); 85 | }); 86 | } 87 | 88 | onOpen() { 89 | this.setState({ open: true }); 90 | } 91 | 92 | onClose() { 93 | this.setState({ open: false }); 94 | publish('new-post'); 95 | } 96 | 97 | async getStatus() { 98 | let users = await getDataFromAO(AO_TWITTER, 'GetUsersCount'); 99 | this.setState({ users: users[0].total_count }); 100 | 101 | let posts = await getDataFromAO(AO_TWITTER, 'GetPostsCount'); 102 | this.setState({ posts: posts[0].total_count }); 103 | 104 | let replies = await getDataFromAO(AO_TWITTER, 'GetRepliesCount'); 105 | this.setState({ replies: replies[0].total_count }); 106 | } 107 | 108 | async getNotis() { 109 | let process = Server.service.getDefaultProcess(); 110 | let address = Server.service.getActiveAddress(); 111 | let postIDs = await getDataFromAO(AO_TWITTER, 'Get-PostIDs', { address }); 112 | // console.log("postIDs:", postIDs) 113 | 114 | for (let i = 0; i < postIDs.length; i++) { 115 | let post_id = postIDs[i].id; 116 | let data = { post_id: post_id, offset: 0 }; 117 | let replies = await getDataFromAO(AO_TWITTER, 'GetReplies', data); 118 | // console.log("replies:", replies) 119 | 120 | for (let j = 0; j < replies.length; j++) { 121 | let reply = replies[j]; 122 | if (reply.address == address) continue; 123 | 124 | let data = { 125 | reply_id: reply.id, 126 | post_id: post_id, 127 | noti_type: 'REPLY', 128 | address: reply.address, 129 | avatar: reply.avatar, 130 | nickname: reply.nickname, 131 | post: reply.post, 132 | bounty: 0, 133 | bounty_type: '', 134 | time: reply.time, 135 | }; 136 | // console.log("insert noti data --> ", data) 137 | 138 | let response = await messageToAO(process, data, 'Record-Noti'); 139 | } 140 | } 141 | } 142 | 143 | renderToobar() { 144 | return ( 145 |
146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 |
155 | 156 |
157 | 158 | 159 | 160 | 161 | 162 |
this.setState({ openMenu: true })} 165 | > 166 | 167 |
168 |
169 | ) 170 | } 171 | 172 | renderPopupMenu() { 173 | return ( 174 |
this.setState({ openMenu: false })} 177 | > 178 |
179 | 180 | Games 181 | 182 | 183 | 184 | TokenEco 185 | 186 | 187 | 188 | Chatroom 189 | 190 | 191 | 192 | Bookmarks 193 | 194 | 195 | 196 | Profile 197 | 198 | 199 | {/*
200 | Log out 201 |
*/} 202 |
203 |
204 | ) 205 | } 206 | 207 | render() { 208 | return ( 209 |
210 |
211 | 212 | 213 | 214 | 215 | {/*
216 |
{this.state.users}
217 |
{this.state.posts}
218 |
{this.state.replies}
219 |
*/} 220 |
221 | 222 | {/* FOR MOBILE */} 223 |
224 | 225 | 226 | 227 | 228 |
229 | 230 |
231 |
232 | 233 | 234 | {this.state.address && 235 |
236 | Post 237 |
238 | } 239 | 240 | 241 |
242 | 243 |
244 | 245 |
246 |
247 | 248 | {this.renderToobar()} 249 | 250 | {this.state.openMenu && 251 | this.renderPopupMenu() 252 | } 253 | 254 | 255 |
256 | ); 257 | } 258 | } 259 | 260 | export default SitePage; -------------------------------------------------------------------------------- /site/src/app/pages/FollowPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './FollowPage.css'; 3 | import { AO_TWITTER, AO_STORY, PAGE_SIZE } from '../util/consts'; 4 | import { getDataFromAO, getProfile, shortAddr } from '../util/util'; 5 | import Loading from '../elements/Loading'; 6 | import { NavLink } from 'react-router-dom'; 7 | import { Server } from '../../server/server'; 8 | 9 | interface FollowPageProps { 10 | type?: string; 11 | } 12 | 13 | interface FollowPageState { 14 | loading: boolean; 15 | loadNextPage: boolean; 16 | isAll: boolean; 17 | followers: any; 18 | following: any; 19 | isFollowing: boolean; 20 | showUnfollow: boolean; 21 | butDisable: boolean; 22 | } 23 | 24 | class FollowPage extends React.Component { 25 | 26 | id = ''; 27 | filterSelected = 0; 28 | 29 | constructor(props: FollowPageProps) { 30 | super(props); 31 | this.state = { 32 | loading: true, 33 | loadNextPage: false, 34 | isAll: false, 35 | followers: '', 36 | following: '', 37 | isFollowing: false, 38 | showUnfollow: false, 39 | butDisable: true, 40 | }; 41 | 42 | this.atBottom = this.atBottom.bind(this); 43 | 44 | // subscribe('wallet-events', () => { 45 | // this.forceUpdate(); 46 | // }); 47 | } 48 | 49 | componentDidMount() { 50 | this.start(); 51 | window.addEventListener('scroll', this.atBottom); 52 | } 53 | 54 | componentWillUnmount(): void { 55 | window.removeEventListener('scroll', this.atBottom); 56 | } 57 | 58 | atBottom() { 59 | const scrollHeight = document.documentElement.scrollHeight; 60 | const scrollTop = document.documentElement.scrollTop; 61 | const clientHeight = document.documentElement.clientHeight; 62 | 63 | if (scrollTop + clientHeight + 300 >= scrollHeight) 64 | setTimeout(() => { 65 | if (!this.state.loading && !this.state.loadNextPage && !this.state.isAll) 66 | this.nextPage(); 67 | }, 200); 68 | } 69 | 70 | async start() { 71 | let path = window.location.hash.slice(1); 72 | let id = path.substring(8); 73 | // console.log("id:", id) 74 | this.id = id; 75 | 76 | // let address = await isLoggedIn(); 77 | // this.setState({ address: id }); 78 | 79 | // await this.isFollowing(); 80 | await this.getProfile(); 81 | await this.getFollows(); 82 | // await this.tempGetFollowsTable(); 83 | } 84 | 85 | async getFollows() { 86 | let following = await getDataFromAO(AO_TWITTER, 'GetFollowing', { follower: this.id, offset: 0 }); 87 | // console.log("following:", following) 88 | this.setState({ following }) 89 | 90 | let followers = await getDataFromAO(AO_TWITTER, 'GetFollowers', { following: this.id, offset: 0 }); 91 | // console.log("followers:", followers) 92 | this.setState({ followers, loading: false }) 93 | 94 | // if (following.length < PAGE_SIZE) 95 | // this.setState({ isAll: true }) 96 | } 97 | 98 | async nextPage() { 99 | // this.setState({ loadNextPage: true }); 100 | 101 | // let offset = this.state.posts.length.toString(); 102 | // console.log("offset:", offset) 103 | 104 | // let posts = await getDataFromAO(AO_STORY, 'GetStories', offset); 105 | // console.log("stories:", posts) 106 | // if (posts.length < PAGE_SIZE) 107 | // this.setState({ isAll: true }) 108 | 109 | // let total = this.state.posts.concat(posts); 110 | 111 | // // Server.service.addPostsToCache(posts); 112 | // this.setState({ posts: total, loadNextPage: false }); 113 | // this.getStats(posts); 114 | } 115 | 116 | onFilter(index: number) { 117 | if (this.filterSelected === index) return; 118 | 119 | this.filterSelected = index; 120 | this.renderFilters(); 121 | this.forceUpdate() 122 | 123 | if (index === 0) { 124 | // this.setState({ posts: [] }); 125 | // setTimeout(() => { 126 | // this.getPosts(this.author); 127 | // }, 10); 128 | } 129 | } 130 | 131 | renderFilters() { 132 | let filters = ['Followers', 'Following']; 133 | 134 | let divs = []; 135 | for (let i = 0; i < filters.length; i++) { 136 | divs.push( 137 |
this.onFilter(i)} key={i} 140 | > 141 | {filters[i]} 142 |
143 | ); 144 | } 145 | 146 | return divs; 147 | } 148 | 149 | showUnfollow() { 150 | this.setState({ showUnfollow: true }) 151 | } 152 | 153 | notShowUnfollow() { 154 | this.setState({ showUnfollow: false }) 155 | } 156 | 157 | // async follow() { 158 | // if (this.state.butDisable) return; 159 | // this.setState({ butDisable: true }) 160 | 161 | // let data = { following: this.state.address, follower: Server.service.getActiveAddress(), time: timeOfNow() } 162 | // // console.log("follow data:", data) 163 | // await messageToAO(AO_TWITTER, data, 'Follow'); 164 | // await this.isFollowing() 165 | 166 | // this.setState({ butDisable: false }) 167 | // await this.getFollows(); 168 | // } 169 | 170 | // async unfollow() { 171 | // if (this.state.butDisable) return; 172 | // this.setState({ butDisable: true }) 173 | 174 | // let data = { following: this.state.address, follower: Server.service.getActiveAddress() } 175 | // // console.log("Unfollow data:", data) 176 | // await messageToAO(AO_TWITTER, data, 'Unfollow'); 177 | // await this.isFollowing() 178 | 179 | // this.setState({ butDisable: false }) 180 | // await this.getFollows(); 181 | // } 182 | 183 | renderFollows() { 184 | if (this.state.loading) return (); 185 | 186 | let data; 187 | if (this.filterSelected === 0) 188 | data = this.state.followers; 189 | else 190 | data = this.state.following; 191 | 192 | // console.log('data',data) 193 | // console.log('this.filterSelected',this.filterSelected) 194 | 195 | let divs = []; 196 | for (let i = 0; i < data.length; i++) { 197 | divs.push( 198 | 203 | 207 |
208 |
209 | {data[i].nickname} 210 |
211 |
{shortAddr(data[i].address, 4)}
212 |
{data[i].bio}
213 |
214 | 215 | {/* {this.state.isFollowing 216 | ?
this.showUnfollow()} 219 | onMouseLeave={() => this.notShowUnfollow()} 220 | onClick={() => this.unfollow()} 221 | > 222 | {this.state.butDisable 223 | ? 224 | : this.state.showUnfollow ? 'Unfollow' : 'Following' 225 | } 226 |
227 | :
this.follow()}> 228 | {this.state.butDisable 229 | ? 230 | : 'Follow' 231 | } 232 |
233 | } */} 234 |
235 | ); 236 | } 237 | 238 | return divs; 239 | } 240 | 241 | async getProfile() { 242 | let profile = await getProfile(this.id); 243 | console.log("follow -> profile:", profile) 244 | Server.service.addProfileToCache(profile[0]); 245 | } 246 | 247 | render() { 248 | let data = Server.service.getProfile(this.id); 249 | 250 | return ( 251 |
252 | {data && 253 |
254 | 255 |
256 |
{data.nickname}
257 |
{shortAddr(data.address, 4)}
258 |
259 |
260 | } 261 | 262 |
263 | {this.renderFilters()} 264 |
265 | 266 | {this.renderFollows()} 267 | 268 | {this.state.loadNextPage && } 269 | {this.state.isAll && 270 |
271 | No more. 272 |
273 | } 274 |
275 | ) 276 | } 277 | } 278 | 279 | export default FollowPage; -------------------------------------------------------------------------------- /site/src/app/pages/StoryPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './StoryPage.css'; 3 | import StoryCard from '../elements/StoryCard'; 4 | import { AiOutlineFire } from 'react-icons/ai'; 5 | import { getDataFromAO, messageToAO, uuid, wait } from '../util/util'; 6 | import { AO_STORY, PAGE_SIZE } from '../util/consts'; 7 | import Loading from '../elements/Loading'; 8 | import { Server } from '../../server/server'; 9 | import PostModal from '../modals/PostModal'; 10 | 11 | declare var window: any; 12 | 13 | interface StoryPageState { 14 | posts: any; 15 | loading: boolean; 16 | loadNextPage: boolean; 17 | open: boolean; 18 | isAll: boolean; 19 | category: string; 20 | } 21 | 22 | class StoryPage extends React.Component<{}, StoryPageState> { 23 | 24 | filterSelected = 0; 25 | 26 | constructor(props: {}) { 27 | super(props); 28 | this.state = { 29 | posts: [], 30 | loading: true, 31 | loadNextPage: false, 32 | open: false, 33 | isAll: false, 34 | category: 'project', 35 | }; 36 | 37 | this.onOpen = this.onOpen.bind(this); 38 | this.onClose = this.onClose.bind(this); 39 | this.atBottom = this.atBottom.bind(this); 40 | this.onCategoryChange = this.onCategoryChange.bind(this); 41 | } 42 | 43 | componentDidMount() { 44 | let tab = Server.service.getStoryTab(); 45 | this.onFilter(tab, true); 46 | window.addEventListener('scroll', this.atBottom); 47 | } 48 | 49 | componentWillUnmount(): void { 50 | // clearInterval(this.refresh); 51 | window.removeEventListener('scroll', this.atBottom); 52 | // Server.service.addPositionToCache(window.pageYOffset); 53 | } 54 | 55 | atBottom() { 56 | const scrollHeight = document.documentElement.scrollHeight; 57 | const scrollTop = document.documentElement.scrollTop; 58 | const clientHeight = document.documentElement.clientHeight; 59 | 60 | if (scrollTop + clientHeight + 300 >= scrollHeight) { 61 | setTimeout(() => { 62 | if (!this.state.loading && !this.state.loadNextPage && !this.state.isAll) 63 | this.nextPage(); 64 | }, 200); 65 | } 66 | } 67 | 68 | onCategoryChange(e: any) { 69 | let category = e.currentTarget.value; 70 | this.setState({ category }); 71 | if (category == 'all') 72 | category = null; 73 | this.getStory(category); 74 | }; 75 | 76 | onOpen() { 77 | // TEMP 78 | // messageToAO(AO_STORY, '5dbe7bf9-27b8-4b8d-9c92-ade3dbd810a7', 'UpdateRecord'); 79 | // messageToAO(AO_STORY, '', 'DeleteRecord'); 80 | // messageToAO('rqF1Db192qSM6CUTfTxfGY3yKAT_t-h2BrSGes0rTpU', '', 'TransferAward'); 81 | // return 82 | 83 | this.setState({ open: true }); 84 | } 85 | 86 | onClose(data: any) { 87 | this.setState({ open: false }); 88 | if (data) { 89 | this.filterSelected = 2; // All New tab 90 | Server.service.setStoryTab(2); 91 | this.getStory(null, true); 92 | this.setState({ isAll: false }); 93 | } 94 | } 95 | 96 | getDataFromCache() { 97 | let data; 98 | 99 | if (this.filterSelected === 0) { // Hot Projects 100 | data = Server.service.getProjectStoriesFromCache(); 101 | } 102 | else if (this.filterSelected === 1) { // Top Story 103 | data = Server.service.getTopStoriesFromCache(); 104 | } 105 | else if (this.filterSelected === 2) { // All New 106 | data = Server.service.getAllStoriesFromCache(); 107 | } 108 | 109 | return data; 110 | } 111 | 112 | addDataToCache(data: any) { 113 | if (this.filterSelected === 0) { // Hot Projects 114 | Server.service.addProjectStoriesToCache(data); 115 | } 116 | else if (this.filterSelected === 1) { // Top Story 117 | Server.service.addTopStoriesToCache(data); 118 | } 119 | else if (this.filterSelected === 2) { // All New 120 | Server.service.addAllStoriesToCache(data); 121 | } 122 | } 123 | 124 | dataOfQuery(offset: any) { 125 | let data; 126 | 127 | if (this.filterSelected === 0) { // Hot Projects 128 | data = { category: 'project', offset }; 129 | } 130 | else if (this.filterSelected === 1) { // Top Story 131 | data = { category: 'top', offset }; 132 | } 133 | else if (this.filterSelected === 2) { // All New 134 | data = { category: null, offset }; 135 | } 136 | 137 | return data; 138 | } 139 | 140 | async getStory(category?: string, new_post?: boolean) { 141 | this.setState({ loading: true, isAll: false }); 142 | 143 | let posts = this.getDataFromCache(); 144 | 145 | if (!posts || new_post) { 146 | let data = { category, offset: 0 }; 147 | posts = await getDataFromAO(AO_STORY, 'GetStories', data); 148 | this.addDataToCache(posts); 149 | } 150 | // console.log("stories:", posts) 151 | if (posts.length < PAGE_SIZE) 152 | this.setState({ isAll: true }) 153 | 154 | this.setState({ posts, loading: false }); 155 | this.getStats(posts); 156 | } 157 | 158 | async nextPage() { 159 | this.setState({ loadNextPage: true }); 160 | 161 | let offset = this.state.posts.length.toString(); 162 | let data = this.dataOfQuery(offset); 163 | let posts = await getDataFromAO(AO_STORY, 'GetStories', data); 164 | // console.log("nextPage --> stories:", posts) 165 | if (posts.length < PAGE_SIZE) 166 | this.setState({ isAll: true }) 167 | 168 | let total = this.state.posts.concat(posts); 169 | this.addDataToCache(total); 170 | 171 | this.setState({ posts: total, loadNextPage: false }); 172 | this.getStats(posts); 173 | } 174 | 175 | async getStats(posts: any) { 176 | for (let i = 0; i < posts.length; i++) { 177 | let stats = await getDataFromAO(AO_STORY, 'GetStats', { post_id: posts[i].id }); 178 | if (stats[0].total_coins || stats[0].total_likes) { 179 | posts[i].coins += stats[0].total_coins; 180 | posts[i].likes += stats[0].total_likes; 181 | } 182 | } 183 | 184 | this.forceUpdate(); 185 | } 186 | 187 | renderStories() { 188 | if (this.state.loading) return (); 189 | 190 | let divs = []; 191 | for (let i = 0; i < this.state.posts.length; i++) { 192 | let story = this.state.posts[i] 193 | divs.push( 194 | 195 | ) 196 | } 197 | 198 | return divs 199 | } 200 | 201 | onFilter(index: number, init?: boolean) { 202 | if (!init) { 203 | if (this.state.loading) return; 204 | if (this.filterSelected === index) return; 205 | } 206 | 207 | this.filterSelected = index; 208 | Server.service.setStoryTab(index); 209 | this.renderFilters(); 210 | 211 | if (index === 0) { // Hot Projects 212 | this.getStory('project'); 213 | } 214 | else if (index === 1) { // Top Story 215 | this.getStory('top'); 216 | } 217 | else if (index === 2) { // All New 218 | this.getStory(); 219 | } 220 | } 221 | 222 | renderFilters() { 223 | let filters = ['Hot Projects', 'Top Story', 'All New']; 224 | 225 | let divs = []; 226 | for (let i = 0; i < filters.length; i++) { 227 | divs.push( 228 |
this.onFilter(i)} key={i} 231 | > 232 | {filters[i]} 233 |
234 | ); 235 | } 236 | 237 | return divs; 238 | } 239 | 240 | render() { 241 | return ( 242 |
243 |
244 |
Story
245 | {Server.service.isLoggedIn() && 246 |
247 | New Story 248 |
249 | } 250 |
251 | 252 |
253 |
254 | {this.renderFilters()} 255 |
256 | 257 | {this.filterSelected != 0 && 258 | 272 | } 273 |
274 | 275 | {this.renderStories()} 276 | 277 | {this.state.loadNextPage && } 278 | {this.state.isAll && 279 |
280 | No more story. 281 |
282 | } 283 | 284 | 285 |
286 | ) 287 | } 288 | } 289 | 290 | export default StoryPage; -------------------------------------------------------------------------------- /site/src/app/elements/Portrait.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Portrait.css'; 3 | import { publish, subscribe } from '../util/event'; 4 | import { AO_STORY, AO_TWITTER, LUA } from '../util/consts'; 5 | import { 6 | browserDetect, 7 | connectArConnectWallet, createArweaveWallet, uploadCodeToProcess, getDefaultProcess, getProfile, 8 | getWalletAddress, isLoggedIn, messageToAO, randomAvatar, shortAddr, shortStr, spawnProcess, timeOfNow 9 | } from '../util/util'; 10 | import { Server } from '../../server/server'; 11 | import { Tooltip } from 'react-tooltip'; 12 | import QuestionModal from '../modals/QuestionModal'; 13 | import { BsToggleOn, BsWallet2 } from 'react-icons/bs'; 14 | import * as Othent from "@othent/kms"; 15 | import MessageModal from '../modals/MessageModal'; 16 | import { ethers } from 'ethers'; 17 | 18 | import { 19 | connect, 20 | } from "@othent/kms"; 21 | import AlertModal from '../modals/AlertModal'; 22 | 23 | declare var window: any; 24 | 25 | interface PortraitProps { 26 | // address: string; 27 | } 28 | 29 | interface PortraitState { 30 | avatar: string; 31 | nickname: string; 32 | address: string; 33 | question: string; 34 | message: string; 35 | alert: string; 36 | } 37 | 38 | class Portrait extends React.Component { 39 | 40 | constructor(props: PortraitProps) { 41 | super(props); 42 | this.state = { 43 | avatar: '', 44 | nickname: '', 45 | address: '', 46 | question: '', 47 | message: '', 48 | alert: '', 49 | }; 50 | 51 | this.onQuestionYes = this.onQuestionYes.bind(this); 52 | this.onQuestionNo = this.onQuestionNo.bind(this); 53 | 54 | subscribe('wallet-events', () => { 55 | // this.forceUpdate(); 56 | this.start(); 57 | }); 58 | 59 | subscribe('profile-updated', () => { 60 | this.isExisted(this.state.address); 61 | }); 62 | } 63 | 64 | componentDidMount() { 65 | this.start(); 66 | } 67 | 68 | componentWillUnmount(): void { 69 | } 70 | 71 | async start() { 72 | let address = await isLoggedIn(); 73 | // console.log("portrait -> address:", address) 74 | this.setState({ address }) 75 | this.isExisted(address) 76 | } 77 | 78 | async connectOthent() { 79 | try { 80 | this.setState({ message: 'Connecting...' }); 81 | let res = await connect(); 82 | // let res = await Othent.connect(); 83 | // console.log("res:", res) 84 | 85 | window.arweaveWallet = Othent; 86 | this.afterConnect(res.walletAddress, res); 87 | } catch (error) { 88 | console.log(error) 89 | this.setState({ message: '' }); 90 | } 91 | } 92 | 93 | async connectArConnect() { 94 | let connected = await connectArConnectWallet(); 95 | if (connected) { 96 | let address = await getWalletAddress(); 97 | this.afterConnect(address); 98 | } 99 | } 100 | 101 | async connectMetaMask() { 102 | const name = browserDetect(); 103 | if (name === 'safari' || name === 'ie' || name === 'yandex' || name === 'others') { 104 | this.setState({ alert: 'MetaMask is not supported for this browser! Please use the Wallet Connect.' }); 105 | return false; 106 | } 107 | 108 | if (typeof window.ethereum === 'undefined') { 109 | this.setState({ alert: 'MetaMask is not installed!' }); 110 | return false; 111 | } 112 | 113 | try { 114 | // const provider = new Web3Provider(window.ethereum); 115 | const provider = new ethers.providers.Web3Provider(window.ethereum); 116 | const accounts = await provider.send("eth_requestAccounts", []); 117 | const address = accounts[0]; 118 | // console.log("[ address ]", address); 119 | this.afterConnect(address); 120 | await createArweaveWallet(); 121 | return true; 122 | } catch (error: any) { 123 | this.setState({ alert: error.message }); 124 | return false; 125 | } 126 | } 127 | 128 | async afterConnect(address: string, othent?: any) { 129 | Server.service.setIsLoggedIn(address); 130 | Server.service.setActiveAddress(address); 131 | localStorage.setItem('owner', address); 132 | 133 | publish('wallet-events'); 134 | 135 | if (othent) 136 | this.setState({ avatar: othent.picture, nickname: othent.name }); 137 | 138 | this.setState({ address, message: '' }); 139 | 140 | if (await this.isExisted(address) == false) 141 | this.register(address, othent); 142 | 143 | // your own process 144 | // let process = await getDefaultProcess(address); 145 | // console.log("Your process:", process) 146 | 147 | // Spawn a new process 148 | // if (!process) { 149 | // process = await spawnProcess(); 150 | // console.log("Spawn --> processId:", process) 151 | // } 152 | 153 | // setTimeout(async () => { 154 | // load lua code into the process 155 | // let messageId = await uploadCodeToProcess(process, LUA); 156 | // console.log("uploadCodeToProcess -->", messageId) 157 | // }, 10000); 158 | } 159 | 160 | async register(address: string, othent?: any) { 161 | // console.log('--> register') 162 | 163 | let nickname = shortAddr(address, 4); 164 | let data = { address, avatar: randomAvatar(), banner: '', nickname, bio: '', time: timeOfNow() }; 165 | 166 | if (othent) { 167 | data = { address, avatar: othent.picture, banner: '', nickname: othent.name, bio: '', time: timeOfNow() }; 168 | } 169 | 170 | messageToAO(AO_TWITTER, data, 'Register'); 171 | messageToAO(AO_STORY, data, 'Register'); 172 | } 173 | 174 | async disconnectWallet() { 175 | this.setState({ message: 'Disconnect...' }); 176 | 177 | await window.arweaveWallet.disconnect(); 178 | 179 | Server.service.setIsLoggedIn(''); 180 | Server.service.setActiveAddress(''); 181 | localStorage.removeItem('id_token'); 182 | localStorage.removeItem('owner'); 183 | publish('wallet-events'); 184 | 185 | this.setState({ address: '', question: '', message: '' }); 186 | } 187 | 188 | onQuestionYes() { 189 | this.disconnectWallet(); 190 | } 191 | 192 | onQuestionNo() { 193 | this.setState({ question: '' }); 194 | } 195 | 196 | async isExisted(address: string) { 197 | let profile = Server.service.getProfile(address); 198 | // console.log("cached profile:", profile) 199 | 200 | if (!profile) { // no cache 201 | profile = await getProfile(address); 202 | // console.log("portrait -> profile:", profile) 203 | 204 | if (profile.length > 0) { 205 | profile = profile[0]; 206 | Server.service.addProfileToCache(profile); 207 | // return true; 208 | } 209 | else 210 | return false; 211 | } 212 | 213 | this.setState({ address, avatar: profile.avatar, nickname: profile.nickname }); 214 | return true; 215 | } 216 | 217 | render() { 218 | let address = this.state.address; 219 | let avatar = this.state.avatar; 220 | let shortAddress = shortAddr(address, 4); 221 | 222 | return ( 223 |
224 | {address 225 | ? 226 |
this.disconnectWallet()} 231 | // onClick={() => this.setState({ question: 'Disconnect?' })} 232 | > 233 | 237 |
238 |
239 | {this.state.nickname ? shortStr(this.state.nickname, 15) : shortAddress} 240 |
241 |
{shortAddress}
242 |
243 |
244 | : 245 |
246 |
247 |
this.connectArConnect()}> 248 | ArConnect 249 |
250 |
this.connectMetaMask()}> 251 | 🦊 Metamask 252 |
253 |
- OR -
254 |
this.connectOthent()}> 255 | Othent 256 |
257 |
Google or others
258 |
259 | 260 |
261 |
this.connectOthent()}> 262 | Connect 263 |
264 |
265 |
266 | } 267 | 268 | 269 | 270 | this.setState({ alert: '' })} /> 271 | 272 |
273 | ); 274 | } 275 | } 276 | 277 | export default Portrait; -------------------------------------------------------------------------------- /site/src/app/pages/ChatPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './ChatPage.css'; 3 | import AlertModal from '../modals/AlertModal'; 4 | import { formatTimestamp, getDataFromAO, getProfile, getWalletAddress, 5 | messageToAO, shortAddr, shortStr, timeOfNow } from '../util/util'; 6 | import { AO_TWITTER } from '../util/consts'; 7 | import Loading from '../elements/Loading'; 8 | import { Navigate } from 'react-router-dom'; 9 | import { publish, subscribe } from '../util/event'; 10 | 11 | declare var window: any; 12 | var msg_timer: any; 13 | 14 | interface ChatPageState { 15 | msg: string; 16 | messages: any; 17 | nickname: string; 18 | question: string; 19 | alert: string; 20 | loading: boolean; 21 | address: string; 22 | friend: string; 23 | my_avatar: string; 24 | my_nickname: string; 25 | friend_avatar: string; 26 | friend_nickname: string; 27 | chatList: any; 28 | navigate: string; 29 | } 30 | 31 | class ChatPage extends React.Component<{}, ChatPageState> { 32 | 33 | constructor(props: {}) { 34 | super(props); 35 | this.state = { 36 | msg: '', 37 | messages: '', 38 | nickname: '', 39 | question: '', 40 | alert: '', 41 | loading: true, 42 | address: '', 43 | friend: '', 44 | my_avatar: '', 45 | my_nickname: '', 46 | friend_avatar: '', 47 | friend_nickname: '', 48 | chatList: '', 49 | navigate: '', 50 | }; 51 | 52 | subscribe('go-chat', () => { 53 | this.setState({ navigate: '' }); 54 | this.start(); 55 | }); 56 | } 57 | 58 | componentDidMount() { 59 | this.start(); 60 | } 61 | 62 | componentWillUnmount(): void { 63 | clearInterval(msg_timer); 64 | } 65 | 66 | async start() { 67 | clearInterval(msg_timer); 68 | 69 | let path = window.location.hash.slice(1); 70 | let friend = path.substring(6); 71 | console.log("friend:", friend) 72 | 73 | let address = await getWalletAddress(); 74 | console.log("me:", address) 75 | 76 | this.setState({ address, friend }); 77 | 78 | if (friend) { 79 | setTimeout(() => { 80 | this.goDM(); 81 | }, 50); 82 | } 83 | else { 84 | setTimeout(() => { 85 | this.getChatList(); 86 | }, 50); 87 | } 88 | } 89 | 90 | async goDM() { 91 | this.getChatList(); 92 | 93 | // 94 | let my_profile = await getProfile(this.state.address); 95 | // console.log("my_profile:", my_profile) 96 | my_profile = my_profile[0]; 97 | if (my_profile) 98 | this.setState({ 99 | my_avatar: my_profile.avatar, 100 | my_nickname: my_profile.nickname, 101 | }) 102 | 103 | // 104 | let friend_profile = await getProfile(this.state.friend); 105 | // console.log("friend_profile:", friend_profile) 106 | 107 | if (friend_profile.length == 0) return; 108 | 109 | friend_profile = friend_profile[0]; 110 | if (friend_profile) 111 | this.setState({ 112 | friend_avatar: friend_profile.avatar, 113 | friend_nickname: friend_profile.nickname, 114 | }) 115 | 116 | setTimeout(() => { 117 | this.getMessages(); 118 | }, 50); 119 | 120 | clearInterval(msg_timer); 121 | 122 | setTimeout(() => { 123 | msg_timer = setInterval(() => this.getMessages(), 2000); 124 | }, 50); 125 | 126 | setTimeout(() => { 127 | this.scrollToBottom(); 128 | }, 1000); 129 | } 130 | 131 | async getChatList() { 132 | if (this.state.chatList.length > 0) return; 133 | 134 | let data = { address: this.state.address }; 135 | let chatList = await getDataFromAO(AO_TWITTER, 'GetMessages', data); 136 | // console.log("getChatList:", chatList) 137 | 138 | this.setState({ chatList }); 139 | 140 | if (chatList.length > 0) 141 | this.goChat(chatList[0].participant); 142 | else 143 | this.setState({ loading: false }); 144 | } 145 | 146 | async getMessages() { 147 | console.log("DM messages -->") 148 | let data = { friend: this.state.friend, address: this.state.address }; 149 | let messages = await getDataFromAO(AO_TWITTER, 'GetMessages', data); 150 | // console.log("messages:", messages) 151 | 152 | this.setState({ messages, loading: false }); 153 | setTimeout(() => { 154 | this.scrollToBottom(); 155 | }, 1000); 156 | } 157 | 158 | goChat(id: string) { 159 | this.setState({ navigate: '/chat/' + id, messages: [] }); 160 | setTimeout(() => { 161 | publish('go-chat'); 162 | // this.setState({ loading: true }); 163 | }, 50); 164 | } 165 | 166 | renderChatList() { 167 | // if (this.state.loading) 168 | // return (); 169 | 170 | let divs = []; 171 | let list = this.state.chatList; 172 | for (let i = 0; i < list.length; i++) { 173 | let data = list[i]; 174 | let selected = (this.state.friend == data.participant); 175 | 176 | divs.push( 177 |
this.goChat(data.participant)} 181 | > 182 | 183 |
184 |
{shortStr(data.nickname, 15)}
185 |
{shortAddr(data.participant, 4)}
186 |
187 |
188 | ) 189 | } 190 | 191 | // return divs.length > 0 ? divs :
No chat yet.
192 | return divs; 193 | } 194 | 195 | renderMessages() { 196 | if (this.state.loading) 197 | return (); 198 | 199 | let divs = []; 200 | 201 | for (let i = 0; i < this.state.messages.length; i++) { 202 | let data = this.state.messages[i]; 203 | let owner = (data.address == this.state.address); 204 | 205 | divs.push( 206 |
207 | {!owner && } 208 | 209 |
210 |
211 |
{ 212 | owner 213 | ? shortStr(this.state.my_nickname, 15) 214 | : shortStr(this.state.friend_nickname, 15)} 215 |
216 | 217 |
{shortAddr(data.address, 3)}
218 |
219 | 220 |
221 | {data.message} 222 |
223 | 224 |
225 | {formatTimestamp(data.time, true)} 226 |
227 |
228 | 229 | {owner && } 230 |
231 | ) 232 | } 233 | 234 | return divs.length > 0 ? divs :
No messages yet.
235 | } 236 | 237 | async sendMessage() { 238 | let msg = this.state.msg.trim(); 239 | if (!msg) { 240 | this.setState({ alert: 'Please input a message.' }) 241 | return; 242 | } else if (msg.length > 500) { 243 | this.setState({ alert: 'Message can be up to 500 characters long.' }) 244 | return; 245 | } 246 | 247 | this.setState({ msg: '' }); 248 | 249 | let data = { address: this.state.address, friend: this.state.friend, message: msg, time: timeOfNow() }; 250 | await messageToAO(AO_TWITTER, data, 'SendMessage'); 251 | 252 | setTimeout(() => { 253 | this.scrollToBottom(); 254 | }, 1000); 255 | } 256 | 257 | handleKeyDown = (event: any) => { 258 | if (event.key === 'Enter') { 259 | event.preventDefault(); // prevent the form submit action 260 | this.sendMessage(); 261 | } 262 | } 263 | 264 | scrollToBottom() { 265 | var scrollableDiv = document.getElementById("scrollableDiv"); 266 | if (scrollableDiv) { 267 | scrollableDiv.scrollTop = scrollableDiv.scrollHeight; 268 | } else { 269 | console.error("Element with id 'scrollableDiv' not found."); 270 | } 271 | } 272 | 273 | render() { 274 | if (this.state.navigate) 275 | return ; 276 | 277 | return ( 278 |
279 | {/*
AO Public Chatroom
*/} 280 |
281 | {this.renderChatList()} 282 |
283 | 284 |
285 | {this.renderMessages()} 286 |
287 | 288 | {this.state.address && this.state.friend && 289 |
290 | this.setState({ msg: e.target.value })} 296 | onKeyDown={this.handleKeyDown} 297 | /> 298 | 299 |
300 | } 301 | 302 | {/* */} 303 | this.setState({ alert: '' })} /> 304 |
305 | ) 306 | } 307 | } 308 | 309 | export default ChatPage; -------------------------------------------------------------------------------- /site/src/app/App.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --page-color: white; 3 | --body-color: white; 4 | --button-color: rgb(0, 145, 250); 5 | --button-hover-color: rgb(118, 129, 205); 6 | --section-color: rgb(227, 248, 227); 7 | --section-hover-color: rgb(207, 243, 207); 8 | --section-header-color: rgb(65, 67, 72); 9 | --link-color: rgb(99, 99, 229); 10 | --page-max-width: 650px; 11 | --fire-color: rgb(207, 82, 45); 12 | --fire-color-hover: rgb(185, 71, 36); 13 | 14 | --rt-color-white: #fff; 15 | --rt-color-dark: #222; 16 | --rt-color-success: #8dc572; 17 | --rt-color-error: #be6464; 18 | --rt-color-warning: #f0ad4e; 19 | --rt-color-info: #337ab7; 20 | --rt-opacity: 0.9; 21 | --rt-transition-show-delay: 0.15s; 22 | --rt-transition-closing-delay: 0.15s; 23 | } 24 | 25 | html { 26 | font-family: Lato; 27 | line-height: 25px; 28 | } 29 | 30 | body { 31 | background: var(--body-color); 32 | margin: 0px; 33 | } 34 | 35 | svg text { 36 | user-select: none; 37 | } 38 | 39 | input { 40 | font-size: inherit; 41 | font-family: inherit; 42 | height: 40px; 43 | min-height: 30px; 44 | border-radius: 6px; 45 | border: 1px solid gray; 46 | padding: 0 10px; 47 | } 48 | 49 | .input:disabled, 50 | input[disabled] { 51 | border: 1px solid lightgray; 52 | cursor: not-allowed; 53 | } 54 | 55 | textarea { 56 | font-size: inherit; 57 | font-family: inherit; 58 | height: 40px; 59 | min-height: 40px; 60 | border-radius: 6px; 61 | border: 0px; 62 | padding: 5px 10px 5px 10px; 63 | } 64 | 65 | [data-lastpass-icon-root] { 66 | display: none !important; 67 | } 68 | 69 | button { 70 | font-size: inherit; 71 | font-family: inherit; 72 | color: white; 73 | background-color: var(--section-header-color); 74 | /* background-color: var(--button-color); */ 75 | border-radius: 10px; 76 | border: 0px; 77 | cursor: pointer; 78 | padding: 0px 20px 0px 20px; 79 | min-height: 40px; 80 | min-width: 80px; 81 | } 82 | 83 | .button:disabled, 84 | button[disabled] { 85 | background-color: gray; 86 | cursor: not-allowed; 87 | } 88 | 89 | .button:hover, 90 | button[hover] { 91 | background-color: var(--button-hover-color); 92 | } 93 | 94 | select { 95 | font-size: inherit; 96 | font-family: inherit; 97 | height: 40px; 98 | border-radius: 6px; 99 | padding: 0 10px; 100 | } 101 | 102 | .select:disabled, 103 | select[disabled] { 104 | background-color: var(--page-color); 105 | cursor: not-allowed; 106 | } 107 | 108 | p { 109 | margin: 0; 110 | } 111 | 112 | a { 113 | color: var(--link-color); 114 | } 115 | 116 | /* loading animation */ 117 | #loading { 118 | display: inline-block; 119 | width: 20px; 120 | height: 20px; 121 | margin-bottom: 10px; 122 | border: 2px solid #ccc; 123 | border-radius: 50%; 124 | border-top-color: var(--link-color); 125 | animation: spin 1s ease-in-out infinite; 126 | } 127 | 128 | @keyframes spin { 129 | to { 130 | transform: rotate(360deg); 131 | } 132 | } 133 | 134 | /* custom scrollbar */ 135 | ::-webkit-scrollbar { 136 | width: 10px; 137 | height: 10px; 138 | } 139 | 140 | ::-webkit-scrollbar-track { 141 | border-radius: 10px; 142 | background: rgb(183, 191, 186); 143 | } 144 | 145 | ::-webkit-scrollbar-thumb { 146 | border-radius: 10px; 147 | background: rgb(133, 146, 138); 148 | } 149 | 150 | /* app styles */ 151 | .app-container { 152 | /* height: 100vh; */ 153 | /* max-height: -webkit-fill-available; */ 154 | max-width: 800px; 155 | margin: auto; 156 | } 157 | 158 | .app-navbar { 159 | position: fixed; 160 | top: 80px; 161 | } 162 | 163 | .app-content { 164 | overflow-y: auto; 165 | } 166 | 167 | .app-page { 168 | margin-left: 180px; 169 | margin-top: 50px; 170 | background-color: var(--page-color); 171 | } 172 | 173 | /* others style */ 174 | .app-status-row { 175 | display: flex; 176 | column-gap: 20px; 177 | justify-content: flex-end; 178 | color: gray; 179 | margin-top: 5px; 180 | } 181 | 182 | @media (max-width: 650px) { 183 | .app-navbar { 184 | display: none; 185 | } 186 | 187 | .app-page { 188 | margin-left: unset; 189 | margin-top: unset; 190 | } 191 | 192 | .app-status-row{ 193 | display: none; 194 | } 195 | } 196 | 197 | .app-status-data { 198 | display: flex; 199 | align-items: center; 200 | column-gap: 5px; 201 | font-size: 15px; 202 | } 203 | 204 | .app-logo-line { 205 | position: fixed; 206 | display: flex; 207 | padding: 10px; 208 | margin-bottom: 10px; 209 | } 210 | 211 | .app-logo { 212 | width: 110px; 213 | } 214 | 215 | .app-logo-text { 216 | color: var(--section-header-color); 217 | font-size: large; 218 | } 219 | 220 | .app-icon-button { 221 | display: flex; 222 | align-items: center; 223 | justify-content: center; 224 | column-gap: 8px; 225 | padding: 2px 13px; 226 | color: white; 227 | cursor: pointer; 228 | border-radius: 7px; 229 | cursor: pointer; 230 | min-width: 80px; 231 | min-height: 40px; 232 | background-color: var(--section-header-color); 233 | } 234 | 235 | .app-icon-button:hover { 236 | background-color: rgb(85, 87, 92); 237 | } 238 | 239 | .fire-color { 240 | background-color: var(--fire-color); 241 | } 242 | 243 | .fire-color:hover { 244 | background-color: var(--fire-color-hover); 245 | } 246 | 247 | .app-post-button { 248 | display: flex; 249 | align-items: center; 250 | justify-content: center; 251 | column-gap: 15px; 252 | width: 110px; 253 | color: white; 254 | border-radius: 30px; 255 | padding: 10px 20px; 256 | font-size: 19px; 257 | margin-left: 10px; 258 | margin-top: 20px; 259 | text-decoration: none; 260 | cursor: pointer; 261 | background-color: var(--section-header-color); 262 | } 263 | 264 | .app-post-button:hover { 265 | background-color: rgb(85, 87, 92); 266 | } 267 | 268 | .app-portrait-container { 269 | display: flex; 270 | align-items: center; 271 | column-gap: 15px; 272 | margin-left: 10px; 273 | margin-top: 50px; 274 | font-size: 19px; 275 | } 276 | 277 | /* ---- Quill editor config ----*/ 278 | 279 | .ql-editor { 280 | font-family: Lato; 281 | font-size: 1.2em; 282 | } 283 | 284 | .ql-container { 285 | border-bottom-left-radius: 0.5em; 286 | border-bottom-right-radius: 0.5em; 287 | } 288 | 289 | .ql-container.ql-snow { 290 | border: none !important; 291 | background-color: var(--section-color); 292 | } 293 | 294 | .ql-toolbar { 295 | border-top-left-radius: 0.5em; 296 | border-top-right-radius: 0.5em; 297 | } 298 | 299 | .ql-toolbar.ql-snow { 300 | border: none !important; 301 | background-color: var(--section-header-color); 302 | } 303 | 304 | .ql-toolbar .ql-stroke { 305 | fill: none !important; 306 | stroke: #fff !important; 307 | } 308 | 309 | .ql-toolbar .ql-fill { 310 | fill: #fff !important; 311 | stroke: none !important; 312 | } 313 | 314 | .ql-toolbar .ql-picker { 315 | color: #fff !important; 316 | } 317 | 318 | .ql-picker-item { 319 | color: black !important; 320 | } 321 | 322 | .ql-picker-item:hover { 323 | color: #06c !important; 324 | } 325 | 326 | .ql-toolbar button { 327 | min-width: unset; 328 | min-height: unset; 329 | } 330 | 331 | .ql-toolbar button:hover .ql-stroke { 332 | fill: none !important; 333 | stroke: yellow !important; 334 | } 335 | 336 | .ql-toolbar button:hover .ql-fill { 337 | fill: yellow !important; 338 | stroke: none !important; 339 | } 340 | 341 | .ql-toolbar button.ql-active .ql-stroke { 342 | fill: none !important; 343 | stroke: yellow !important; 344 | } 345 | 346 | .ql-toolbar button.ql-active .ql-fill { 347 | fill: yellow !important; 348 | stroke: none !important; 349 | } 350 | 351 | /* .ql-editor.ql-blank::before { */ 352 | /* color: #ffffff80 !important; */ 353 | /* } */ 354 | 355 | .ql-toolbar .ql-picker-label:hover { 356 | color: yellow !important; 357 | } 358 | 359 | .ql-toolbar .ql-picker-label.ql-active { 360 | color: yellow !important; 361 | } 362 | 363 | .ql-toolbar .ql-picker-label:hover .ql-stroke { 364 | fill: none !important; 365 | stroke: yellow !important; 366 | } 367 | 368 | .ql-toolbar .ql-picker-label.ql-active .ql-stroke { 369 | fill: none !important; 370 | stroke: yellow !important; 371 | } 372 | 373 | .ql-align-center { 374 | text-align: center; 375 | } 376 | 377 | .ql-align-right { 378 | text-align: right; 379 | } 380 | 381 | .ql-editor-image { 382 | max-width: 100%; 383 | cursor: pointer; 384 | } 385 | 386 | .ql-video { 387 | width: 100%; 388 | height: 300px; 389 | } 390 | 391 | /* skeleton */ 392 | .skeleton { 393 | display: flex; 394 | flex-direction: column; 395 | row-gap: 5px; 396 | overflow: hidden; 397 | padding: 0 10px; 398 | } 399 | 400 | .skeleton-profile { 401 | display: flex; 402 | align-items: center; 403 | column-gap: 10px; 404 | overflow: hidden; 405 | margin-bottom: 15px; 406 | } 407 | 408 | .skeleton-profile-portrait { 409 | background-image: linear-gradient(90deg, #556959 25%, #419758 37%, #716060 63%); 410 | width: 35px; 411 | height: 35px; 412 | border-radius: 50%; 413 | background-size: 400% 100%; 414 | background-position: 100% 50%; 415 | animation: skeleton-ani 2s ease infinite; 416 | } 417 | 418 | .skeleton-bar { 419 | background-image: linear-gradient(90deg, #556959 25%, #419758 37%, #716060 63%); 420 | width: 85%; 421 | height: 18px; 422 | background-size: 400% 100%; 423 | background-position: 100% 50%; 424 | animation: skeleton-ani 2s ease infinite; 425 | } 426 | 427 | .skeleton-bar.width2 { 428 | width: 75%; 429 | } 430 | 431 | .skeleton-bar.width3 { 432 | width: 100px; 433 | } 434 | 435 | @keyframes skeleton-ani { 436 | 0% { 437 | background-position: 100% 50%; 438 | } 439 | 440 | 100% { 441 | background-position: 0 50%; 442 | } 443 | } -------------------------------------------------------------------------------- /site/src/app/modals/EditProfileModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BsFillXCircleFill } from 'react-icons/bs'; 3 | import { Server } from '../../server/server'; 4 | import { messageToAO, randomAvatar, uuid } from '../util/util'; 5 | import './Modal.css' 6 | import './EditProfileModal.css' 7 | import MessageModal from './MessageModal'; 8 | import AlertModal from './AlertModal'; 9 | import Compressor from 'compressorjs'; 10 | import { createAvatar } from '@dicebear/core'; 11 | import { micah } from '@dicebear/collection'; 12 | import { AO_STORY, AO_TWITTER } from '../util/consts'; 13 | import { publish } from '../util/event'; 14 | 15 | interface EditProfileModalProps { 16 | open: boolean; 17 | onClose: Function; 18 | data: any; 19 | } 20 | 21 | interface EditProfileModalState { 22 | changeBanner: boolean; 23 | changePortrait: boolean; 24 | banner: string; 25 | avatar: string; 26 | nickname: string; 27 | bio: string; 28 | message: string; 29 | alert: string; 30 | openBannerList: boolean; 31 | openPortraitList: boolean; 32 | } 33 | 34 | class EditProfileModal extends React.Component { 35 | 36 | pickBanner = false; 37 | 38 | constructor(props: EditProfileModalProps) { 39 | super(props); 40 | 41 | this.state = { 42 | changeBanner: false, 43 | changePortrait: false, 44 | banner: '', 45 | avatar: '', 46 | nickname: '', 47 | bio: '', 48 | message: '', 49 | alert: '', 50 | openBannerList: false, 51 | openPortraitList: false 52 | }; 53 | 54 | this.onOpenBannerList = this.onOpenBannerList.bind(this); 55 | this.onCloseBannerList = this.onCloseBannerList.bind(this); 56 | this.onOpenPortraitList = this.onOpenPortraitList.bind(this); 57 | this.onClosePortraitList = this.onClosePortraitList.bind(this); 58 | this.onChangeName = this.onChangeName.bind(this); 59 | this.onChangeBio = this.onChangeBio.bind(this); 60 | this.saveProfile = this.saveProfile.bind(this); 61 | this.onClose = this.onClose.bind(this); 62 | this.onSelectFileChange = this.onSelectFileChange.bind(this); 63 | } 64 | 65 | componentDidMount(): void { 66 | this.start(); 67 | } 68 | 69 | async start() { 70 | this.setState({ 71 | banner: this.props.data.banner, 72 | avatar: this.props.data.avatar, 73 | nickname: this.props.data.nickname, 74 | bio: this.props.data.bio, 75 | }); 76 | } 77 | 78 | onOpenBannerList() { 79 | this.setState({ openBannerList: true }); 80 | } 81 | 82 | onCloseBannerList(banner: string) { 83 | let b = banner ? banner : this.state.banner; 84 | this.setState({ openBannerList: false, banner: b }); 85 | } 86 | 87 | onOpenPortraitList() { 88 | this.setState({ openPortraitList: true }); 89 | } 90 | 91 | onClosePortraitList(portrait: string) { 92 | let p = portrait ? portrait : this.state.avatar; 93 | this.setState({ openPortraitList: false, avatar: p }); 94 | } 95 | 96 | onChangeName(e: any) { 97 | this.setState({ nickname: e.currentTarget.value }); 98 | } 99 | 100 | onChangeBio(e: any) { 101 | this.setState({ bio: e.currentTarget.value }); 102 | } 103 | 104 | async saveProfile() { 105 | let profile = Server.service.getProfile(Server.service.getActiveAddress()); 106 | // console.log("cached profile:", profile) 107 | 108 | let nickname = this.state.nickname.trim(); 109 | 110 | let dirty = false; 111 | if (this.state.banner != profile.banner) dirty = true; 112 | if (this.state.avatar != profile.avatar) dirty = true; 113 | if (nickname != profile.nickname) dirty = true; 114 | if (this.state.bio != profile.bio) dirty = true; 115 | 116 | if (!dirty) { 117 | this.props.onClose(); 118 | return; 119 | } 120 | 121 | let errorMsg = ''; 122 | if (nickname.length < 2) 123 | errorMsg = 'Nickname must be at least 2 characters.'; 124 | if (nickname.length > 25) 125 | errorMsg = 'Nickname can be up to 25 characters.'; 126 | if (errorMsg != '') { 127 | this.setState({ alert: errorMsg }); 128 | return; 129 | } 130 | 131 | this.setState({ message: 'Saving profile...' }); 132 | 133 | let data = { 134 | address: Server.service.getActiveAddress(), 135 | avatar: this.state.avatar, 136 | banner: this.state.banner, 137 | nickname: nickname, 138 | bio: this.state.bio.trim(), 139 | time: this.props.data.time 140 | }; 141 | // console.log("data:", data) 142 | 143 | await messageToAO(AO_STORY, data, 'Register'); 144 | 145 | let response = await messageToAO(AO_TWITTER, data, 'Register'); 146 | 147 | if (response) { 148 | Server.service.addProfileToCache(data); 149 | this.setState({ message: '' }); 150 | this.props.onClose(); 151 | publish('profile-updated'); 152 | } 153 | else { 154 | this.setState({ message: '', alert: 'Setting the profile failed.' }); 155 | } 156 | } 157 | 158 | onClose() { 159 | this.props.onClose(); 160 | } 161 | 162 | selectImage(pickBanner: boolean) { 163 | this.pickBanner = pickBanner; 164 | 165 | const fileElem = document.getElementById("fileElem"); 166 | if (fileElem) { 167 | fileElem.click(); 168 | } 169 | } 170 | 171 | onSelectFileChange(e: React.FormEvent): void { 172 | this.processImage(e.currentTarget.files[0]); 173 | }; 174 | 175 | processImage(file: any) { 176 | if (!file) return; 177 | // let img = URL.createObjectURL(file); 178 | // console.log('FILE:', img); 179 | // this.setState({ portrait: img }); 180 | 181 | // Compress the file 182 | new Compressor(file, { 183 | maxWidth: 800, 184 | maxHeight: 800, 185 | convertSize: 180000, 186 | success: (result) => { 187 | // Encode the file using the FileReader API to Base64 188 | const reader = new FileReader(); 189 | reader.onloadend = () => { 190 | // console.log('Compress CoverImage', reader.result); 191 | let image = reader.result.toString(); 192 | if (this.pickBanner) 193 | this.setState({ banner: image }); 194 | else 195 | this.setState({ avatar: image }); 196 | }; 197 | 198 | reader.readAsDataURL(result); 199 | }, 200 | }); 201 | } 202 | 203 | createAvatar() { 204 | let random = uuid(); 205 | // localStorage.setItem('avatar', random); 206 | 207 | let nickname = localStorage.getItem('nickname'); 208 | const resp = createAvatar(micah, { 209 | seed: nickname + random 210 | }); 211 | 212 | const avatar = resp.toDataUriSync(); 213 | this.setState({ avatar }); 214 | } 215 | 216 | render() { 217 | if (!this.props.open) 218 | return (
); 219 | 220 | return ( 221 |
222 |
223 | 226 | 227 |
Edit Profile
228 |
229 |
230 | {/* this.selectImage(true)} /> */} 231 | 233 | this.selectImage(false)} /> 236 | {/* this.selectImage(false)} /> */} 237 | 238 | 244 | 245 | 252 |
253 | 254 |
255 |
256 |
Name
257 | 263 |
264 | 265 |
266 |
Bio
267 |