├── .editorconfig ├── .env ├── .github └── workflows │ └── contract-tests.yml ├── .gitignore ├── Clarinet.toml ├── README.md ├── contracts ├── clarinet │ └── sip-10-ft-standard.clar ├── hey-token.clar └── hey.clar ├── craco.config.js ├── jest.config.js ├── package.json ├── public ├── assets │ └── Stacks128w.png ├── icon.png ├── index.html └── token-metadata.json ├── settings └── Development.toml ├── src ├── common │ ├── addresses.ts │ ├── constants.ts │ ├── feed.ts │ ├── fonts │ │ ├── fira-code │ │ │ ├── woff │ │ │ │ ├── FiraCode-Bold.woff │ │ │ │ ├── FiraCode-Light.woff │ │ │ │ ├── FiraCode-Medium.woff │ │ │ │ ├── FiraCode-Regular.woff │ │ │ │ ├── FiraCode-SemiBold.woff │ │ │ │ └── FiraCode-VF.woff │ │ │ └── woff2 │ │ │ │ ├── FiraCode-Bold.woff2 │ │ │ │ ├── FiraCode-Light.woff2 │ │ │ │ ├── FiraCode-Medium.woff2 │ │ │ │ ├── FiraCode-Regular.woff2 │ │ │ │ ├── FiraCode-SemiBold.woff2 │ │ │ │ └── FiraCode-VF.woff2 │ │ ├── inter │ │ │ ├── Inter-Italic.woff │ │ │ ├── Inter-Italic.woff2 │ │ │ ├── Inter-Medium.woff │ │ │ ├── Inter-Medium.woff2 │ │ │ ├── Inter-MediumItalic.woff │ │ │ ├── Inter-MediumItalic.woff2 │ │ │ ├── Inter-Regular.woff │ │ │ ├── Inter-Regular.woff2 │ │ │ ├── Inter-SemiBold.woff │ │ │ ├── Inter-SemiBold.woff2 │ │ │ ├── Inter-SemiBoldItalic.woff │ │ │ ├── Inter-SemiBoldItalic.woff2 │ │ │ └── Inter.var.woff2 │ │ └── open-sauce-one │ │ │ ├── opensauceone-bold-webfont.woff │ │ │ ├── opensauceone-bold-webfont.woff2 │ │ │ ├── opensauceone-bolditalic-webfont.woff │ │ │ ├── opensauceone-bolditalic-webfont.woff2 │ │ │ ├── opensauceone-italic-webfont.woff │ │ │ ├── opensauceone-italic-webfont.woff2 │ │ │ ├── opensauceone-regular-webfont.woff │ │ │ ├── opensauceone-regular-webfont.woff2 │ │ │ ├── opensauceone-semibold-webfont.woff │ │ │ ├── opensauceone-semibold-webfont.woff2 │ │ │ ├── opensauceone-semibolditalic-webfont.woff │ │ │ └── opensauceone-semibolditalic-webfont.woff2 │ ├── hooks │ │ ├── use-account-names.ts │ │ ├── use-api-revalidation.ts │ │ ├── use-attachment.ts │ │ ├── use-auth-options.ts │ │ ├── use-boolean.ts │ │ ├── use-claim-hey.ts │ │ ├── use-compose-field.ts │ │ ├── use-compose.ts │ │ ├── use-current-address.ts │ │ ├── use-feed.ts │ │ ├── use-get-item-likes.ts │ │ ├── use-hey-balance.ts │ │ ├── use-hey-contract.ts │ │ ├── use-hey-transactions.ts │ │ ├── use-hover.ts │ │ ├── use-like-hey.ts │ │ ├── use-loading.ts │ │ ├── use-network.ts │ │ ├── use-publish-hey.ts │ │ ├── use-scroll-to-bottom.ts │ │ ├── use-show-welcome.ts │ │ ├── use-user.ts │ │ └── use-usersession.ts │ ├── time.ts │ └── utils.ts ├── components │ ├── app.tsx │ ├── avatar.tsx │ ├── button.tsx │ ├── claim-hey-button.tsx │ ├── compose.tsx │ ├── connect-graphic.tsx │ ├── connect-wallet-button.tsx │ ├── feed-item.tsx │ ├── feed.tsx │ ├── giphy.tsx │ ├── header.tsx │ ├── link.tsx │ ├── logo.tsx │ ├── pending-overlay.tsx │ ├── typography.tsx │ ├── user-area.tsx │ ├── user-avatar.tsx │ └── welcome-panel.tsx ├── index.css ├── index.tsx ├── react-app-env.d.ts └── store │ ├── api.ts │ ├── auth.ts │ ├── feed.ts │ ├── giphy.ts │ ├── hey.ts │ ├── names.ts │ └── ui.ts ├── tests ├── hey-token-client.ts ├── hey-token.test.ts └── hey.test.ts ├── tsconfig.json ├── tsconfig.paths.json ├── tsconfig.tests.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /.github/workflows/contract-tests.yml: -------------------------------------------------------------------------------- 1 | name: 'Test: Contracts' 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | jobs: 8 | test-contracts: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: '14' 17 | 18 | - name: Install dependencies 19 | uses: nick-invision/retry@v2 20 | with: 21 | timeout_seconds: 240 22 | max_attempts: 3 23 | retry_on: error 24 | command: yarn 25 | 26 | - name: Run tests 27 | run: yarn jest 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .idea 25 | -------------------------------------------------------------------------------- /Clarinet.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "." 3 | 4 | [contracts.sip-010-trait] 5 | path = "contracts/clarinet/sip-10-ft-standard.clar" 6 | depends_on = [] 7 | 8 | [contracts.hey] 9 | path = "contracts/hey.clar" 10 | depends_on = ["sip-010-trait"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heystack 2 | 3 | Sample messaging app. 4 | 5 | ```bash 6 | git clone https://github.com/hirosystems/heystack 7 | yarn && yarn start 8 | ``` 9 | 10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | -------------------------------------------------------------------------------- /contracts/clarinet/sip-10-ft-standard.clar: -------------------------------------------------------------------------------- 1 | (define-trait sip-010-trait 2 | ( 3 | ;; Transfer from the caller to a new principal 4 | (transfer (uint principal principal (optional (buff 34))) (response bool uint)) 5 | 6 | ;; the human readable name of the token 7 | (get-name () (response (string-ascii 32) uint)) 8 | 9 | ;; the ticker symbol, or empty if none 10 | (get-symbol () (response (string-ascii 32) uint)) 11 | 12 | ;; the number of decimals used, e.g. 6 would mean 1_000_000 represents 1 token 13 | (get-decimals () (response uint uint)) 14 | 15 | ;; the balance of the passed principal 16 | (get-balance (principal) (response uint uint)) 17 | 18 | ;; the current total supply (which does not need to be a constant) 19 | (get-total-supply () (response uint uint)) 20 | 21 | ;; an optional URI that represents metadata of this token 22 | (get-token-uri () (response (optional (string-utf8 256)) uint)) 23 | ) 24 | ) 25 | -------------------------------------------------------------------------------- /contracts/hey-token.clar: -------------------------------------------------------------------------------- 1 | ;; Implement the `ft-trait` trait defined in the `ft-trait` contract 2 | ;; https://github.com/hstove/stacks-fungible-token 3 | (impl-trait 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.ft-trait.sip-010-trait) 4 | 5 | (define-constant contract-creator tx-sender) 6 | 7 | (define-fungible-token hey-token) 8 | 9 | ;; Mint developer tokens 10 | (ft-mint? hey-token u10000 contract-creator) 11 | (ft-mint? hey-token u10000 'ST399W7Z9WS0GMSNQGJGME5JADNKN56R65VGM5KGA) ;; fara 12 | (ft-mint? hey-token u10000 'ST1X6M947Z7E58CNE0H8YJVJTVKS9VW0PHEG3NHN3) ;; thomas 13 | (ft-mint? hey-token u10000 'ST1NY8TXACV7D74886MK05SYW2XA72XJMDVPF3F3D) ;; kyran 14 | (ft-mint? hey-token u10000 'ST34XEPDJJFJKFPT87CCZQCPGXR4PJ8ERFRP0F3GX) ;; jasper 15 | (ft-mint? hey-token u10000 'ST3AGWHGAZKQS4JQ67WQZW5X8HZYZ4ZBWPPNWNMKF) ;; andres 16 | (ft-mint? hey-token u10000 'ST17YZQB1228EK9MPHQXA8GC4G3HVWZ66X779FEBY) ;; esh 17 | (ft-mint? hey-token u10000 'ST3Q0M9WAVBW633CG72VHNFZM2H82D2BJMBX85WP4) ;; mark 18 | 19 | ;; get the token balance of owner 20 | (define-read-only (get-balance (owner principal)) 21 | (begin 22 | (ok (ft-get-balance hey-token owner)))) 23 | 24 | ;; returns the total number of tokens 25 | (define-read-only (get-total-supply) 26 | (ok (ft-get-supply hey-token))) 27 | 28 | ;; returns the token name 29 | (define-read-only (get-name) 30 | (ok "Heystack Token")) 31 | 32 | ;; the symbol or "ticker" for this token 33 | (define-read-only (get-symbol) 34 | (ok "HEY")) 35 | 36 | ;; the number of decimals used 37 | (define-read-only (get-decimals) 38 | (ok u0)) 39 | 40 | ;; Transfers tokens to a recipient 41 | (define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) 42 | (if (is-eq tx-sender sender) 43 | (begin 44 | (try! (ft-transfer? hey-token amount sender recipient)) 45 | (print memo) 46 | (ok true) 47 | ) 48 | (err u4))) 49 | 50 | (define-public (get-token-uri) 51 | (ok (some u"https://heystack.xyz/token-metadata.json"))) 52 | 53 | (define-public (gift-tokens (recipient principal)) 54 | (begin 55 | (asserts! (is-eq tx-sender recipient) (err u0)) 56 | (ft-mint? hey-token u1 recipient) 57 | ) 58 | ) 59 | -------------------------------------------------------------------------------- /contracts/hey.clar: -------------------------------------------------------------------------------- 1 | ;; hey 2 | ;; HeyStack -- a smart contract app built for the Stacks blockchain 3 | 4 | (define-constant contract-creator tx-sender) 5 | 6 | (define-constant ERR_INVALID_CONTENT u0) 7 | (define-constant ERR_CANNOT_LIKE_NON_EXISTENT_CONTENT u1) 8 | (define-constant ERR_CAN_ONLY_REQUEST_HEY_FOR_YOURSELF u2) 9 | 10 | (define-constant HEY_TREASURY 'ST18QBQ9HBSGZ76SKYVN970Q4MVZHJDAX1S7QSP62) 11 | 12 | ;; 13 | ;; Data maps and vars 14 | (define-data-var content-index uint u0) 15 | 16 | (define-read-only (get-content-index) 17 | (ok (var-get content-index)) 18 | ) 19 | 20 | (define-map like-state 21 | { content-index: uint } 22 | { likes: uint } 23 | ) 24 | 25 | (define-map publisher-state 26 | { content-index: uint } 27 | { publisher: principal } 28 | ) 29 | 30 | (define-read-only (get-like-count (id uint)) 31 | ;; Checks map for like count of given id 32 | ;; defaults to 0 likes if no entry found 33 | (ok (default-to { likes: u0 } (map-get? like-state { content-index: id }))) 34 | ) 35 | 36 | 37 | (define-read-only (get-message-publisher (id uint)) 38 | ;; Checks map for like count of given id 39 | ;; defaults to 0 likes if no entry found 40 | (ok (unwrap-panic (get publisher (map-get? publisher-state { content-index: id })))) 41 | ) 42 | 43 | ;; 44 | ;; Private functions 45 | (define-private (increment-content-index) 46 | (begin 47 | (var-set content-index (+ (var-get content-index) u1)) 48 | (ok (var-get content-index)) 49 | ) 50 | ) 51 | 52 | (define-private (get-balance (recipient principal)) 53 | (contract-call? 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.hey-token get-balance recipient) 54 | ) 55 | 56 | ;; 57 | ;; Public functions 58 | (define-public (send-message (content (string-utf8 140) ) (attachment-uri (optional (string-utf8 256)))) 59 | (let ((id (unwrap! (increment-content-index) (err u0)))) 60 | (print { content: content, publisher: tx-sender, index: id, attachment-uri: attachment-uri }) 61 | (map-set like-state 62 | { content-index: id } 63 | { likes: u0 } 64 | ) 65 | (map-set publisher-state 66 | { content-index: id } 67 | { publisher: tx-sender } 68 | ) 69 | (transfer-hey u1 HEY_TREASURY) 70 | ) 71 | ) 72 | 73 | (define-public (like-message (id uint)) 74 | (begin 75 | (asserts! (>= (var-get content-index) id) (err ERR_CANNOT_LIKE_NON_EXISTENT_CONTENT)) 76 | (map-set like-state 77 | { content-index: id } 78 | { likes: (+ u1 (get likes (unwrap! (get-like-count id) (err u0)))) } 79 | ) 80 | (transfer-hey u1 (unwrap-panic (get-message-publisher id))) 81 | ) 82 | ) 83 | 84 | ;; 85 | ;; Token contract interactions 86 | (define-public (request-hey (recipient principal)) 87 | (begin 88 | (asserts! (is-eq contract-caller recipient) (err ERR_CAN_ONLY_REQUEST_HEY_FOR_YOURSELF)) 89 | (contract-call? 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.hey-token gift-tokens recipient) 90 | ) 91 | ) 92 | 93 | (define-public (transfer-hey (amount uint) (recipient principal)) 94 | (contract-call? 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.hey-token transfer amount tx-sender recipient none) 95 | ) 96 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const CracoAlias = require('craco-alias'); 2 | const CracoEsbuildPlugin = require('craco-esbuild'); 3 | 4 | module.exports = { 5 | plugins: [ 6 | { 7 | plugin: CracoAlias, 8 | options: { 9 | source: 'tsconfig', 10 | baseUrl: './src', 11 | tsConfigPath: './tsconfig.paths.json', 12 | }, 13 | }, 14 | { plugin: CracoEsbuildPlugin }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/src/', '/tests/', '/contracts/'], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hirosystems/heystack", 3 | "version": "0.1.0", 4 | "private": true, 5 | "prettier": "@stacks/prettier-config", 6 | "dependencies": { 7 | "@craco/craco": "6.1.2", 8 | "@emotion/react": "11.4.0", 9 | "@stacks/auth": "2.0.0-beta.0", 10 | "@stacks/blockchain-api-client": "0.61.0", 11 | "@stacks/connect": "5.5.0", 12 | "@stacks/connect-react": "10.0.0", 13 | "@stacks/network": "2.0.0-beta.0", 14 | "@stacks/transactions": "2.0.0-beta.0", 15 | "@stacks/ui": "7.8.0", 16 | "@stacks/ui-utils": "7.5.0", 17 | "boring-avatars": "1.5.5", 18 | "capsize": "2.0.0", 19 | "dayjs": "^1.10.5", 20 | "framer-motion": "4.1.17", 21 | "immer": "^9.0.2", 22 | "jotai": "0.16.8", 23 | "modern-normalize": "1.1.0", 24 | "react": "17.0.2", 25 | "react-dom": "17.0.2", 26 | "react-hot-toast": "^2.0.0", 27 | "react-icons": "4.2.0", 28 | "react-query": "3.16.0", 29 | "react-textarea-autosize": "8.3.3", 30 | "ts-debounce": "3.0.0", 31 | "typescript": "4.1.2", 32 | "web-vitals": "2.0.0" 33 | }, 34 | "devDependencies": { 35 | "@blockstack/clarity": "0.3.14", 36 | "@blockstack/clarity-native-bin": "0.3.14", 37 | "@blockstack/stacks-blockchain-api-types": "0.55.3", 38 | "@stacks/eslint-config": "1.0.9", 39 | "@stacks/prettier-config": "0.0.8", 40 | "@testing-library/jest-dom": "5.11.4", 41 | "@testing-library/react": "11.1.0", 42 | "@testing-library/user-event": "13.1.9", 43 | "@types/jest": "26.0.15", 44 | "@types/node": "15.6.2", 45 | "@types/react": "17.0.9", 46 | "@types/react-dom": "17.0.6", 47 | "craco-alias": "3.0.1", 48 | "craco-esbuild": "0.3.2", 49 | "csstype": "3.0.8", 50 | "esbuild": "0.12.5", 51 | "eslint-plugin-prettier": "3.4.0", 52 | "jest": "27.0.3", 53 | "prettier": "2.3.0", 54 | "react-scripts": "4.0.3", 55 | "ts-jest": "27.0.2" 56 | }, 57 | "scripts": { 58 | "dev": "yarn start", 59 | "start": "craco start", 60 | "build": "craco build", 61 | "test": "craco test", 62 | "eject": "react-scripts eject", 63 | "lint": "yarn lint:eslint && yarn lint:prettier", 64 | "lint:eslint": "eslint \"src/**/*.{ts,tsx}\"", 65 | "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix", 66 | "lint:prettier": "prettier --check \"src/**/*.{ts,tsx}\" *.js", 67 | "lint:prettier:fix": "prettier --write \"src/**/*.{ts,tsx}\" *.js" 68 | }, 69 | "eslintConfig": { 70 | "extends": [ 71 | "@stacks/eslint-config" 72 | ], 73 | "rules": { 74 | "@typescript-eslint/no-unsafe-assignment": 0, 75 | "@typescript-eslint/explicit-module-boundary-types": 0, 76 | "@typescript-eslint/no-unsafe-member-access": 0, 77 | "@typescript-eslint/no-unsafe-call": 0, 78 | "@typescript-eslint/no-unsafe-return": 0, 79 | "@typescript-eslint/require-await": 0 80 | }, 81 | "parserOptions": { 82 | "project": [ 83 | "./tsconfig.json" 84 | ] 85 | } 86 | }, 87 | "browserslist": { 88 | "production": [ 89 | ">0.2%", 90 | "not dead", 91 | "not op_mini all" 92 | ], 93 | "development": [ 94 | "last 1 chrome version", 95 | "last 1 firefox version", 96 | "last 1 safari version" 97 | ] 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /public/assets/Stacks128w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/public/assets/Stacks128w.png -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/public/icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Heystack 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /public/token-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Heystack", 3 | "description": "Heystack is a SIP-010-compliant fungible token on the Stacks Blockchain, used on the Heystack app", 4 | "image": "https://heystack.xyz/assets/Stacks128w.png" 5 | } 6 | -------------------------------------------------------------------------------- /settings/Development.toml: -------------------------------------------------------------------------------- 1 | [network] 2 | name = "Development" 3 | 4 | [accounts.wallet_1] 5 | mnemonic = "dance news bachelor pink hammer clerk solve lake mushroom warm draw cousin forest shock believe smoke lift spin laundry couch gloom hold hurry decline" 6 | balance = 1_000_000 7 | 8 | [accounts.wallet_2] 9 | mnemonic = "apology together shy taxi glare struggle hip camp engage lion possible during squeeze hen exotic marriage misery kiwi once quiz enough exhibit immense tooth" 10 | balance = 1_000_000 11 | 12 | [accounts.wallet_3] 13 | mnemonic = "replace swing shove congress smoke banana tired term blanket nominee leave club myself swing egg virus answer bulk useful start decrease family energy february" 14 | balance = 1_000_000 15 | 16 | [accounts.wallet_4] 17 | mnemonic = "fetch outside black test wash cover just actual execute nice door want airport betray quantum stamp fish act pen trust portion fatigue scissors vague" 18 | balance = 1_000_000 19 | 20 | [accounts.wallet_5] 21 | mnemonic = "east load echo merit ignore hip tag obvious truly adjust smart panther deer aisle north hotel process frown lock property catch bless notice topple" 22 | balance = 1_000_000 23 | 24 | [accounts.wallet_6] 25 | mnemonic = "pulp when detect fun unaware reduce promote tank success lecture cool cheese object amazing hunt plug wing month hello tunnel detect connect floor brush" 26 | balance = 1_000_000 27 | 28 | [accounts.wallet_7] 29 | mnemonic = "glide clown kitchen picnic basket hidden asset beyond kid plug carbon talent drama wet pet rhythm hero nest purity baby bicycle ghost sponsor dragon" 30 | balance = 1_000_000 31 | 32 | [accounts.wallet_8] 33 | mnemonic = "antenna bitter find rely gadget father exact excuse cross easy elbow alcohol injury loud silk bird crime cabbage winter fit wide screen update october" 34 | balance = 1_000_000 35 | 36 | [accounts.wallet_9] 37 | mnemonic = "market ocean tortoise venue vivid coach machine category conduct enable insect jump fog file test core book chaos crucial burst version curious prosper fever" 38 | balance = 1_000_000 39 | -------------------------------------------------------------------------------- /src/common/addresses.ts: -------------------------------------------------------------------------------- 1 | import { c32address, c32addressDecode, versions } from 'c32check'; 2 | 3 | type Networks = 'testnet' | 'mainnet'; 4 | 5 | type Versions = 6 | | typeof versions.testnet.p2pkh 7 | | typeof versions.testnet.p2sh 8 | | typeof versions.mainnet.p2pkh 9 | | typeof versions.mainnet.p2sh; 10 | 11 | interface AddressDetails { 12 | version: Versions; 13 | network: Networks; 14 | type: 'p2pkh' | 'p2sh'; 15 | } 16 | 17 | /** 18 | * Get address details 19 | * 20 | * Takes a C32 address and provides more verbose details about type and network 21 | */ 22 | export function getAddressDetails(address: string): AddressDetails { 23 | const [version] = c32addressDecode(address); 24 | 25 | if (version === versions.testnet.p2pkh) { 26 | return { 27 | version, 28 | network: 'testnet', 29 | type: 'p2pkh', 30 | }; 31 | } else if (version === versions.testnet.p2sh) { 32 | return { 33 | version, 34 | network: 'testnet', 35 | type: 'p2sh', 36 | }; 37 | } else if (version === versions.mainnet.p2pkh) { 38 | return { 39 | version, 40 | type: 'p2pkh', 41 | network: 'mainnet', 42 | }; 43 | } else if (version === versions.mainnet.p2sh) { 44 | return { 45 | version, 46 | type: 'p2sh', 47 | network: 'mainnet', 48 | }; 49 | } else { 50 | throw new Error(`Unexpected address version: ${version}`); 51 | } 52 | } 53 | 54 | /** 55 | * Invert address 56 | * 57 | * Automatically invert address between testnet/mainnet 58 | */ 59 | export const invertAddress = (address: string): string => { 60 | const [version, hash160] = c32addressDecode(address); 61 | let _version = 0; 62 | if (version === versions.mainnet.p2pkh) { 63 | _version = versions.testnet.p2pkh; 64 | } else if (version === versions.mainnet.p2sh) { 65 | _version = versions.testnet.p2sh; 66 | } else if (version === versions.testnet.p2pkh) { 67 | _version = versions.mainnet.p2pkh; 68 | } else if (version === versions.testnet.p2sh) { 69 | _version = versions.mainnet.p2sh; 70 | } else { 71 | throw new Error(`Unexpected address version: ${version}`); 72 | } 73 | return c32address(_version, hash160); 74 | }; 75 | 76 | /** 77 | * Convert address 78 | * 79 | * Converts a STACKS address to a given network mode (testnet/mainnet) 80 | */ 81 | 82 | export function convertAddress(address: string, network: 'testnet' | 'mainnet'): string { 83 | const details = getAddressDetails(address); 84 | if (details.network !== network) { 85 | return invertAddress(address); 86 | } 87 | return address; 88 | } 89 | -------------------------------------------------------------------------------- /src/common/constants.ts: -------------------------------------------------------------------------------- 1 | export const HEY_TOKEN_ADDRESS = 'ST21FTC82CCKE0YH9SK5SJ1D4XEMRA069FKV0VJ8N.hey-token'; 2 | export const HEY_CONTRACT = 'ST21FTC82CCKE0YH9SK5SJ1D4XEMRA069FKV0VJ8N.hey-final'; 3 | export const MESSAGE_FUNCTION = 'send-message'; 4 | export const LIKE_FUNCTION = 'like-message'; 5 | export const REQUEST_FUNCTION = 'request-hey'; 6 | -------------------------------------------------------------------------------- /src/common/feed.ts: -------------------------------------------------------------------------------- 1 | export interface FeedItem { 2 | user: { 3 | address: string; 4 | name?: string; 5 | }; 6 | heystack: { 7 | id: number; 8 | content: string; 9 | upvotes: number; 10 | uri?: string; 11 | timestamp?: number; 12 | }; 13 | } 14 | 15 | export const feed: FeedItem[] = [ 16 | { 17 | user: { 18 | address: 'SP3TMFBG2FSSEHA5Q81ZMVMRB9GK0METVDV7RENRC', 19 | name: 'j.btc', 20 | }, 21 | heystack: { 22 | id: 0, 23 | content: 'upvote this and give me HEY', 24 | upvotes: 500, 25 | }, 26 | }, 27 | { 28 | user: { 29 | address: 'SPCTKKK7WHNX7HYKXW15GPNRHVV70DP1J0Z2M4CM', 30 | name: 'andy.btc', 31 | }, 32 | heystack: { 33 | id: 1, 34 | content: 'wen stx moon ser pls', 35 | upvotes: 20, 36 | }, 37 | }, 38 | { 39 | user: { 40 | address: 'SP24MGQCNYEMQX33FB5BPM94RG3R972Q87BTM3X98', 41 | name: 'thomas.btc', 42 | }, 43 | heystack: { 44 | id: 2, 45 | content: 'We discussed generally the trade-offs to showing raw parameter details by default.', 46 | upvotes: 999, 47 | }, 48 | }, 49 | { 50 | user: { 51 | address: 'SPCTKKK7WHNX7HYKXW15GPNRHVV70DP1J0Z2M4CM', 52 | name: 'mark.btc', 53 | }, 54 | heystack: { 55 | id: 3, 56 | content: 'We discussed generally the trade-offs to showing raw parameter details by default.', 57 | upvotes: 2, 58 | }, 59 | }, 60 | { 61 | user: { 62 | address: 'SP3TMFBG2FSSEHA5Q81ZMVMRB9GK0METVDV7RENRC', 63 | name: 'j.btc', 64 | }, 65 | heystack: { 66 | id: 4, 67 | content: 'Hey helllooo', 68 | upvotes: 0, 69 | }, 70 | }, 71 | ]; 72 | -------------------------------------------------------------------------------- /src/common/fonts/fira-code/woff/FiraCode-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/fira-code/woff/FiraCode-Bold.woff -------------------------------------------------------------------------------- /src/common/fonts/fira-code/woff/FiraCode-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/fira-code/woff/FiraCode-Light.woff -------------------------------------------------------------------------------- /src/common/fonts/fira-code/woff/FiraCode-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/fira-code/woff/FiraCode-Medium.woff -------------------------------------------------------------------------------- /src/common/fonts/fira-code/woff/FiraCode-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/fira-code/woff/FiraCode-Regular.woff -------------------------------------------------------------------------------- /src/common/fonts/fira-code/woff/FiraCode-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/fira-code/woff/FiraCode-SemiBold.woff -------------------------------------------------------------------------------- /src/common/fonts/fira-code/woff/FiraCode-VF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/fira-code/woff/FiraCode-VF.woff -------------------------------------------------------------------------------- /src/common/fonts/fira-code/woff2/FiraCode-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/fira-code/woff2/FiraCode-Bold.woff2 -------------------------------------------------------------------------------- /src/common/fonts/fira-code/woff2/FiraCode-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/fira-code/woff2/FiraCode-Light.woff2 -------------------------------------------------------------------------------- /src/common/fonts/fira-code/woff2/FiraCode-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/fira-code/woff2/FiraCode-Medium.woff2 -------------------------------------------------------------------------------- /src/common/fonts/fira-code/woff2/FiraCode-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/fira-code/woff2/FiraCode-Regular.woff2 -------------------------------------------------------------------------------- /src/common/fonts/fira-code/woff2/FiraCode-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/fira-code/woff2/FiraCode-SemiBold.woff2 -------------------------------------------------------------------------------- /src/common/fonts/fira-code/woff2/FiraCode-VF.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/fira-code/woff2/FiraCode-VF.woff2 -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter-Italic.woff -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter-Italic.woff2 -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter-Medium.woff -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter-Medium.woff2 -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter-MediumItalic.woff -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter-MediumItalic.woff2 -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter-Regular.woff -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter-Regular.woff2 -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter-SemiBold.woff -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter-SemiBold.woff2 -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter-SemiBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter-SemiBoldItalic.woff -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter-SemiBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter-SemiBoldItalic.woff2 -------------------------------------------------------------------------------- /src/common/fonts/inter/Inter.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/inter/Inter.var.woff2 -------------------------------------------------------------------------------- /src/common/fonts/open-sauce-one/opensauceone-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/open-sauce-one/opensauceone-bold-webfont.woff -------------------------------------------------------------------------------- /src/common/fonts/open-sauce-one/opensauceone-bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/open-sauce-one/opensauceone-bold-webfont.woff2 -------------------------------------------------------------------------------- /src/common/fonts/open-sauce-one/opensauceone-bolditalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/open-sauce-one/opensauceone-bolditalic-webfont.woff -------------------------------------------------------------------------------- /src/common/fonts/open-sauce-one/opensauceone-bolditalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/open-sauce-one/opensauceone-bolditalic-webfont.woff2 -------------------------------------------------------------------------------- /src/common/fonts/open-sauce-one/opensauceone-italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/open-sauce-one/opensauceone-italic-webfont.woff -------------------------------------------------------------------------------- /src/common/fonts/open-sauce-one/opensauceone-italic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/open-sauce-one/opensauceone-italic-webfont.woff2 -------------------------------------------------------------------------------- /src/common/fonts/open-sauce-one/opensauceone-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/open-sauce-one/opensauceone-regular-webfont.woff -------------------------------------------------------------------------------- /src/common/fonts/open-sauce-one/opensauceone-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/open-sauce-one/opensauceone-regular-webfont.woff2 -------------------------------------------------------------------------------- /src/common/fonts/open-sauce-one/opensauceone-semibold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/open-sauce-one/opensauceone-semibold-webfont.woff -------------------------------------------------------------------------------- /src/common/fonts/open-sauce-one/opensauceone-semibold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/open-sauce-one/opensauceone-semibold-webfont.woff2 -------------------------------------------------------------------------------- /src/common/fonts/open-sauce-one/opensauceone-semibolditalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/open-sauce-one/opensauceone-semibolditalic-webfont.woff -------------------------------------------------------------------------------- /src/common/fonts/open-sauce-one/opensauceone-semibolditalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirosystems/heystack/6a96aca933ead9d122a4016f513761be204f091e/src/common/fonts/open-sauce-one/opensauceone-semibolditalic-webfont.woff2 -------------------------------------------------------------------------------- /src/common/hooks/use-account-names.ts: -------------------------------------------------------------------------------- 1 | import { useAtomValue } from 'jotai/utils'; 2 | import { namesAtom } from '@store/names'; 3 | 4 | export function useAccountNames(address: string) { 5 | return useAtomValue(namesAtom(address)); 6 | } 7 | -------------------------------------------------------------------------------- /src/common/hooks/use-api-revalidation.ts: -------------------------------------------------------------------------------- 1 | import { useUpdateAtom } from 'jotai/utils'; 2 | import { incrementAtom } from '@store/hey'; 3 | import { useCallback } from 'react'; 4 | 5 | export function useApiRevalidation() { 6 | const updateIncrement = useUpdateAtom(incrementAtom); 7 | return useCallback(() => updateIncrement(i => i + 1), [updateIncrement]); 8 | } 9 | -------------------------------------------------------------------------------- /src/common/hooks/use-attachment.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from 'jotai'; 2 | import { attachmentUriAtom } from '@store/feed'; 3 | 4 | export function useAttachment() { 5 | const [state, setState] = useAtom(attachmentUriAtom); 6 | const updateAttachment = (value: string) => setState(value); 7 | const resetAttachment = () => updateAttachment(''); 8 | const hasAttachment = state !== ''; 9 | return { 10 | hasAttachment, 11 | attachment: state, 12 | updateAttachment, 13 | resetAttachment, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/common/hooks/use-auth-options.ts: -------------------------------------------------------------------------------- 1 | import { useUserSession } from '@hooks/use-usersession'; 2 | import { useLoading } from '@hooks/use-loading'; 3 | import { LOADING_KEYS } from '@store/ui'; 4 | import { useUser } from '@hooks/use-user'; 5 | import { useCallback } from 'react'; 6 | import { FinishedData } from '@stacks/connect-react'; 7 | import { AuthOptions } from '@stacks/connect'; 8 | 9 | export function useAuthOptions() { 10 | const userSession = useUserSession(); 11 | const { setIsLoading } = useLoading(LOADING_KEYS.AUTH); 12 | const { setUser } = useUser(); 13 | 14 | const onFinish = useCallback( 15 | async ({ userSession }: FinishedData) => { 16 | const userData = userSession?.loadUserData?.(); 17 | await setUser(userData); 18 | void setIsLoading(false); 19 | }, 20 | [setUser] 21 | ); 22 | const onCancel = useCallback(() => { 23 | void setIsLoading(false); 24 | }, [setIsLoading]); 25 | 26 | const authOptions: AuthOptions = { 27 | manifestPath: '/static/manifest.json', 28 | userSession, 29 | onFinish, 30 | onCancel, 31 | appDetails: { 32 | name: 'Heystack', 33 | icon: '/icon.png', 34 | }, 35 | }; 36 | return authOptions; 37 | } 38 | -------------------------------------------------------------------------------- /src/common/hooks/use-boolean.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from 'jotai'; 2 | import { booleanAtom } from '@store/ui'; 3 | 4 | export function useToggle(key: string) { 5 | const [toggle, setToggle] = useAtom(booleanAtom(key)); 6 | const handleToggle = () => setToggle(s => !s); 7 | return { 8 | toggle, 9 | handleToggle, 10 | setToggle, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/common/hooks/use-claim-hey.ts: -------------------------------------------------------------------------------- 1 | import { useLoading } from '@hooks/use-loading'; 2 | import { LOADING_KEYS } from '@store/ui'; 3 | import { useConnect } from '@stacks/connect-react'; 4 | import { useNetwork } from '@hooks/use-network'; 5 | import { useCallback } from 'react'; 6 | import { useHeyContract } from '@hooks/use-hey-contract'; 7 | import { REQUEST_FUNCTION } from '@common/constants'; 8 | import { principalCV } from '@stacks/transactions/dist/clarity/types/principalCV'; 9 | import { useCurrentAddress } from '@hooks/use-current-address'; 10 | 11 | export function useHandleClaimHey() { 12 | const address = useCurrentAddress(); 13 | const { setIsLoading } = useLoading(LOADING_KEYS.CLAIM_HEY); 14 | const { doContractCall } = useConnect(); 15 | const [contractAddress, contractName] = useHeyContract(); 16 | const network = useNetwork(); 17 | 18 | const onFinish = useCallback(() => { 19 | void setIsLoading(false); 20 | }, [setIsLoading]); 21 | 22 | const onCancel = useCallback(() => { 23 | void setIsLoading(false); 24 | }, [setIsLoading]); 25 | 26 | return useCallback(() => { 27 | void setIsLoading(true); 28 | void doContractCall({ 29 | contractAddress, 30 | contractName, 31 | functionName: REQUEST_FUNCTION, 32 | functionArgs: [principalCV(address)], 33 | onFinish, 34 | onCancel, 35 | network, 36 | stxAddress: address, 37 | }); 38 | }, [setIsLoading, onFinish, network, onCancel, address, doContractCall]); 39 | } 40 | -------------------------------------------------------------------------------- /src/common/hooks/use-compose-field.ts: -------------------------------------------------------------------------------- 1 | import { useHeyBalance } from '@hooks/use-hey-balance'; 2 | import { useUser } from '@hooks/use-user'; 3 | import { useCompose } from '@hooks/use-compose'; 4 | import { useHandlePublishContent } from '@hooks/use-publish-hey'; 5 | import { useLoading } from '@hooks/use-loading'; 6 | import { LOADING_KEYS } from '@store/ui'; 7 | import { useCallback } from 'react'; 8 | import { useToggle } from '@hooks/use-boolean'; 9 | 10 | export const useComposeField = () => { 11 | const balance = useHeyBalance(); 12 | const { isSignedIn } = useUser(); 13 | const { value, handleUpdate, handleReset } = useCompose(); 14 | const handlePublishContent = useHandlePublishContent(); 15 | const { isLoading } = useLoading(LOADING_KEYS.SEND_HEY); 16 | const handleSubmit = useCallback(() => { 17 | handlePublishContent(value, () => { 18 | void handleReset(); 19 | }); 20 | }, [handlePublishContent, value, handleReset]); 21 | 22 | const hasNoBalance = balance === '0' || !balance; 23 | 24 | const onSubmit = useCallback(() => { 25 | if (!isSignedIn) { 26 | return; 27 | } 28 | handleSubmit(); 29 | }, [handleSubmit, isSignedIn]); 30 | 31 | return { 32 | onSubmit, 33 | onChange: handleUpdate, 34 | value, 35 | isSignedIn, 36 | isLoading, 37 | hasNoBalance, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/common/hooks/use-compose.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useAtom } from 'jotai'; 3 | import { composeHeystackAom } from '@store/feed'; 4 | import { useToggle } from '@hooks/use-boolean'; 5 | import { gihpyQueryAtom } from '@store/giphy'; 6 | import { useAttachment } from '@hooks/use-attachment'; 7 | 8 | export function useCompose() { 9 | const { toggle: giphyIsShowing } = useToggle('GIF_RESULTS'); 10 | const { resetAttachment } = useAttachment(); 11 | const [value, setValue] = useAtom(composeHeystackAom); 12 | const [giphyValue, setGiphyValue] = useAtom(gihpyQueryAtom); 13 | 14 | const handleUpdate = useCallback( 15 | (event: React.FormEvent) => { 16 | const update = event.currentTarget.value; 17 | if (giphyIsShowing) { 18 | void setGiphyValue(event.currentTarget.value); 19 | return; 20 | } 21 | if (update.length < 140) { 22 | void setValue(event.currentTarget.value); 23 | } 24 | }, 25 | [setGiphyValue, setValue, giphyIsShowing] 26 | ); 27 | 28 | const handleReset = () => { 29 | void setGiphyValue(''); 30 | void resetAttachment(); 31 | void setValue(''); 32 | }; 33 | 34 | return { 35 | value: giphyIsShowing ? giphyValue : value, 36 | handleUpdate, 37 | handleReset, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/common/hooks/use-current-address.ts: -------------------------------------------------------------------------------- 1 | import { useUser } from '@hooks/use-user'; 2 | 3 | export function useCurrentAddress() { 4 | const { addresses } = useUser(); 5 | return addresses?.testnet; 6 | } 7 | 8 | // Temporarily being used to get names from mainnet address 9 | export function useCurrentMainnetAddress() { 10 | const { addresses } = useUser(); 11 | return addresses?.mainnet; 12 | } 13 | -------------------------------------------------------------------------------- /src/common/hooks/use-feed.ts: -------------------------------------------------------------------------------- 1 | import { contentTransactionsAtom, Heystack } from '@store/hey'; 2 | import { useAtomValue } from 'jotai/utils'; 3 | import { useEffect } from 'react'; 4 | import { feedItemsAtom } from '@store/feed'; 5 | import { useImmerAtom } from 'jotai/immer'; 6 | 7 | export function useFeed() { 8 | const apiData = useAtomValue(contentTransactionsAtom); 9 | const [feed, setFeed] = useImmerAtom>(feedItemsAtom); 10 | // this effect is to update our feed view without causing un-needed re-renders 11 | // @see https://docs.pmnd.rs/jotai/integrations/immer for immer docs 12 | useEffect(() => { 13 | // init 14 | if (Object.keys(feed).length === 0) { 15 | apiData.forEach(item => { 16 | setFeed(draft => { 17 | draft[item.id] = item; 18 | }); 19 | }); 20 | } else { 21 | // find our new items that have yet to be added 22 | const newItems = apiData.filter(item => !feed[item.id] && item.isPending); 23 | 24 | if (newItems?.length) { 25 | // mutate state with new items 26 | void setFeed(draft => { 27 | newItems.forEach(item => { 28 | if (!draft[item.id]) draft[item.id] = item; 29 | }); 30 | }); 31 | } 32 | 33 | // this updates as items confirm 34 | const itemsNoLongerPending = apiData.filter(item => !item.isPending); 35 | 36 | // this is static unless we update it 37 | const feedItemsPending = Object.values(feed).filter(item => item.isPending); 38 | 39 | // for each item that is no longer pending 40 | itemsNoLongerPending.forEach(item => { 41 | // if we find it in currently pending items 42 | if (feedItemsPending.find(_item => item.id === _item.id)) { 43 | // update just this one 44 | setFeed(draft => { 45 | draft[item.id] = { 46 | ...draft[item.id], 47 | index: item.index, 48 | isPending: false, 49 | }; 50 | }); 51 | } 52 | }); 53 | } 54 | }, [apiData, feed, setFeed]); 55 | 56 | return Object.values(feed); 57 | } 58 | -------------------------------------------------------------------------------- /src/common/hooks/use-get-item-likes.ts: -------------------------------------------------------------------------------- 1 | import { useAtomValue } from 'jotai/utils'; 2 | import { itemLikesAtom } from '@store/hey'; 3 | 4 | export function useGetItemLikes(index: number) { 5 | return useAtomValue(itemLikesAtom(index)); 6 | } 7 | -------------------------------------------------------------------------------- /src/common/hooks/use-hey-balance.ts: -------------------------------------------------------------------------------- 1 | import { useAtomValue } from 'jotai/utils'; 2 | import { userHeyBalanceAtom } from '@store/hey'; 3 | import { useCurrentAddress } from '@hooks/use-current-address'; 4 | 5 | export function useHeyBalance() { 6 | const address = useCurrentAddress(); 7 | return useAtomValue(userHeyBalanceAtom(address)); 8 | } 9 | -------------------------------------------------------------------------------- /src/common/hooks/use-hey-contract.ts: -------------------------------------------------------------------------------- 1 | import { HEY_CONTRACT, HEY_TOKEN_ADDRESS } from '@common/constants'; 2 | 3 | export function useHeyContract(): [contractAddress: string, contractName: string] { 4 | return HEY_CONTRACT.split('.') as [contractAddress: string, contractName: string]; 5 | } 6 | 7 | export function useHeyTokenContract() { 8 | return HEY_TOKEN_ADDRESS.split('.') as [contractAddress: string, contractName: string]; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/hooks/use-hey-transactions.ts: -------------------------------------------------------------------------------- 1 | import { useAtomValue } from 'jotai/utils'; 2 | import { heyTransactionsAtom } from '@store/hey'; 3 | 4 | export function useHeyTransactions() { 5 | return useAtomValue(heyTransactionsAtom); 6 | } 7 | -------------------------------------------------------------------------------- /src/common/hooks/use-hover.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function useHover( 4 | setter: (state: boolean) => void, 5 | pause?: boolean 6 | ): { 7 | onMouseEnter: (e: React.MouseEvent) => void; 8 | onMouseLeave: (e: React.MouseEvent) => void; 9 | } { 10 | const bind = React.useMemo( 11 | () => ({ 12 | onMouseEnter: (e: React.MouseEvent) => (!pause ? setter(true) : null), 13 | onMouseLeave: (e: React.MouseEvent) => (!pause ? setter(false) : null), 14 | }), 15 | [pause] 16 | ); 17 | 18 | return bind; 19 | } 20 | -------------------------------------------------------------------------------- /src/common/hooks/use-like-hey.ts: -------------------------------------------------------------------------------- 1 | import { useLoading } from '@hooks/use-loading'; 2 | import { LOADING_KEYS, showPendingOverlayAtom } from '@store/ui'; 3 | import { FinishedTxPayload, useConnect } from '@stacks/connect-react'; 4 | import { useNetwork } from '@hooks/use-network'; 5 | import { useCallback } from 'react'; 6 | import { 7 | createAssetInfo, 8 | createFungiblePostCondition, 9 | FungibleConditionCode, 10 | uintCV, 11 | } from '@stacks/transactions'; 12 | import { useUpdateAtom } from 'jotai/utils'; 13 | import { useCurrentAddress } from '@hooks/use-current-address'; 14 | import { useHeyContract } from '@hooks/use-hey-contract'; 15 | import { LIKE_FUNCTION } from '@common/constants'; 16 | import BN from 'bn.js'; 17 | import { toast } from 'react-hot-toast'; 18 | 19 | export function useHandleLikeHey() { 20 | const setShowPendingOverlay = useUpdateAtom(showPendingOverlayAtom); 21 | const address = useCurrentAddress(); 22 | const [contractAddress, contractName] = useHeyContract(); 23 | const { setIsLoading } = useLoading(LOADING_KEYS.CLAIM_HEY); 24 | const { doContractCall } = useConnect(); 25 | const network = useNetwork(); 26 | const onFinish = useCallback(() => { 27 | toast.success('Your like has been submitted!'); 28 | void setIsLoading(false); 29 | void setShowPendingOverlay(false); 30 | }, [setIsLoading, setShowPendingOverlay, toast]); 31 | const onCancel = useCallback(() => { 32 | void setIsLoading(false); 33 | void setShowPendingOverlay(false); 34 | }, [setIsLoading, setShowPendingOverlay]); 35 | 36 | return useCallback( 37 | (id: number) => { 38 | void setShowPendingOverlay(true); 39 | void setIsLoading(true); 40 | 41 | void doContractCall({ 42 | contractAddress, 43 | contractName, 44 | functionName: LIKE_FUNCTION, 45 | functionArgs: [uintCV(id)], 46 | postConditions: [ 47 | createFungiblePostCondition( 48 | address, 49 | FungibleConditionCode.Equal, 50 | new BN(1), 51 | createAssetInfo(contractAddress, 'hey-token', 'hey-token') 52 | ), 53 | ], 54 | onFinish, 55 | onCancel, 56 | network, 57 | stxAddress: address, 58 | }); 59 | }, 60 | [setIsLoading, onFinish, network, onCancel, address, doContractCall] 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/common/hooks/use-loading.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from 'jotai'; 2 | import { loadingAtom } from '@store/ui'; 3 | 4 | export function useLoading(key: string) { 5 | const [isLoading, setIsLoading] = useAtom(loadingAtom(key)); 6 | 7 | return { isLoading, setIsLoading }; 8 | } 9 | -------------------------------------------------------------------------------- /src/common/hooks/use-network.ts: -------------------------------------------------------------------------------- 1 | import { useAtomValue } from 'jotai/utils'; 2 | import { networkAtom } from '@store/ui'; 3 | 4 | export function useNetwork() { 5 | return useAtomValue(networkAtom); 6 | } 7 | -------------------------------------------------------------------------------- /src/common/hooks/use-publish-hey.ts: -------------------------------------------------------------------------------- 1 | import { useLoading } from '@hooks/use-loading'; 2 | import { LOADING_KEYS, showPendingOverlayAtom } from '@store/ui'; 3 | import { useConnect } from '@stacks/connect-react'; 4 | import { useNetwork } from '@hooks/use-network'; 5 | import { useCallback } from 'react'; 6 | import { 7 | createAssetInfo, 8 | createFungiblePostCondition, 9 | FungibleConditionCode, 10 | noneCV, 11 | someCV, 12 | stringUtf8CV, 13 | } from '@stacks/transactions'; 14 | import { useAtomValue, useUpdateAtom } from 'jotai/utils'; 15 | import { useCurrentAddress } from '@hooks/use-current-address'; 16 | import { useHeyContract } from '@hooks/use-hey-contract'; 17 | import { MESSAGE_FUNCTION } from '@common/constants'; 18 | import BN from 'bn.js'; 19 | import { attachmentUriAtom } from '@store/feed'; 20 | 21 | export function useHandlePublishContent() { 22 | const setShowPendingOverlay = useUpdateAtom(showPendingOverlayAtom); 23 | const attachmentUri = useAtomValue(attachmentUriAtom); 24 | const address = useCurrentAddress(); 25 | const [contractAddress, contractName] = useHeyContract(); 26 | const { setIsLoading } = useLoading(LOADING_KEYS.CLAIM_HEY); 27 | const { doContractCall } = useConnect(); 28 | const network = useNetwork(); 29 | const onFinish = useCallback(() => { 30 | void setIsLoading(false); 31 | void setShowPendingOverlay(false); 32 | }, [setIsLoading, setShowPendingOverlay]); 33 | const onCancel = useCallback(() => { 34 | void setIsLoading(false); 35 | void setShowPendingOverlay(false); 36 | }, [setIsLoading, setShowPendingOverlay]); 37 | 38 | return useCallback( 39 | (content: string, _onFinish: () => void) => { 40 | void setShowPendingOverlay(true); 41 | void setIsLoading(true); 42 | 43 | void doContractCall({ 44 | contractAddress, 45 | contractName, 46 | functionName: MESSAGE_FUNCTION, 47 | functionArgs: [ 48 | stringUtf8CV(content), 49 | attachmentUri !== '' ? someCV(stringUtf8CV(attachmentUri)) : noneCV(), 50 | ], 51 | onFinish: () => { 52 | _onFinish(); 53 | onFinish(); 54 | }, 55 | postConditions: [ 56 | createFungiblePostCondition( 57 | address, 58 | FungibleConditionCode.Equal, 59 | new BN(1), 60 | createAssetInfo(contractAddress, 'hey-token', 'hey-token') 61 | ), 62 | ], 63 | onCancel, 64 | network, 65 | stxAddress: address, 66 | }); 67 | }, 68 | [setIsLoading, onFinish, network, onCancel, address, doContractCall] 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/common/hooks/use-scroll-to-bottom.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export function useScrollToBottom(enabled: boolean) { 4 | const ref = useRef(null); 5 | useEffect(() => { 6 | if (enabled) setTimeout(() => ref.current?.scrollIntoView(), 100); 7 | }); 8 | return ref; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/hooks/use-show-welcome.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from 'jotai'; 2 | import { showAboutAtom } from '@store/ui'; 3 | 4 | export function useShowWelcome() { 5 | const [isShowing, setIsShowing] = useAtom(showAboutAtom); 6 | const toggleIsShowing = () => setIsShowing(s => !s); 7 | return { 8 | isShowing, 9 | toggleIsShowing, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/common/hooks/use-user.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from 'jotai'; 2 | import { userAtom } from '@store/auth'; 3 | import { UserData } from '@stacks/auth'; 4 | 5 | export function useUser() { 6 | const [user, setUser] = useAtom(userAtom); 7 | return { 8 | user, 9 | profile: user?.profile, 10 | addresses: user?.profile?.stxAddress, 11 | setUser, 12 | isSignedIn: !!user, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/common/hooks/use-usersession.ts: -------------------------------------------------------------------------------- 1 | import { useAtomValue } from 'jotai/utils'; 2 | import { userSessionAtom } from '@store/auth'; 3 | 4 | export function useUserSession() { 5 | return useAtomValue(userSessionAtom); 6 | } 7 | -------------------------------------------------------------------------------- /src/common/time.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import relativeTime from 'dayjs/plugin/relativeTime'; 3 | 4 | dayjs.extend(relativeTime); 5 | 6 | export function toRelativeTime(ts: number) { 7 | return dayjs(ts).fromNow(); 8 | } 9 | -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import { color, ColorsStringLiteral } from '@stacks/ui'; 2 | import { Property } from 'csstype'; 3 | 4 | export const border = ( 5 | _color: ColorsStringLiteral = 'border', 6 | width = 1, 7 | style: Property.BorderStyle = 'solid' 8 | ): string => `${width}px ${style as string} ${color(_color)}`; 9 | -------------------------------------------------------------------------------- /src/components/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { color, ColorModeProvider, Flex, Spinner, ThemeProvider } from '@stacks/ui'; 3 | import { Connect } from '@stacks/connect-react'; 4 | import { Header } from '@components/header'; 5 | import { WelcomePanel } from '@components/welcome-panel'; 6 | import { Feed } from '@components/feed'; 7 | import { useAuthOptions } from '@hooks/use-auth-options'; 8 | import 'modern-normalize/modern-normalize.css'; 9 | import { PendingOverlay } from '@components/pending-overlay'; 10 | import { Toaster } from 'react-hot-toast'; 11 | 12 | const AppWrapper: React.FC = memo(({ children }) => { 13 | const authOptions = useAuthOptions(); 14 | return ( 15 | 16 | 17 | 18 | {children} 19 | 20 | 21 | ); 22 | }); 23 | 24 | const App: React.FC = memo(() => { 25 | return ( 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 42 | 43 | 44 | } 45 | > 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | }); 54 | 55 | export default App; 56 | -------------------------------------------------------------------------------- /src/components/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps } from '@stacks/ui'; 2 | import BoringAvatar from 'boring-avatars'; 3 | import React from 'react'; 4 | 5 | export const Avatar = (props: { name: string } & BoxProps) => ( 6 | 21 | ); 22 | -------------------------------------------------------------------------------- /src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button as _Button, ButtonProps } from '@stacks/ui'; 3 | 4 | export const Button = (props: ButtonProps) => ( 5 | <_Button 6 | css={{ 7 | '& > div > div': { 8 | borderStyle: 'solid', 9 | }, 10 | }} 11 | borderRadius="10px" 12 | {...props} 13 | /> 14 | ); 15 | -------------------------------------------------------------------------------- /src/components/claim-hey-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@components/button'; 2 | import React from 'react'; 3 | import { useLoading } from '@hooks/use-loading'; 4 | import { LOADING_KEYS } from '@store/ui'; 5 | import { useHandleClaimHey } from '@hooks/use-claim-hey'; 6 | import { ButtonProps } from '@stacks/ui'; 7 | 8 | export const ClaimHeyButton = (props: ButtonProps) => { 9 | const { isLoading } = useLoading(LOADING_KEYS.CLAIM_HEY); 10 | const handleFaucetCall = useHandleClaimHey(); 11 | return ( 12 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/compose.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useRef } from 'react'; 2 | import Textarea from 'react-textarea-autosize'; 3 | import { Fade, Box, Stack, Flex, color, transition } from '@stacks/ui'; 4 | import { border } from '@common/utils'; 5 | import { Button } from '@components/button'; 6 | import { ConnectWalletButton } from '@components/connect-wallet-button'; 7 | import { ClaimHeyButton } from '@components/claim-hey-button'; 8 | import { useComposeField } from '@hooks/use-compose-field'; 9 | import { useToggle } from '@hooks/use-boolean'; 10 | import { GiphyResultsCard } from '@components/giphy'; 11 | import { useAttachment } from '@hooks/use-attachment'; 12 | 13 | const ComposeField = memo(() => { 14 | const { onChange, value, onSubmit, isSignedIn, isLoading, hasNoBalance } = useComposeField(); 15 | const { toggle, handleToggle } = useToggle('GIF_RESULTS'); 16 | const { hasAttachment } = useAttachment(); 17 | const ref = useRef(null); 18 | return ( 19 | { 27 | e?.preventDefault(); 28 | onSubmit(); 29 | }} 30 | mx="auto" 31 | position="relative" 32 | width="600px" 33 | > 34 | { 46 | if (event.key === 'Enter') { 47 | event.preventDefault(); 48 | onSubmit(); 49 | } 50 | }} 51 | /> 52 | 53 | 54 | {styles => 55 | isSignedIn ? ( 56 | hasNoBalance ? ( 57 | Claim hey 58 | ) : ( 59 | 62 | ) 63 | ) : ( 64 | 65 | ) 66 | } 67 | 68 | { 77 | void handleToggle(); 78 | if (!toggle) { 79 | setTimeout(() => { 80 | ref.current?.focus(); 81 | ref.current?.select(); 82 | }, 100); 83 | } 84 | }} 85 | transition={transition} 86 | _hover={{ cursor: 'pointer' }} 87 | > 88 | {toggle ? 'HIDE' : `GIF${hasAttachment ? ' (1)' : ''}`} 89 | 90 | 91 | 92 | 93 | ); 94 | }); 95 | 96 | const ComposeLoading = memo(() => ( 97 | 108 | 117 | 118 | )); 119 | export const Compose = memo(() => ( 120 | }> 121 | 122 | 123 | )); 124 | -------------------------------------------------------------------------------- /src/components/connect-wallet-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@components/button'; 2 | import React from 'react'; 3 | import { useConnect } from '@stacks/connect-react'; 4 | import { ButtonProps } from '@stacks/ui'; 5 | import { useLoading } from '@hooks/use-loading'; 6 | import { LOADING_KEYS } from '@store/ui'; 7 | 8 | export const ConnectWalletButton: React.FC = props => { 9 | const { doOpenAuth } = useConnect(); 10 | const { isLoading, setIsLoading } = useLoading(LOADING_KEYS.AUTH); 11 | return ( 12 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/feed-item.tsx: -------------------------------------------------------------------------------- 1 | import { Heystack } from '@store/hey'; 2 | import { convertAddress } from '@common/addresses'; 3 | import { useAtomValue } from 'jotai/utils'; 4 | import { namesAtom } from '@store/names'; 5 | import { Caption, Text } from '@components/typography'; 6 | import { truncateMiddle } from '@stacks/ui-utils'; 7 | import React, { memo, useEffect, useRef } from 'react'; 8 | import { Box, color, Flex, Stack } from '@stacks/ui'; 9 | import { border } from '@common/utils'; 10 | import { useGetItemLikes } from '@hooks/use-get-item-likes'; 11 | import { useHandleLikeHey } from '@hooks/use-like-hey'; 12 | import { useUser } from '@hooks/use-user'; 13 | import { toRelativeTime } from '@common/time'; 14 | import { FiArrowUpCircle } from 'react-icons/all'; 15 | import { motion } from 'framer-motion'; 16 | import { Avatar } from '@components/avatar'; 17 | import { useScrollToBottom } from '@hooks/use-scroll-to-bottom'; 18 | 19 | const Address = ({ item }: { item: Heystack }) => { 20 | const address = convertAddress(item.sender, 'mainnet'); 21 | const names = useAtomValue(namesAtom(address)); 22 | return {names?.[0] || truncateMiddle(item.sender)}; 23 | }; 24 | 25 | const Message = memo(({ isUser, item }: { isUser: boolean; item: Heystack }) => { 26 | return ( 27 | 34 | 35 | {!isUser && ( 36 | 37 | {truncateMiddle(item.sender)}}> 38 |
39 | 40 | 41 | )} 42 | {item.content} 43 | {item.attachment ? ( 44 | 45 | ) : null} 46 | 47 | 48 | ); 49 | }); 50 | 51 | const ItemLikes = ({ index }: { index: number }) => { 52 | const likes = useGetItemLikes(index); 53 | return {likes}; 54 | }; 55 | const ItemDetailsRow = memo(({ isUser, item }: { isUser: boolean; item: Heystack }) => { 56 | const handleLikeHey = useHandleLikeHey(); 57 | const { isSignedIn } = useUser(); 58 | 59 | return ( 60 | 61 | 62 | {item.isPending && Pending} 63 | {toRelativeTime(item.timestamp * 1000)} 64 | {item.index ? ( 65 | handleLikeHey(item.index as number)} 67 | alignItems="center" 68 | _hover={ 69 | !isSignedIn || isUser ? undefined : { cursor: 'pointer', color: color('brand') } 70 | } 71 | isInline 72 | color={color('text-caption')} 73 | > 74 | 75 | {!item.isPending && item.index && ( 76 | ...}> 77 | 78 | 79 | )} 80 | 81 | ) : null} 82 | 83 | 84 | ); 85 | }); 86 | 87 | export const FeedItemComponent = ({ 88 | isUser, 89 | item, 90 | isLast, 91 | ...rest 92 | }: { 93 | isLast: boolean; 94 | isUser: boolean; 95 | item: Heystack; 96 | }) => { 97 | const ref = useScrollToBottom(isLast); 98 | return ( 99 | 113 | 114 | 122 | 123 | 124 | 125 | 126 | ); 127 | }; 128 | -------------------------------------------------------------------------------- /src/components/feed.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Box, Flex } from '@stacks/ui'; 3 | import { AnimatePresence } from 'framer-motion'; 4 | import { Compose } from '@components/compose'; 5 | import { useFeed } from '@hooks/use-feed'; 6 | import { FeedItemComponent } from '@components/feed-item'; 7 | import { useCurrentAddress } from '@hooks/use-current-address'; 8 | 9 | const FeedList = memo(() => { 10 | const feed = useFeed(); 11 | const address = useCurrentAddress(); 12 | return ( 13 | 14 | {feed.map((item, key) => { 15 | const isUser = item.sender === address; 16 | return ( 17 | 23 | ); 24 | })} 25 | 26 | ); 27 | }); 28 | 29 | export const Feed = () => { 30 | return ( 31 | 32 | 46 | 47 | 48 | 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/giphy.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { useAtomValue, useUpdateAtom } from 'jotai/utils'; 3 | import { gihpyResultsAtom } from '@store/giphy'; 4 | import { attachmentUriAtom } from '@store/feed'; 5 | import { useToggle } from '@hooks/use-boolean'; 6 | import { Box, color, Fade, Flex, Grid, IconButton, Spinner, Stack } from '@stacks/ui'; 7 | import { Caption, Title } from '@components/typography'; 8 | import { border } from '@common/utils'; 9 | import { FiX } from 'react-icons/fi'; 10 | 11 | const GiphyGrid = memo(() => { 12 | const results = useAtomValue(gihpyResultsAtom); 13 | const setAttachment = useUpdateAtom(attachmentUriAtom); 14 | const { setToggle } = useToggle('GIF_RESULTS'); 15 | return ( 16 | 23 | {results?.length ? ( 24 | results?.map((item: any) => ( 25 | { 30 | void setToggle(false); 31 | void setAttachment(item.images.downsized_medium.url); 32 | }} 33 | src={item.images.downsized.url} 34 | /> 35 | )) 36 | ) : ( 37 | 45 | No results, try a different query 46 | 47 | )} 48 | 49 | ); 50 | }); 51 | 52 | export const GiphyResultsCard = memo(() => { 53 | const { toggle, handleToggle } = useToggle('GIF_RESULTS'); 54 | return ( 55 | 56 | {styles => ( 57 | 67 | 78 | 79 | Giphy search 80 | 81 | 82 | 85 | 86 | 87 | } 88 | > 89 | 90 | 91 | 92 | 93 | )} 94 | 95 | ); 96 | }); 97 | -------------------------------------------------------------------------------- /src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Box, Flex, Stack, StackProps } from '@stacks/ui'; 3 | import { Logo } from '@components/logo'; 4 | import { UserArea } from '@components/user-area'; 5 | 6 | export const Header = memo((props: StackProps) => ( 7 | <> 8 | 9 | 10 | 🐴 11 | 12 | 13 | 14 | 15 | 16 | )); 17 | -------------------------------------------------------------------------------- /src/components/link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, Box, BoxProps } from '@stacks/ui'; 3 | 4 | export const buildEnterKeyEvent = (onClick: () => void) => { 5 | return (event: React.KeyboardEvent) => { 6 | if (event.key === 'Enter' && onClick) { 7 | onClick(); 8 | } 9 | }; 10 | }; 11 | 12 | export const Link: React.FC = ({ 13 | _hover = {}, 14 | children, 15 | fontSize = '12px', 16 | textStyle = 'caption.medium', 17 | onClick, 18 | ...rest 19 | }) => ( 20 | 21 | 26 | {children} 27 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /src/components/logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, BoxProps } from '@stacks/ui'; 3 | 4 | export const Logo: React.FC = (props: BoxProps) => { 5 | return ( 6 | 7 | 11 | 15 | 19 | 23 | 27 | 31 | 35 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/pending-overlay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { color, Fade, Flex, Spinner, Slide, Stack, Box } from '@stacks/ui'; 3 | import { showPendingOverlayAtom } from '@store/ui'; 4 | import { Caption } from '@components/typography'; 5 | import { useAtom } from 'jotai'; 6 | 7 | export const PendingOverlay = () => { 8 | const [isShowing, setIsShowing] = useAtom(showPendingOverlayAtom); 9 | return ( 10 | 11 | {style => ( 12 | setIsShowing(false)} 24 | > 25 | 26 | {slideStyles => ( 27 | 37 | 46 | Confirm the transaction in your wallet 47 | 53 | 54 | 55 | )} 56 | 57 | 58 | )} 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/typography.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import capsize from 'capsize'; 3 | 4 | import { color, Text as BaseText } from '@stacks/ui'; 5 | import type { BoxProps } from '@stacks/ui'; 6 | import { forwardRefWithAs } from '@stacks/ui-core'; 7 | 8 | const interMetrics = { 9 | capHeight: 2048, 10 | ascent: 2728, 11 | descent: -680, 12 | lineGap: 0, 13 | unitsPerEm: 2816, 14 | }; 15 | const openSauceMetrics = { 16 | capHeight: 1563, 17 | ascent: 2105, 18 | descent: -525, 19 | lineGap: 0, 20 | unitsPerEm: 2048, 21 | }; 22 | 23 | const h1 = capsize({ 24 | fontMetrics: openSauceMetrics, 25 | fontSize: 24, 26 | leading: 32, 27 | }); 28 | 29 | // B2 30 | const h2 = capsize({ 31 | fontMetrics: openSauceMetrics, 32 | fontSize: 18, 33 | leading: 28, 34 | }); 35 | // B3 36 | const h3 = capsize({ 37 | fontMetrics: openSauceMetrics, 38 | fontSize: 16, 39 | leading: 24, 40 | }); 41 | // C1 42 | const h4 = capsize({ 43 | fontMetrics: openSauceMetrics, 44 | fontSize: 14, 45 | leading: 20, 46 | }); 47 | // C2 48 | const h5 = capsize({ 49 | fontMetrics: openSauceMetrics, 50 | fontSize: 12, 51 | leading: 16, 52 | }); 53 | 54 | const c1 = capsize({ 55 | fontMetrics: interMetrics, 56 | fontSize: 14, 57 | leading: 20, 58 | }); 59 | const c2 = capsize({ 60 | fontMetrics: interMetrics, 61 | fontSize: 12, 62 | leading: 16, 63 | }); 64 | const body = capsize({ 65 | fontMetrics: interMetrics, 66 | fontSize: 17, 67 | leading: 28, 68 | }); 69 | 70 | const captionStyles = (variant?: 'c1' | 'c2') => { 71 | switch (variant) { 72 | case 'c2': 73 | return c2; 74 | default: 75 | return c1; 76 | } 77 | }; 78 | 79 | type Headings = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span'; 80 | 81 | export const titleStyles = (as: Headings) => { 82 | switch (as) { 83 | case 'h1': 84 | return h1; 85 | case 'h2': 86 | return h2; 87 | case 'h3': 88 | return h3; 89 | case 'h4': 90 | return h4; 91 | case 'h5': 92 | case 'h6': 93 | return h5; 94 | default: 95 | return undefined; 96 | } 97 | }; 98 | 99 | export const Title = forwardRefWithAs((props, ref) => ( 100 | 111 | )); 112 | 113 | export const Text = forwardRefWithAs((props, ref) => ( 114 | 123 | )); 124 | 125 | export const Body: React.FC = props => ; 126 | 127 | export const Caption = forwardRefWithAs<{ variant?: 'c1' | 'c2' } & BoxProps, 'span'>( 128 | ({ variant, ...props }, ref) => ( 129 | 138 | ) 139 | ); 140 | -------------------------------------------------------------------------------- /src/components/user-area.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useState } from 'react'; 2 | import { useAtom } from 'jotai'; 3 | import { FiChevronDown, FiLogOut } from 'react-icons/fi'; 4 | import { Box, BoxProps, color, Fade, Flex, Stack } from '@stacks/ui'; 5 | import { userAtom } from '@store/auth'; 6 | import { Link } from '@components/link'; 7 | import { useHover } from '@common/hooks/use-hover'; 8 | import { Caption, Text } from '@components/typography'; 9 | import { border } from '@common/utils'; 10 | import { useUser } from '@hooks/use-user'; 11 | import { truncateMiddle } from '@stacks/ui-utils'; 12 | import { useUserSession } from '@hooks/use-usersession'; 13 | import { ConnectWalletButton } from '@components/connect-wallet-button'; 14 | import { useHeyBalance } from '@hooks/use-hey-balance'; 15 | import { useCurrentAddress, useCurrentMainnetAddress } from '@hooks/use-current-address'; 16 | import { UserAvatar } from '@components/user-avatar'; 17 | import { useAccountNames } from '@common/hooks/use-account-names'; 18 | 19 | const Dropdown: React.FC void; show?: boolean }> = memo( 20 | ({ onSignOut, show }) => { 21 | return ( 22 | 23 | {styles => ( 24 | 25 | { 27 | onSignOut?.(); 28 | }} 29 | isInline 30 | _hover={{ bg: color('bg-alt') }} 31 | alignItems="center" 32 | border={border()} 33 | overflow="hidden" 34 | boxShadow="mid" 35 | minHeight="60px" 36 | minWidth="212px" 37 | bg={color('bg')} 38 | borderRadius="12px" 39 | p="base" 40 | position="relative" 41 | zIndex={9999999999} 42 | > 43 | 44 | 52 | Disconnect 53 | 54 | 55 | 56 | )} 57 | 58 | ); 59 | } 60 | ); 61 | 62 | const AccountNameComponent = memo(() => { 63 | const { user } = useUser(); 64 | // Temporarily getting names from mainnet 65 | const address = useCurrentMainnetAddress(); 66 | const testnetAddress = useCurrentAddress(); 67 | const names = useAccountNames(address); 68 | const name = names?.[0]; 69 | return {name || user?.username || truncateMiddle(testnetAddress)}; 70 | }); 71 | 72 | const BalanceComponent = memo(() => { 73 | const balance = useHeyBalance(); 74 | return {balance || 0} HEY; 75 | }); 76 | 77 | const Menu: React.FC = memo(() => { 78 | const { setUser } = useUser(); 79 | const userSession = useUserSession(); 80 | const [isHovered, setIsHovered] = useState(false); 81 | const bind = useHover(setIsHovered); 82 | const handleRemoveHover = useCallback(() => setIsHovered(false), [setIsHovered]); 83 | const testnetAddress = useCurrentAddress(); 84 | 85 | const handleSignOut = useCallback(() => { 86 | handleRemoveHover(); 87 | userSession.signUserOut(); 88 | void setUser(undefined); 89 | }, [userSession, setUser, handleRemoveHover]); 90 | 91 | return ( 92 | 99 | 100 | 101 | 102 | {truncateMiddle(testnetAddress)}}> 103 | 104 | 105 | 106 | -- HEY}> 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | ); 116 | }); 117 | 118 | export const UserArea: React.FC = memo(() => { 119 | const [user] = useAtom(userAtom); 120 | 121 | return ( 122 | 123 | {user ? : } 124 | 125 | ); 126 | }); 127 | -------------------------------------------------------------------------------- /src/components/user-avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BoxProps } from '@stacks/ui'; 3 | import { Avatar } from '@components/avatar'; 4 | import { useCurrentAddress } from '@hooks/use-current-address'; 5 | 6 | export const UserAvatar = (props: BoxProps) => { 7 | const address = useCurrentAddress(); 8 | return ; 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/welcome-panel.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Box, Fade, IconButton, Stack, StackProps } from '@stacks/ui'; 3 | import { Button } from '@components/button'; 4 | import { border } from '@common/utils'; 5 | import { Caption, Text, Title } from '@components/typography'; 6 | import { ConnectGraphic } from '@components/connect-graphic'; 7 | import { FiChevronDown, FiX } from 'react-icons/fi'; 8 | import { useUser } from '@hooks/use-user'; 9 | import { ConnectWalletButton } from '@components/connect-wallet-button'; 10 | import { useLoading } from '@hooks/use-loading'; 11 | import { LOADING_KEYS } from '@store/ui'; 12 | import { useHandleClaimHey } from '@hooks/use-claim-hey'; 13 | import { UserAvatar } from '@components/user-avatar'; 14 | import { useShowWelcome } from '@hooks/use-show-welcome'; 15 | import { ClaimHeyButton } from '@components/claim-hey-button'; 16 | import { useHeyBalance } from '@hooks/use-hey-balance'; 17 | 18 | const HiddenTitle = memo(() => { 19 | const { toggleIsShowing } = useShowWelcome(); 20 | 21 | return ( 22 | 23 | 24 | 33 | 34 | ); 35 | }); 36 | const AboutSection = memo((props: StackProps) => { 37 | const { toggleIsShowing } = useShowWelcome(); 38 | return ( 39 | 40 | 41 | 42 | 51 | 52 | 53 | Heystack is a decentralized chat app built on Stacks. 💬 Spend HEY to chat and upvote, 💸 54 | receive HEY when your chats are upvoted. 55 | 56 | 57 | ); 58 | }); 59 | 60 | const SignedOutView: React.FC = ({ ...props }) => { 61 | return ( 62 | 70 | 71 | 72 | 73 | 74 | 75 | Connect your wallet to get 100 HEY and start chatting 76 | 77 | 78 | 79 | ); 80 | }; 81 | const SignedInView: React.FC = ({ ...props }) => { 82 | const balance = useHeyBalance(); 83 | if (balance !== '0') return null; 84 | return ( 85 | 93 | 94 | 95 | Welcome to Heystack! Claim your 100 HEY to start chatting{' '} 96 | 97 | 98 | 99 | ); 100 | }; 101 | 102 | const UserSection = memo((props: StackProps) => { 103 | const { user } = useUser(); // something like this 104 | 105 | return ( 106 | 114 | }> 115 | {!user ? : } 116 | 117 | 118 | ); 119 | }); 120 | 121 | const LearnMoreSection = memo((props: StackProps) => { 122 | return ( 123 | 124 | 125 | Learn how to build your own decentralized app on Stacks by building Heystack 126 | 127 | 128 | 129 | ); 130 | }); 131 | export const WelcomePanel = memo(props => { 132 | const { isShowing } = useShowWelcome(); 133 | return ( 134 | <> 135 | 143 | 144 | About Heystack 145 | 146 | 147 | 148 | {styles => ( 149 | 159 | 160 | 161 | 162 | 163 | )} 164 | 165 | 166 | {styles => ( 167 | 177 | 178 | 179 | )} 180 | 181 | 182 | ); 183 | }); 184 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Inter", -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: "Fira Code", source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Open Sauce One'; 17 | src: url('./common/fonts/open-sauce-one/opensauceone-bold-webfont.woff2') format('woff2'), 18 | url('./common/fonts/open-sauce-one/opensauceone-bold-webfont.woff') format('woff'); 19 | font-weight: bold; 20 | font-style: normal; 21 | } 22 | 23 | @font-face { 24 | font-family: 'Open Sauce One'; 25 | src: url('./common/fonts/open-sauce-one/opensauceone-bolditalic-webfont.woff2') format('woff2'), 26 | url('./common/fonts/open-sauce-one/opensauceone-bolditalic-webfont.woff') format('woff'); 27 | font-weight: bold; 28 | font-style: italic; 29 | } 30 | 31 | @font-face { 32 | font-family: 'Open Sauce One'; 33 | src: url('./common/fonts/open-sauce-one/opensauceone-italic-webfont.woff2') format('woff2'), 34 | url('./common/fonts/open-sauce-one/opensauceone-italic-webfont.woff') format('woff'); 35 | font-weight: normal; 36 | font-style: italic; 37 | } 38 | 39 | @font-face { 40 | font-family: 'Open Sauce One'; 41 | src: url('./common/fonts/open-sauce-one/opensauceone-regular-webfont.woff2') format('woff2'), 42 | url('./common/fonts/open-sauce-one/opensauceone-regular-webfont.woff') format('woff'); 43 | font-weight: normal; 44 | font-style: normal; 45 | } 46 | 47 | @font-face { 48 | font-family: 'Open Sauce One'; 49 | src: url('./common/fonts/open-sauce-one/opensauceone-semibold-webfont.woff2') format('woff2'), 50 | url('./common/fonts/open-sauce-one/opensauceone-semibold-webfont.woff') format('woff'); 51 | font-weight: 500; 52 | font-style: normal; 53 | } 54 | 55 | @font-face { 56 | font-family: 'Open Sauce One'; 57 | src: url('./common/fonts/open-sauce-one/opensauceone-semibolditalic-webfont.woff2') format('woff2'), 58 | url('./common/fonts/open-sauce-one/opensauceone-semibolditalic-webfont.woff') format('woff'); 59 | font-weight: 500; 60 | font-style: italic; 61 | } 62 | 63 | @font-face { 64 | font-family: 'Inter'; 65 | font-style: normal; 66 | font-weight: 400; 67 | font-display: swap; 68 | src: url("./common/fonts/inter/Inter-Regular.woff2") format("woff2"), 69 | url("./common/fonts/inter/Inter-Regular.woff") format("woff"); 70 | } 71 | 72 | @font-face { 73 | font-family: 'Inter'; 74 | font-style: italic; 75 | font-weight: 400; 76 | font-display: swap; 77 | src: url("./common/fonts/inter/Inter-Italic.woff2") format("woff2"), 78 | url("./common/fonts/inter/Inter-Italic.woff") format("woff"); 79 | } 80 | 81 | @font-face { 82 | font-family: 'Inter'; 83 | font-style: normal; 84 | font-weight: 500; 85 | font-display: swap; 86 | src: url("./common/fonts/inter/Inter-Medium.woff2") format("woff2"), 87 | url("./common/fonts/inter/Inter-Medium.woff") format("woff"); 88 | } 89 | 90 | @font-face { 91 | font-family: 'Inter'; 92 | font-style: italic; 93 | font-weight: 500; 94 | font-display: swap; 95 | src: url("./common/fonts/inter/Inter-MediumItalic.woff2") format("woff2"), 96 | url("./common/fonts/inter/Inter-MediumItalic.woff") format("woff"); 97 | } 98 | 99 | 100 | @font-face { 101 | font-family: 'Fira Code'; 102 | src: url('./common/fonts/fira-code/woff2/FiraCode-Light.woff2') format('woff2'), 103 | url("./common/fonts/fira-code/woff/FiraCode-Light.woff") format("woff"); 104 | font-weight: 300; 105 | font-style: normal; 106 | } 107 | 108 | @font-face { 109 | font-family: 'Fira Code'; 110 | src: url('./common/fonts/fira-code/woff2/FiraCode-Regular.woff2') format('woff2'), 111 | url("./common/fonts/fira-code/woff/FiraCode-Regular.woff") format("woff"); 112 | font-weight: 400; 113 | font-style: normal; 114 | } 115 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'jotai'; 4 | import './index.css'; 5 | import App from './components/app'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('app') 12 | ); 13 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/store/api.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { 3 | AccountsApi, 4 | Configuration, 5 | SmartContractsApi, 6 | TransactionsApi, 7 | } from '@stacks/blockchain-api-client'; 8 | import { networkAtom } from '@store/ui'; 9 | 10 | const configAtom = atom(get => { 11 | const network = get(networkAtom); 12 | return new Configuration({ basePath: network.coreApiUrl }); 13 | }); 14 | 15 | export const smartContractsClientAtom = atom(get => { 16 | const config = get(configAtom); 17 | return new SmartContractsApi(config); 18 | }); 19 | 20 | export const accountsClientAtom = atom(get => { 21 | const config = get(configAtom); 22 | return new AccountsApi(config); 23 | }); 24 | export const transactionsClientAtom = atom(get => { 25 | const config = get(configAtom); 26 | return new TransactionsApi(config); 27 | }); 28 | -------------------------------------------------------------------------------- /src/store/auth.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { AppConfig, UserSession } from '@stacks/auth'; 3 | import { atomWithDefault } from 'jotai/utils'; 4 | 5 | export const appConfig = new AppConfig(['store_write', 'publish_data'], document.location.href); 6 | export const userSessionAtom = atom(() => new UserSession({ appConfig })); 7 | export const userAtom = atomWithDefault(get => { 8 | const userSession = get(userSessionAtom); 9 | if (userSession.isUserSignedIn()) { 10 | return userSession.loadUserData(); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /src/store/feed.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { Heystack } from '@store/hey'; 3 | 4 | export const composeHeystackAom = atom(''); 5 | export const attachmentUriAtom = atom(''); 6 | 7 | export const feedItemsAtom = atom>({}); 8 | -------------------------------------------------------------------------------- /src/store/giphy.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { debounce } from 'ts-debounce'; 3 | 4 | const giphySearchUrl = 'https://api.giphy.com/v1/gifs/search?api_key='; 5 | const API_KEY = 'YgPXheG6YuQ9DzWoYsVYooZepqy0ezii'; 6 | 7 | const fetchGiphy = debounce(async (query: string) => { 8 | const url = `${giphySearchUrl}${API_KEY}&q=${query.replace(' ', '+')}`; 9 | const res = await fetch(url); 10 | const results = await res.json(); 11 | return results.data; 12 | }, 500); 13 | 14 | export const gihpyQueryAtom = atom('simpsons'); 15 | 16 | export const gihpyResultsAtom = atom(async get => { 17 | const query = get(gihpyQueryAtom); 18 | if (query === '' || !query) return; 19 | return fetchGiphy(query); 20 | }); 21 | -------------------------------------------------------------------------------- /src/store/hey.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { userAtom } from '@store/auth'; 3 | import { accountsClientAtom, smartContractsClientAtom, transactionsClientAtom } from '@store/api'; 4 | import { HEY_CONTRACT, HEY_TOKEN_ADDRESS, MESSAGE_FUNCTION } from '@common/constants'; 5 | import { principalCV } from '@stacks/transactions/dist/clarity/types/principalCV'; 6 | import { cvToHex, cvToJSON, cvToString, hexToCV, uintCV } from '@stacks/transactions'; 7 | import { 8 | ContractCallTransaction, 9 | MempoolTransactionListResponse, 10 | TransactionResults, 11 | } from '@blockstack/stacks-blockchain-api-types'; 12 | import { atomWithQuery } from 'jotai/query'; 13 | import { atomFamily } from 'jotai/utils'; 14 | 15 | export interface Heystack { 16 | sender: string; 17 | content: string; 18 | attachment?: string; 19 | id: string; 20 | timestamp: number; 21 | isPending?: boolean; 22 | index?: number; 23 | } 24 | 25 | export const incrementAtom = atom(0); 26 | 27 | export const userHeyBalanceAtom = atomFamily((address: string) => 28 | atomWithQuery(get => ({ 29 | queryKey: ['balance', address], 30 | refetchInterval: 5000, 31 | ...(defaultOptions as any), 32 | queryFn: async (): Promise => { 33 | if (!address) return null; 34 | const client = get(smartContractsClientAtom); 35 | const [contractAddress, contractName] = HEY_TOKEN_ADDRESS.split('.'); 36 | const data = await client.callReadOnlyFunction({ 37 | contractAddress, 38 | contractName, 39 | functionName: 'get-balance', 40 | readOnlyFunctionArgs: { 41 | sender: address, 42 | arguments: [cvToHex(principalCV(address || ''))], 43 | }, 44 | }); 45 | if (data.okay && data.result) { 46 | return cvToString(hexToCV(data.result)).replace('(ok u', '').replace(')', ''); 47 | } 48 | return null; 49 | }, 50 | })) 51 | ); 52 | 53 | const defaultOptions = { 54 | refetchOnReconnect: true, 55 | refetchOnWindowFocus: true, 56 | refetchOnMount: true, 57 | keepPreviousData: true, 58 | }; 59 | export const heyTransactionsAtom = atomWithQuery(get => ({ 60 | queryKey: ['hey-txs'], 61 | ...(defaultOptions as any), 62 | refetchInterval: 500, 63 | queryFn: async (): Promise => { 64 | const client = get(accountsClientAtom); 65 | const txClient = get(transactionsClientAtom); 66 | 67 | const txs = await client.getAccountTransactions({ 68 | limit: 50, 69 | principal: HEY_CONTRACT, 70 | }); 71 | const txids = (txs as TransactionResults).results 72 | .filter( 73 | tx => 74 | tx.tx_type === 'contract_call' && 75 | tx.contract_call.function_name === MESSAGE_FUNCTION && 76 | tx.tx_status === 'success' 77 | ) 78 | .map(tx => tx.tx_id); 79 | 80 | const final = await Promise.all(txids.map(async txId => txClient.getTransactionById({ txId }))); 81 | return final as ContractCallTransaction[]; 82 | }, 83 | })); 84 | 85 | export const pendingTxsAtom = atomWithQuery(get => ({ 86 | queryKey: ['hey-pending-txs'], 87 | refetchInterval: 1000, 88 | ...(defaultOptions as any), 89 | queryFn: async (): Promise => { 90 | const client = get(transactionsClientAtom); 91 | 92 | const txs = await client.getMempoolTransactionList({ limit: 96 }); 93 | const heyTxs = (txs as MempoolTransactionListResponse).results 94 | .filter( 95 | tx => 96 | tx.tx_type === 'contract_call' && 97 | tx.contract_call.contract_id === HEY_CONTRACT && 98 | tx.contract_call.function_name === MESSAGE_FUNCTION && 99 | tx.tx_status === 'pending' 100 | ) 101 | .map(tx => tx.tx_id); 102 | 103 | const final = await Promise.all(heyTxs.map(async txId => client.getTransactionById({ txId }))); 104 | 105 | return ( 106 | (final as ContractCallTransaction[]).map(tx => { 107 | const attachment = tx.contract_call.function_args?.[1].repr 108 | .replace(`(some u"`, '') 109 | .slice(0, -1); 110 | 111 | return { 112 | sender: tx.sender_address, 113 | content: tx.contract_call.function_args?.[0].repr 114 | .replace(`u"`, '') 115 | .slice(0, -1) as string, 116 | id: tx.tx_id, 117 | attachment: attachment === 'non' ? undefined : attachment, 118 | timestamp: (tx as any).receipt_time, 119 | isPending: true, 120 | }; 121 | }) || [] 122 | ); 123 | }, 124 | })); 125 | 126 | export const contentTransactionsAtom = atom(get => { 127 | const txs = get(heyTransactionsAtom); 128 | const pending = get(pendingTxsAtom); 129 | const feed = txs.map(tx => { 130 | const content = tx.contract_call.function_args?.[0].repr.replace(`u"`, '').slice(0, -1); 131 | const attachment = tx.contract_call.function_args?.[1].repr 132 | .replace(`(some u"`, '') 133 | .slice(0, -1); 134 | const contractLog = tx.events?.[0] 135 | ? (tx.events?.[0] as any)?.event_type === 'smart_contract_log' && 136 | (tx.events?.[0] as any).contract_log 137 | ? cvToJSON(hexToCV((tx.events?.[0] as any)?.contract_log?.value?.hex)) 138 | : null 139 | : null; 140 | 141 | return { 142 | content, 143 | attachment: attachment === 'non' ? undefined : attachment, 144 | sender: tx.sender_address, 145 | id: tx.tx_id, 146 | index: contractLog?.value?.index?.value, 147 | timestamp: tx.burn_block_time, 148 | }; 149 | }); 150 | const combined = [...pending, ...feed]; 151 | return combined 152 | .filter(item => combined.find(_item => item.id === _item.id)) 153 | .sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1)) as Heystack[]; 154 | }); 155 | 156 | export const itemLikesAtom = atomFamily((index: number) => 157 | atomWithQuery(get => ({ 158 | queryKey: ['likes', index], 159 | refetchInterval: 1000, 160 | ...(defaultOptions as any), 161 | queryFn: async (): Promise => { 162 | const client = get(smartContractsClientAtom); 163 | const [contractAddress, contractName] = HEY_CONTRACT.split('.'); 164 | const data = await client.callReadOnlyFunction({ 165 | contractAddress, 166 | contractName, 167 | functionName: 'get-like-count', 168 | readOnlyFunctionArgs: { 169 | sender: contractAddress, 170 | arguments: [cvToHex(uintCV(index))], 171 | }, 172 | }); 173 | if (data.okay && data.result) { 174 | const result = cvToJSON(hexToCV(data.result)); 175 | return result.value.value.likes.value; 176 | } 177 | return 0; 178 | }, 179 | })) 180 | ); 181 | -------------------------------------------------------------------------------- /src/store/names.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { atomFamily } from 'jotai/utils'; 3 | import { mainnetNetworkAtom } from './ui'; 4 | 5 | const STALE_TIME = 30 * 60 * 1000; 6 | 7 | const makeKey = (networkUrl: string, address: string): string => { 8 | return `${networkUrl}__${address}`; 9 | }; 10 | 11 | interface Param { 12 | networkUrl: string; 13 | address: string; 14 | } 15 | 16 | function getLocalNames(networkUrl: string, address: string): [string[], number] | null { 17 | const key = makeKey(networkUrl, address); 18 | const value = localStorage.getItem(key); 19 | if (!value) return null; 20 | return JSON.parse(value); 21 | } 22 | 23 | function setLocalNames(networkUrl: string, address: string, data: [string[], number]): void { 24 | const key = makeKey(networkUrl, address); 25 | return localStorage.setItem(key, JSON.stringify(data)); 26 | } 27 | 28 | async function fetchNamesByAddress(param: Param) { 29 | const res = await fetch(param.networkUrl + `/v1/addresses/stacks/${param.address}`); 30 | const data = await res.json(); 31 | return data?.names || []; 32 | } 33 | 34 | export const namesAtom = atomFamily((address: string) => 35 | atom(async get => { 36 | if (!address || address === '') return; 37 | // We are temporarily forcing this to look for names on mainnet 38 | const network = get(mainnetNetworkAtom); 39 | if (!network) return null; 40 | 41 | const local = getLocalNames(network.coreApiUrl, address); 42 | 43 | if (local) { 44 | const [names, timestamp] = local; 45 | const now = Date.now(); 46 | const isStale = now - timestamp > STALE_TIME; 47 | if (!isStale) return names; 48 | } 49 | 50 | try { 51 | const names = await fetchNamesByAddress({ 52 | networkUrl: network.coreApiUrl, 53 | address, 54 | }); 55 | if (names?.length) { 56 | setLocalNames(network.coreApiUrl, address, [names, Date.now()]); 57 | } 58 | return names || []; 59 | } catch (e) { 60 | console.error(e); 61 | return []; 62 | } 63 | }) 64 | ); 65 | -------------------------------------------------------------------------------- /src/store/ui.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily, atomWithStorage } from 'jotai/utils'; 2 | import { atom } from 'jotai'; 3 | import { StacksMainnet, StacksTestnet } from '@stacks/network'; 4 | 5 | export enum LOADING_KEYS { 6 | AUTH = 'loading/AUTH', 7 | CLAIM_HEY = 'loading/CLAIM_HEY', 8 | SEND_HEY = 'loading/SEND_HEY', 9 | } 10 | 11 | export const loadingAtom = atomFamily(key => atom(false)); 12 | export const networkAtom = atom(() => { 13 | const _network = new StacksTestnet(); 14 | _network.coreApiUrl = 'https://stacks-node-api.testnet.stacks.co'; 15 | return _network; 16 | }); 17 | // Used temporarily to force getting names from mainnet 18 | export const mainnetNetworkAtom = atom(() => { 19 | const _network = new StacksMainnet(); 20 | _network.coreApiUrl = 'https://stacks-node-api.stacks.co'; 21 | return _network; 22 | }); 23 | export const showPendingOverlayAtom = atom(false); 24 | 25 | export const showAboutAtom = atomWithStorage('show-welcome', true); 26 | export const booleanAtom = atomFamily((key: string) => atom(false)); 27 | -------------------------------------------------------------------------------- /tests/hey-token-client.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, Result, Receipt } from '@blockstack/clarity'; 2 | 3 | const unwrapError = (receipt: Receipt) => { 4 | if (!receipt.error) throw new Error('No error found'); 5 | const error = receipt.error as unknown as Error; 6 | const message = error.message.split('\n')[0]; 7 | return parseInt(message.split('Aborted: u')[1], 10); 8 | }; 9 | 10 | export class TransferError extends Error { 11 | public code: number; 12 | 13 | constructor(code: number) { 14 | super(`Transfer rejected with error code ${code}`); 15 | this.code = code; 16 | } 17 | } 18 | 19 | export class HeyTokenClient extends Client { 20 | constructor(principal: string, provider: Provider) { 21 | super(`${principal}.hey-token`, 'hey-token', provider); 22 | } 23 | 24 | async transfer( 25 | recipient: string, 26 | amount: number, 27 | params: { sender: string; senderArg?: string } 28 | ): Promise { 29 | const { sender, senderArg } = params; 30 | const tx = this.createTransaction({ 31 | method: { 32 | name: 'transfer', 33 | args: [`u${amount}`, `'${senderArg || sender}`, `'${recipient}`, 'none'], 34 | }, 35 | }); 36 | await tx.sign(sender); 37 | const receipt = await this.submitTransaction(tx); 38 | if (receipt.success) { 39 | const result = Result.unwrap(receipt); 40 | return result.startsWith('Transaction executed and committed. Returned: true'); 41 | } 42 | const error = unwrapError(receipt); 43 | throw new TransferError(error); 44 | } 45 | 46 | async balanceOf(owner: string): Promise { 47 | const query = this.createQuery({ 48 | method: { 49 | name: 'get-balance', 50 | args: [`'${owner}`], 51 | }, 52 | }); 53 | const receipt = await this.submitQuery(query); 54 | return Result.unwrapUInt(receipt); 55 | } 56 | 57 | async totalSupply(): Promise { 58 | const query = this.createQuery({ 59 | method: { 60 | name: 'get-total-supply', 61 | args: [], 62 | }, 63 | }); 64 | const receipt = await this.submitQuery(query); 65 | return Result.unwrapUInt(receipt); 66 | } 67 | 68 | async decimals(): Promise { 69 | const query = this.createQuery({ 70 | method: { 71 | name: 'get-decimals', 72 | args: [], 73 | }, 74 | }); 75 | const receipt = await this.submitQuery(query); 76 | return Result.unwrapUInt(receipt); 77 | } 78 | 79 | async symbol(): Promise { 80 | const query = this.createQuery({ 81 | method: { 82 | name: 'get-symbol', 83 | args: [], 84 | }, 85 | }); 86 | const receipt = await this.submitQuery(query); 87 | return Result.unwrap(receipt).split('"')[1]; 88 | } 89 | 90 | async getName() { 91 | const query = this.createQuery({ 92 | method: { 93 | name: 'get-name', 94 | args: [], 95 | }, 96 | }); 97 | const receipt = await this.submitQuery(query); 98 | return Result.unwrap(receipt).split('"')[1]; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/hey-token.test.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, ProviderRegistry } from '@blockstack/clarity'; 2 | import { HeyTokenClient, TransferError } from './hey-token-client'; 3 | 4 | let traitClient: Client; 5 | let provider: Provider; 6 | let heyClient: HeyTokenClient; 7 | 8 | const alice = 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA'; 9 | const bob = 'ST1TWA18TSWGDAFZT377THRQQ451D1MSEM69C761'; 10 | const charlie = 'ST50GEWRE7W5B02G3J3K19GNDDAPC3XPZPYQRQDW'; 11 | 12 | describe('Hey token', () => { 13 | beforeAll(async () => { 14 | provider = await ProviderRegistry.createProvider(); 15 | traitClient = new Client(`${alice}.ft-trait`, 'clarinet/sip-10-ft-standard', provider); 16 | heyClient = new HeyTokenClient(alice, provider); 17 | }); 18 | 19 | test('The contracts are valid', async () => { 20 | await traitClient.checkContract(); 21 | await traitClient.deployContract(); 22 | 23 | await heyClient.checkContract(); 24 | await heyClient.deployContract(); 25 | }); 26 | 27 | describe('Using the contracts', () => { 28 | // We don't set any initial balances 29 | test('Balances after minting are correct', async () => { 30 | expect(await heyClient.balanceOf(alice)).toEqual(10000); 31 | expect(await heyClient.balanceOf(bob)).toEqual(0); 32 | expect(await heyClient.balanceOf(charlie)).toEqual(0); 33 | }); 34 | 35 | // test('Transfering tokens', async () => { 36 | // await heyClient.transfer(bob, 123, { sender: alice }); 37 | // expect(await heyClient.balanceOf(alice)).toEqual(99999999999877); 38 | // expect(await heyClient.balanceOf(bob)).toEqual(100000000000123); 39 | // await heyClient.transfer(charlie, 100, { sender: bob }); 40 | // expect(await heyClient.balanceOf(charlie)).toEqual(12445); 41 | // expect(await heyClient.balanceOf(bob)).toEqual(100000000000023); 42 | // }); 43 | 44 | test('total supply', async () => { 45 | const totalSupply = await heyClient.totalSupply(); 46 | expect(totalSupply).toEqual(80000); 47 | }); 48 | 49 | test('name', async () => { 50 | const name = await heyClient.getName(); 51 | expect(name).toEqual('Heystack Token'); 52 | }); 53 | 54 | test('decimals', async () => { 55 | const decimals = await heyClient.decimals(); 56 | expect(decimals).toEqual(0); 57 | }); 58 | 59 | test('symbol', async () => { 60 | const symbol = await heyClient.symbol(); 61 | expect(symbol).toEqual('HEY'); 62 | }); 63 | 64 | test('Cannot transfer more than your balance', async () => { 65 | const previousBalance = await heyClient.balanceOf(charlie); 66 | await expect(heyClient.transfer(bob, 10000000000, { sender: charlie })).rejects.toThrowError( 67 | new TransferError(1) 68 | ); 69 | expect(await heyClient.balanceOf(charlie)).toEqual(previousBalance); 70 | }); 71 | 72 | test('Cannot invoke transfer with "sender" different than tx-sender', async () => { 73 | await expect( 74 | heyClient.transfer(bob, 10000000000, { 75 | sender: charlie, 76 | senderArg: alice, 77 | }) 78 | ).rejects.toThrowError(new TransferError(4)); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/hey.test.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, ProviderRegistry } from '@blockstack/clarity'; 2 | import { cvToString, uintCV, stringUtf8CV } from '@stacks/transactions'; 3 | import { HeyTokenClient } from './hey-token-client'; 4 | 5 | let traitClient: Client; 6 | let heyClient: Client; 7 | let provider: Provider; 8 | let heyTokenClient: HeyTokenClient; 9 | 10 | const contractDelployer = 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA'; 11 | const bob = 'ST1TWA18TSWGDAFZT377THRQQ451D1MSEM69C761'; 12 | const charles = 'ST50GEWRE7W5B02G3J3K19GNDDAPC3XPZPYQRQDW'; 13 | const derek = 'ST26AE5W55C7MCSH2MRTT22K2555YWJ9BFRCW56YX'; 14 | 15 | describe('Hey token', () => { 16 | beforeEach(async () => { 17 | provider = await ProviderRegistry.createProvider(); 18 | traitClient = new Client( 19 | `${contractDelployer}.ft-trait`, 20 | 'clarinet/sip-10-ft-standard', 21 | provider 22 | ); 23 | heyTokenClient = new HeyTokenClient(contractDelployer, provider); 24 | heyClient = new Client(`${contractDelployer}.hey`, 'hey', provider); 25 | }); 26 | 27 | afterEach(() => provider.close()); 28 | 29 | test('The contracts are valid', async () => { 30 | await traitClient.checkContract(); 31 | await traitClient.deployContract(); 32 | await heyTokenClient.checkContract(); 33 | await heyTokenClient.deployContract(); 34 | await heyClient.checkContract(); 35 | await heyClient.deployContract(); 36 | }); 37 | 38 | describe('Contract functions', () => { 39 | beforeEach(async () => { 40 | await traitClient.deployContract(); 41 | await heyTokenClient.deployContract(); 42 | await heyClient.deployContract(); 43 | }); 44 | 45 | async function getContentIndex() { 46 | const { result } = await heyClient.submitQuery( 47 | heyClient.createQuery({ 48 | method: { name: 'get-content-index', args: [] }, 49 | }) 50 | ); 51 | return result; 52 | } 53 | 54 | async function getLikeCount(contentId: number) { 55 | const { result } = await heyClient.submitQuery( 56 | heyClient.createQuery({ 57 | method: { name: 'get-like-count', args: [cvToString(uintCV(contentId))] }, 58 | }) 59 | ); 60 | return result; 61 | } 62 | 63 | async function sendMessage(from: string, message: string) { 64 | const tx = heyClient.createTransaction({ 65 | method: { name: 'send-message', args: [cvToString(stringUtf8CV(message)), 'none'] }, 66 | }); 67 | tx.sign(from); 68 | return heyClient.submitTransaction(tx); 69 | } 70 | 71 | async function likeMessage(from: string, contentId: number) { 72 | const tx = heyClient.createTransaction({ 73 | method: { name: 'like-message', args: [cvToString(uintCV(contentId))] }, 74 | }); 75 | tx.sign(from); 76 | return heyClient.submitTransaction(tx); 77 | } 78 | 79 | async function requestHey(principal: string) { 80 | const tx = heyClient.createTransaction({ 81 | method: { name: 'request-hey', args: [`'${principal}`] }, 82 | }); 83 | tx.sign(principal); 84 | return heyClient.submitTransaction(tx); 85 | } 86 | 87 | describe('(send-message)', () => { 88 | test('calling with no arguments will fail', async () => { 89 | const query = heyClient.createQuery({ 90 | method: { name: 'send-message', args: [] }, 91 | }); 92 | await expect(heyClient.submitQuery(query)).rejects.toThrow(); 93 | }); 94 | 95 | test('calling with a well-formed arguments', async () => { 96 | await requestHey(bob); 97 | expect(await getContentIndex()).toEqual('(ok u0)'); 98 | const resp = await sendMessage(bob, 'testessage'); 99 | expect(await getContentIndex()).toEqual('(ok u1)'); 100 | expect(resp.success).toBeTruthy(); 101 | }); 102 | 103 | test('principal is saved in a map', async () => { 104 | await requestHey(bob); 105 | await sendMessage(bob, 'wat een mooie bezienswaardigheid'); 106 | const { result } = await heyClient.submitQuery( 107 | heyClient.createQuery({ 108 | method: { name: 'get-message-publisher', args: ['u1'] }, 109 | }) 110 | ); 111 | expect(result).toContain(bob); 112 | }); 113 | }); 114 | 115 | describe('(like-message)', () => { 116 | test('that nothing happens when user has no HEY', async () => { 117 | await sendMessage(bob, `hello everybody, it's me`); 118 | await likeMessage(bob, 0); 119 | }); 120 | 121 | test('it increments likes-state map value', async () => { 122 | expect(await getLikeCount(0)).toEqual('(ok (tuple (likes u0)))'); 123 | await requestHey(charles); 124 | await requestHey(bob); 125 | 126 | const bobsBalance = await heyTokenClient.balanceOf(bob); 127 | const charlesBalance = await heyTokenClient.balanceOf(charles); 128 | 129 | expect(bobsBalance).toEqual(1); 130 | expect(charlesBalance).toEqual(1); 131 | await sendMessage(charles, `Greetings, traveller`); 132 | await likeMessage(bob, 1); 133 | 134 | const bobsBalance2 = await heyTokenClient.balanceOf(bob); 135 | const charlesBalance2 = await heyTokenClient.balanceOf(charles); 136 | expect(bobsBalance2).toEqual(0); 137 | expect(charlesBalance2).toEqual(1); 138 | expect(await getLikeCount(1)).toEqual('(ok (tuple (likes u1)))'); 139 | }); 140 | }); 141 | 142 | describe('(request-hey)', () => { 143 | test('tokens are distributed to principal', async () => { 144 | await requestHey(bob); 145 | await requestHey(bob); 146 | const bobsBalance = await heyTokenClient.balanceOf(bob); 147 | expect(bobsBalance).toEqual(2); 148 | }); 149 | 150 | test('tokens can only be sent to the contract caller', async () => { 151 | const tx = heyClient.createTransaction({ 152 | method: { name: 'request-hey', args: [`'${bob}`] }, 153 | }); 154 | tx.sign(charles); 155 | await heyClient.submitTransaction(tx); 156 | expect(await heyTokenClient.balanceOf(bob)).toEqual(0); 157 | }); 158 | }); 159 | 160 | describe('(transfer-hey)', () => { 161 | test('tokens are transferred', async () => { 162 | await requestHey(bob); 163 | await requestHey(charles); 164 | 165 | const bobsBalance = await heyTokenClient.balanceOf(bob); 166 | const charlesBalance = await heyTokenClient.balanceOf(charles); 167 | 168 | expect(bobsBalance).toEqual(1); 169 | expect(charlesBalance).toEqual(1); 170 | 171 | const tx = heyClient.createTransaction({ 172 | method: { name: 'transfer-hey', args: [`u1`, `'${charles}`] }, 173 | }); 174 | tx.sign(bob); 175 | await heyClient.submitTransaction(tx); 176 | 177 | const bobsBalance2 = await heyTokenClient.balanceOf(bob); 178 | const charlesBalance2 = await heyTokenClient.balanceOf(charles); 179 | expect(bobsBalance2).toEqual(0); 180 | expect(charlesBalance2).toEqual(2); 181 | }); 182 | }); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx", 23 | "downlevelIteration": true 24 | }, 25 | "include": [ 26 | "src" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "paths": { 5 | "@components/*": [ 6 | "components/*" 7 | ], 8 | "@pages/*": [ 9 | "pages/*" 10 | ], 11 | "@common/*": [ 12 | "common/*" 13 | ], 14 | "@store/*": [ 15 | "store/*" 16 | ], 17 | "@hooks/*": [ 18 | "common/hooks/*" 19 | ] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["tests"] 21 | } 22 | --------------------------------------------------------------------------------