├── images
├── icon_16.png
├── icon_32.png
├── icon_48.png
├── icon_128.png
└── cloutmask.svg
├── js
└── options.js
├── vendor
├── tribute
│ ├── tribute.css
│ ├── LICENSE.txt
│ └── tribute.min.js
├── taboverride
│ ├── LICENSE.txt
│ └── taboverride.min.js
├── buffer
│ ├── LICENSE.txt
│ └── buffer.min.js
├── sweetalert2
│ └── LICENSE.txt
└── bootstrap
│ └── LICENSE.txt
├── options.html
├── .github
└── workflows
│ └── release.yml
├── LICENSE
├── README.md
├── manifest.json
├── lib
├── common.js
├── embed.js
├── follow.js
├── cloutmask.js
├── api.js
├── post.js
├── profile.js
└── nft.js
├── background.js
├── main.css
└── main.js
/images/icon_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iPaulPro/BitCloutPlus/HEAD/images/icon_16.png
--------------------------------------------------------------------------------
/images/icon_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iPaulPro/BitCloutPlus/HEAD/images/icon_32.png
--------------------------------------------------------------------------------
/images/icon_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iPaulPro/BitCloutPlus/HEAD/images/icon_48.png
--------------------------------------------------------------------------------
/images/icon_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iPaulPro/BitCloutPlus/HEAD/images/icon_128.png
--------------------------------------------------------------------------------
/js/options.js:
--------------------------------------------------------------------------------
1 | const saveOptions = () => {
2 | const longPostEnabled = document.getElementById('longPostCheck').checked
3 | chrome.storage.local.set({ longPost: longPostEnabled }, () => {
4 | window.close()
5 | })
6 | }
7 |
8 | const restoreOptions = () => {
9 | chrome.storage.local.get(['longPost'], items => {
10 | const enabled = items.longPost === undefined || items.longPost
11 | document.getElementById('longPostCheck').checked = enabled
12 | })
13 | }
14 |
15 | document.addEventListener('DOMContentLoaded', restoreOptions);
16 | document.getElementById('save').addEventListener('click', saveOptions);
--------------------------------------------------------------------------------
/vendor/tribute/tribute.css:
--------------------------------------------------------------------------------
1 | .tribute-container {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | height: auto;
6 | max-height: 300px;
7 | max-width: 500px;
8 | overflow: auto;
9 | display: block;
10 | z-index: 999999;
11 | }
12 | .tribute-container ul {
13 | margin: 0;
14 | margin-top: 2px;
15 | padding: 0;
16 | list-style: none;
17 | background: #efefef;
18 | }
19 | .tribute-container li {
20 | padding: 5px 5px;
21 | cursor: pointer;
22 | }
23 | .tribute-container li.highlight {
24 | background: #ddd;
25 | }
26 | .tribute-container li span {
27 | font-weight: bold;
28 | }
29 | .tribute-container li.no-match {
30 | cursor: default;
31 | }
32 | .tribute-container .menu-highlighted {
33 | font-weight: bold;
34 | }
--------------------------------------------------------------------------------
/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BitClout+ Options
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Settings
14 |
15 |
16 |
17 |
18 | Enable post enhancement
19 |
20 |
21 |
22 | Save settings
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Web Store Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v2
15 |
16 | - name: Create Zip
17 | run: zip -r release.zip . -x ".git/*" ".github/*" ".gitignore" "README.md"
18 |
19 | - name: Create GitHub Release
20 | uses: ncipollo/release-action@v1
21 | with:
22 | artifacts: "release.zip"
23 | token: ${{ secrets.GITHUB_TOKEN }}
24 |
25 | - name: Publish to Web Store
26 | uses: trmcnvn/chrome-addon@v2
27 | with:
28 | extension: ${{ secrets.GOOGLE_EXTENSION_ID }}
29 | zip: release.zip
30 | client-id: ${{ secrets.GOOGLE_CLIENT_ID }}
31 | client-secret: ${{ secrets.GOOGLE_CLIENT_SECRET }}
32 | refresh-token: ${{ secrets.GOOGLE_REFRESH_TOKEN }}
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Paul Burke
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/vendor/taboverride/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Bill Bryant
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/vendor/buffer/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Feross Aboukhadijeh, and other contributors.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/vendor/sweetalert2/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Tristan Edwards & Limon Monte
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BitClout Plus
2 |
3 | [](https://github.com/iPaulPro/BitCloutPlus/actions/workflows/release.yml)
4 |
5 | Chrome / Brave extension to enhance bitclout.com and node.deso.org pages
6 |
7 |
8 |
9 | ## Introduction
10 |
11 | Details can be found at [bitclout.plus](https://bitclout.plus)
12 |
13 | ## Installation
14 |
15 | Install from the [Chrome Web Store](https://get.bitclout.plus), or you may install manually:
16 |
17 | #### Step 1
18 |
19 | Download the latest release from the [releases](https://github.com/iPaulPro/BitCloutPlus/releases) page and unzip.
20 |
21 | #### Step 2
22 |
23 | Enable [developer mode](https://developer.chrome.com/docs/extensions/mv2/faq/#faq-dev-01) in Chrome or Brave.
24 |
25 | #### Step 3
26 |
27 | From the [Extensions](chrome://extensions/) page click "Load unpacked". Select the unzipped project folder.
28 |
29 |
30 | ## Credits
31 |
32 | Created by [paulburke](https://bitclout.com/u/paulburke)
33 |
--------------------------------------------------------------------------------
/vendor/bootstrap/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2011-2021 Twitter, Inc.
4 | Copyright (c) 2011-2021 The Bootstrap Authors
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/vendor/tribute/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017-2020 ZURB, Inc.
4 | Copyright (c) 2014 Jeff Collins
5 | Copyright (c) 2012 Matt York
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in
15 | all copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | THE SOFTWARE.
24 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "BitClout Plus",
3 | "description": "BitClout on steroids",
4 | "version": "0.12.4",
5 | "manifest_version": 3,
6 | "content_security_policy": {
7 | "extension_pages": "script-src 'self'; connect-src https://node.deso.org https://bitclout.com; object-src 'self'; frame-ancestors 'none';"
8 | },
9 | "author": "Paul Burke",
10 | "homepage_url": "https://github.com/iPaulPro/BitCloutPlus",
11 | "short_name": "BitClout+",
12 | "icons": {
13 | "16": "images/icon_16.png",
14 | "32": "images/icon_32.png",
15 | "48": "images/icon_48.png",
16 | "128": "images/icon_128.png"
17 | },
18 | "omnibox": {
19 | "keyword": "deso"
20 | },
21 | "content_scripts": [
22 | {
23 | "matches": [
24 | "https://bitclout.com/*",
25 | "https://node.deso.org/*"
26 | ],
27 | "js": [
28 | "vendor/tribute/tribute.min.js",
29 | "vendor/buffer/buffer.min.js",
30 | "vendor/sweetalert2/sweetalert2.all.min.js",
31 | "vendor/taboverride/taboverride.min.js",
32 | "lib/common.js",
33 | "lib/embed.js",
34 | "lib/api.js",
35 | "lib/nft.js",
36 | "lib/post.js",
37 | "lib/profile.js",
38 | "lib/follow.js",
39 | "lib/cloutmask.js",
40 | "main.js"
41 | ],
42 | "css": [
43 | "vendor/tribute/tribute.css",
44 | "main.css"
45 | ]
46 | }
47 | ],
48 | "web_accessible_resources": [
49 | {
50 | "resources": [
51 | "images/*"
52 | ],
53 | "matches": [
54 | "https://bitclout.com/*",
55 | "https://node.deso.org/*"
56 | ]
57 | }
58 | ],
59 | "background": {
60 | "service_worker": "background.js"
61 | },
62 | "permissions": [
63 | "storage"
64 | ],
65 | "options_ui": {
66 | "page": "options.html",
67 | "open_in_tab": false
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/images/cloutmask.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/lib/common.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) Paul Burke 2021
3 | Github: @ipaulpro/bitcloutplus
4 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
5 | */
6 |
7 | let pendingIdentityMessageId, pendingTransactionHex
8 |
9 | const getLoggedInPublicKey = function () {
10 | const key = window.localStorage.getItem('lastLoggedInUserV2')
11 | if (!key) return undefined
12 |
13 | return JSON.parse(key)
14 | }
15 |
16 | const getLoggedInUsername = function () {
17 | const elementList = document.getElementsByClassName('change-account-selector__acount-name')
18 |
19 | try {
20 | const changeAccountSelector = elementList.item(0)
21 | return changeAccountSelector.innerText.trim()
22 | } catch (e) {}
23 |
24 | return ''
25 | }
26 |
27 | const getUsernameFromUrl = function () {
28 | const segments = new URL(window.location).pathname.split('/')
29 | if (segments[1] === 'u') return segments[2]
30 | return undefined
31 | }
32 |
33 | const getPublicKeyFromPage = () => {
34 | const topCard = document.querySelector('creator-profile-top-card')
35 | if (!topCard) return
36 |
37 | return topCard.querySelector('.creator-profile__ellipsis-restriction').innerText.trim()
38 | }
39 |
40 | const getPostHashHexFromUrl = function () {
41 | const segments = new URL(window.location).pathname.split('/')
42 | if (segments[1] === 'posts' || segments[1] === 'nft') return segments[2]
43 | return undefined
44 | }
45 |
46 | const getSpotPrice = function () {
47 | const balanceBox = document.getElementsByClassName('right-bar-creators__balance-box').item(0)
48 |
49 | try {
50 | const priceContainerDiv = balanceBox.firstElementChild
51 | const priceDiv = priceContainerDiv.children.item(1).firstElementChild
52 | return parseFloat(priceDiv.innerText.replace(/[^0-9.]+/g, ''))
53 | } catch (e) {}
54 |
55 | return 0
56 | }
57 |
58 | const postIdentityMessage = (id, method, payload) => {
59 | const identityFrame = document.getElementById('identity')
60 | if (!identityFrame) throw 'No identity frame found'
61 |
62 | identityFrame.contentWindow.postMessage({
63 | id: id,
64 | service: 'identity',
65 | method: method,
66 | payload: payload
67 | }, '*')
68 | }
69 |
70 | const sendSignTransactionMsg = (identity, transactionHex, id) => {
71 | const payload = {
72 | transactionHex: transactionHex
73 | }
74 |
75 | if (identity) {
76 | payload.accessLevel = identity.accessLevel
77 | payload.accessLevelHmac = identity.accessLevelHmac
78 | payload.encryptedSeedHex = identity.encryptedSeedHex
79 | }
80 |
81 | pendingIdentityMessageId = id
82 | pendingTransactionHex = transactionHex
83 |
84 | postIdentityMessage(id, 'sign', payload)
85 | }
86 |
87 | const getIdentityUsers = () => {
88 | const users = window.localStorage.getItem('identityUsersV2')
89 | return users && JSON.parse(users)
90 | }
91 |
92 | const getCurrentIdentity = () => {
93 | const key = getLoggedInPublicKey()
94 | const identityUsers = getIdentityUsers()
95 | return key && identityUsers && identityUsers[key]
96 | }
97 |
98 | const uuid = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
99 | const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8)
100 | return v.toString(16)
101 | })
102 |
103 | const isEmpty = obj => Object.keys(obj).length === 0
--------------------------------------------------------------------------------
/background.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) Paul Burke 2021
3 | Github: @ipaulpro/bitcloutplus
4 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
5 | */
6 |
7 | const PUBLIC_KEY_PREFIX = 'BC1YL'
8 | const PUBLIC_KEY_LENGTH = 55
9 |
10 | const DELAY_MS_MEMPOOL_CALL = 5 * 60 * 1000 // 5 min
11 | const KEY_LATEST_MEMPOOL_CALL = 'latest-mempool-call'
12 | const KEY_LATEST_MEMPOOL_USERS = 'latest-mempool-users'
13 |
14 | const MSG_GET_MEMPOOL_TRANSACTORS = 'get-mempool-transactors'
15 |
16 | // Omnibox
17 |
18 | chrome.omnibox.onInputEntered.addListener(async text => {
19 | // Encode user input for special characters , / ? : @ & = + $ #
20 | const query = encodeURIComponent(text)
21 |
22 | if (isPublicKeyBase58Check(query)) {
23 | let username
24 |
25 | try {
26 | username = await getUsernameForPublicKey(query)
27 | } catch (e) {
28 | console.error(`No username found for '${query}'`, e)
29 | }
30 |
31 | if (username) {
32 | await openUserInTab(username)
33 | return
34 | }
35 | }
36 |
37 | await openUserInTab(query)
38 | })
39 |
40 | const isPublicKeyBase58Check = (query) => {
41 | return query.startsWith(PUBLIC_KEY_PREFIX) && query.length === PUBLIC_KEY_LENGTH
42 | }
43 |
44 | const getUsernameForPublicKey = (publicKey) => {
45 | if (!publicKey) return Promise.reject('Missing required parameter publicKey')
46 |
47 | return fetch(`https://node.deso.org/api/v0/get-user-name-for-public-key/${publicKey}`)
48 | .then(res => res.json())
49 | .then(atob)
50 | }
51 |
52 | const openUserInTab = async (username) => {
53 | const newURL = 'https://node.deso.org/u/' + username
54 | await chrome.tabs.create({url: newURL})
55 | }
56 |
57 | // Worker thread
58 |
59 | chrome.runtime.onMessage.addListener( (message, sender, sendResponse) => {
60 | switch (message.type) {
61 | case MSG_GET_MEMPOOL_TRANSACTORS:
62 | getMempoolTransactors()
63 | .then(mempoolTransactors => sendResponse({mempoolTransactors}))
64 | .catch(() => sendResponse({mempoolTransactors: []}))
65 | }
66 |
67 | return true
68 | })
69 |
70 | const getMempoolTransactors = async () => {
71 | const savedTransactors = await getSavedMempoolTransactors()
72 | if (savedTransactors) return savedTransactors
73 |
74 | const transactions = await getMempoolTransactions()
75 | const transactors = extractTransactors(transactions)
76 |
77 | await saveTransactors(transactors)
78 |
79 | return transactors
80 | }
81 |
82 | const getSavedMempoolTransactors = async () => {
83 | const savedItems = await chrome.storage.local.get([KEY_LATEST_MEMPOOL_CALL, KEY_LATEST_MEMPOOL_USERS])
84 |
85 | const latestCall = savedItems[KEY_LATEST_MEMPOOL_CALL] ?? 0
86 | if (Date.now() - latestCall < DELAY_MS_MEMPOOL_CALL) {
87 | const savedUsers = savedItems[KEY_LATEST_MEMPOOL_USERS]
88 | return JSON.parse(savedUsers)
89 | }
90 |
91 | return null
92 | }
93 |
94 | const getMempoolTransactions = () => {
95 | const request = {
96 | 'headers': {
97 | 'content-type': 'application/json',
98 | },
99 | 'method': 'POST',
100 | 'body': JSON.stringify({
101 | IsMempool: true
102 | })
103 | }
104 |
105 | return fetch(`https://node.deso.org/api/v1/transaction-info`, request)
106 | .then(res => res.json())
107 | .then(res => res['Transactions'])
108 | }
109 |
110 | const extractTransactors = (transactions) => {
111 | const transactors = new Set()
112 | transactions.forEach(transaction => {
113 | const transactor = transaction['TransactionMetadata']['TransactorPublicKeyBase58Check']
114 | transactors.add(transactor)
115 | })
116 | return [...transactors]
117 | }
118 |
119 | const saveTransactors = async (transactors) => {
120 | const items = {}
121 | items[KEY_LATEST_MEMPOOL_CALL] = Date.now()
122 | items[KEY_LATEST_MEMPOOL_USERS] = JSON.stringify(transactors)
123 |
124 | await chrome.storage.local.set(items)
125 | }
126 |
--------------------------------------------------------------------------------
/vendor/taboverride/taboverride.min.js:
--------------------------------------------------------------------------------
1 | /*! taboverride v4.0.3 | https://github.com/wjbryant/taboverride
2 | (c) 2015 Bill Bryant | http://opensource.org/licenses/mit */
3 | !function(a){"use strict";var b;"object"==typeof exports?a(exports):"function"==typeof define&&define.amd?define(["exports"],a):(b=window.tabOverride={},a(b))}(function(a){"use strict";function b(a,b){var c,d,e,f=["alt","ctrl","meta","shift"],g=a.length,h=!0;for(c=0;g>c;c+=1)if(!b[a[c]]){h=!1;break}if(h)for(c=0;cd;d+=1)if(e===a[d]){h=!0;break}}else h=!1;if(!h)break}return h}function c(a,c){return a===q&&b(s,c)}function d(a,c){return a===r&&b(t,c)}function e(a,b){return function(c,d){var e,f="";if(arguments.length){if("number"==typeof c&&(a(c),b.length=0,d&&d.length))for(e=0;e1?(i=f.slice(0,l).split(m).length-1,j=t.split(m).length-1):i=j=0}if(E===q||E===r)if(b=p,e=b.length,y=0,z=0,A=0,l!==s&&-1!==t.indexOf("\n"))if(w=0===l||"\n"===f.charAt(l-1)?l:f.lastIndexOf("\n",l-1)+1,s===f.length||"\n"===f.charAt(s)?x=s:"\n"===f.charAt(s-1)?x=s-1:(x=f.indexOf("\n",s),-1===x&&(x=f.length)),c(E,a))y=1,D.value=f.slice(0,w)+b+f.slice(w,x).replace(/\n/g,function(){return y+=1,"\n"+b})+f.slice(x),g?(g.collapse(),g.moveEnd(F,s+y*e-j-i),g.moveStart(F,l+e-i),g.select()):(D.selectionStart=l+e,D.selectionEnd=s+y*e,D.scrollTop=k);else{if(!d(E,a))return;0===f.slice(w).indexOf(b)&&(w===l?t=t.slice(e):A=e,z=e),D.value=f.slice(0,w)+f.slice(w+A,l)+t.replace(new RegExp("\n"+b,"g"),function(){return y+=1,"\n"})+f.slice(s),g?(g.collapse(),g.moveEnd(F,s-z-y*e-j-i),g.moveStart(F,l-A-i),g.select()):(D.selectionStart=l-A,D.selectionEnd=s-z-y*e)}else if(c(E,a))g?(g.text=b,g.select()):(D.value=f.slice(0,l)+b+f.slice(s),D.selectionEnd=D.selectionStart=l+e,D.scrollTop=k);else{if(!d(E,a))return;0===f.slice(l-e).indexOf(b)&&(D.value=f.slice(0,l-e)+f.slice(l),g?(g.move(F,l-e-i),g.select()):(D.selectionEnd=D.selectionStart=l-e,D.scrollTop=k))}else if(u){if(0===l||"\n"===f.charAt(l-1))return void(v=!0);if(w=f.lastIndexOf("\n",l-1)+1,x=f.indexOf("\n",l),-1===x&&(x=f.length),B=f.slice(w,x).match(/^[ \t]*/)[0],C=B.length,w+C>l)return void(v=!0);g?(g.text="\n"+B,g.select()):(D.value=f.slice(0,l)+"\n"+B+f.slice(s),D.selectionEnd=D.selectionStart=l+n+C,D.scrollTop=k)}return a.preventDefault?void a.preventDefault():(a.returnValue=!1,!1)}}function g(a){a=a||event;var b=a.keyCode;if(c(b,a)||d(b,a)||13===b&&u&&!v){if(!a.preventDefault)return a.returnValue=!1,!1;a.preventDefault()}}function h(a,b){var c,d=x[a]||[],e=d.length;for(c=0;e>c;c+=1)d[c].apply(null,b)}function i(a){function b(b){for(c=0;f>c;c+=1)b(a[c].type,a[c].handler)}var c,d,e,f=a.length;return o.addEventListener?(d=function(a){b(function(b,c){a.removeEventListener(b,c,!1)})},e=function(a){d(a),b(function(b,c){a.addEventListener(b,c,!1)})}):o.attachEvent&&(d=function(a){b(function(b,c){a.detachEvent("on"+b,c)})},e=function(a){d(a),b(function(b,c){a.attachEvent("on"+b,c)})}),{add:e,remove:d}}function j(a){h("addListeners",[a]),l.add(a)}function k(a){h("removeListeners",[a]),l.remove(a)}var l,m,n,o=window.document,p="\t",q=9,r=9,s=[],t=["shiftKey"],u=!0,v=!1,w=o.createElement("textarea"),x={};l=i([{type:"keydown",handler:f},{type:"keypress",handler:g}]),w.value="\n",m=w.value,n=m.length,w=null,a.utils={executeExtensions:h,isValidModifierKeyCombo:b,createListeners:i,addListeners:j,removeListeners:k},a.handlers={keydown:f,keypress:g},a.addExtension=function(a,b){return a&&"string"==typeof a&&"function"==typeof b&&(x[a]||(x[a]=[]),x[a].push(b)),this},a.set=function(a,b){var c,d,e,f,g,i,l;if(a)for(c=arguments.length<2||b,d=a,e=d.length,"number"!=typeof e&&(d=[d],e=1),c?(f=j,g="true"):(f=k,g=""),i=0;e>i;i+=1)l=d[i],l&&l.nodeName&&"textarea"===l.nodeName.toLowerCase()&&(h("set",[l,c]),l.setAttribute("data-taboverride-enabled",g),f(l));return this},a.tabSize=function(a){var b;if(arguments.length){if(a&&"number"==typeof a&&a>0)for(p="",b=0;a>b;b+=1)p+=" ";else p="\t";return this}return"\t"===p?0:p.length},a.autoIndent=function(a){return arguments.length?(u=a?!0:!1,this):u},a.tabKey=e(function(a){return arguments.length?void(q=a):q},s),a.untabKey=e(function(a){return arguments.length?void(r=a):r},t)});
4 | //# sourceMappingURL=taboverride.min.js.map
--------------------------------------------------------------------------------
/main.css:
--------------------------------------------------------------------------------
1 | /* Bootstrap overrides */
2 |
3 | bs-dropdown-container .dropdown-menu {
4 | min-width: 12rem !important;
5 | }
6 |
7 | /* Hide scrollbar for Chrome, Safari and Opera */
8 | .global__nav__inner::-webkit-scrollbar, .global__sidebar__inner::-webkit-scrollbar {
9 | display: none;
10 | }
11 |
12 | /* Hide scrollbar for IE, Edge and Firefox */
13 | .global__nav__inner, .global__sidebar__inner {
14 | -ms-overflow-style: none; /* IE and Edge */
15 | scrollbar-width: none; /* Firefox */
16 | }
17 | /* Vendor overrides */
18 |
19 | .tribute-container {
20 | filter: drop-shadow(0 0 0.25rem rgba(0, 0, 0, 0.54));
21 | margin-top: 0.75rem;
22 | }
23 |
24 | .tribute-container ul {
25 | padding: 0 !important;
26 | background: var(--secondary) !important;
27 | border-radius: 14px !important;
28 | min-width: 220px !important;
29 | }
30 |
31 | .tribute-container li {
32 | padding: 12px 36px 12px 12px;
33 | font-size: 15px;
34 | color: var(--text);
35 | }
36 |
37 | .tribute-container li:not(:last-child) {
38 | border-bottom: 1px solid var(--border) !important;
39 | }
40 |
41 | .tribute-container li.highlight {
42 | background: var(--border) !important;
43 | }
44 |
45 | .tribute-container li:first-child.highlight {
46 | border-top-left-radius: 14px;
47 | border-top-right-radius: 14px;
48 | }
49 |
50 | .tribute-container li:last-child.highlight {
51 | border-bottom-left-radius: 14px;
52 | border-bottom-right-radius: 14px;
53 | }
54 |
55 | .tribute-avatar {
56 | height: 40px;
57 | width: 40px;
58 | border-radius: 6px;
59 | background-size: cover;
60 | background-repeat: no-repeat;
61 | background-position: 50%;
62 | }
63 |
64 | /* Project */
65 |
66 | #plus-add-new-post {
67 | background-color: var(--secondary);
68 | border-color: var(--secondary);
69 | color: var(--grey);
70 | border-radius: 24px;
71 | padding-left: 2rem;
72 | padding-right: 2rem;
73 | }
74 |
75 | #plus-add-new-post:hover {
76 | background-color: var(--border);
77 | border-color: var(--border);
78 | color: var(--text);
79 | }
80 |
81 | #plus-profile-sell-btn {
82 | background-color: var(--border);
83 | border-color: var(--border);
84 | color: var(--textalt);
85 | }
86 |
87 | #plus-profile-sell-btn:hover {
88 | background-color: var(--secondary);
89 | border-color: var(--secondary);
90 | color: var(--text);
91 | }
92 |
93 | .plus-text-muted {
94 | color: var(--grey);
95 | }
96 |
97 | .plus-text-primary {
98 | color: var(--text);
99 | }
100 |
101 | .plus-text-red {
102 | color: var(--cred);
103 | }
104 |
105 | .plus-badge {
106 | background-color: var(--secalt) !important;
107 | color: var(--norm) !important;
108 | padding: 0.3em 0.6em !important;
109 | font-weight: normal !important;
110 | font-size: 11px !important;
111 | }
112 |
113 | .plus-badge-icon {
114 | padding: 0.5em !important;
115 | }
116 |
117 | .plus-tooltip {
118 | position: relative;
119 | display: inline-block;
120 | }
121 |
122 | .plus-tooltip .plus-tooltip-text {
123 | font-family: "Roboto", sans-serif;
124 | visibility: hidden;
125 | background-color: black;
126 | color: #fff;
127 | text-align: center;
128 | padding: 0.5em 1em;
129 | border-radius: 6px;
130 | position: absolute;
131 | z-index: 1;
132 | }
133 |
134 | .plus-tooltip:hover .plus-tooltip-text {
135 | visibility: visible;
136 | }
137 |
138 | .plus-active-indicator-container {
139 | position: absolute;
140 | right: 0;
141 | bottom: 0;
142 | background-color: white;
143 | border-radius: 50% 0 0 0;
144 | height: 16px;
145 | width: 16px;
146 | display: grid;
147 | padding: 1px 1px;
148 | place-items: center;
149 | box-shadow: -1px -1px 0.15rem #40000000;
150 | }
151 |
152 | .plus-active-indicator {
153 | width: 10px;
154 | height: 10px;
155 | clip-path: circle(50%);
156 | }
157 |
158 | .__clout-mask-disabled {
159 | opacity: 0.5;
160 | pointer-events: none;
161 | }
162 |
163 | .change-account-selector_list__inner > .change-account-selector_list-item > .plus-account-logout > .btn > .plus-account-logout-icon {
164 | visibility: hidden;
165 | }
166 |
167 | .change-account-selector_list__inner:hover > .change-account-selector_list-item > .plus-account-logout > .btn > .plus-account-logout-icon {
168 | visibility: visible;
169 | }
170 |
171 | /* Hide scrollbar for Chrome, Safari and Opera */
172 | #plus-online-users-list::-webkit-scrollbar {
173 | display: none;
174 | }
175 |
176 | /* Hide scrollbar for IE, Edge and Firefox */
177 | #plus-online-users-list {
178 | -ms-overflow-style: none; /* IE and Edge */
179 | scrollbar-width: none; /* Firefox */
180 | }
181 |
182 | .plus-message-link {
183 | color: #6c757d;
184 | }
185 |
186 | .plus-message-link:hover {
187 | color: white;
188 | }
--------------------------------------------------------------------------------
/lib/embed.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) Paul Burke 2021
3 | Github: @ipaulpro/bitcloutplus
4 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
5 |
6 | Adapted from https://github.com/bitclout/frontend/
7 | */
8 |
9 | const isVimeoFromURL = (url) => {
10 | const pattern = /\bvimeo\.com$/
11 | return pattern.test(url.hostname)
12 | }
13 |
14 | const isYoutubeFromURL = (url) => {
15 | const patterns = [/\byoutube\.com$/, /\byoutu\.be$/]
16 | return patterns.some((p) => p.test(url.hostname))
17 | }
18 |
19 | const isTiktokFromURL = (url) => {
20 | const pattern = /\btiktok\.com$/
21 | return pattern.test(url.hostname)
22 | }
23 |
24 | const isGiphyFromURL = (url) => {
25 | const pattern = /\bgiphy\.com$/
26 | return pattern.test(url.hostname)
27 | }
28 |
29 | const isSpotifyFromURL = (url) => {
30 | const pattern = /\bspotify\.com$/
31 | return pattern.test(url.hostname)
32 | }
33 |
34 | const isSoundCloudFromURL = (url) => {
35 | const pattern = /\bsoundcloud\.com$/
36 | return pattern.test(url.hostname)
37 | }
38 |
39 | // This regex helps extract the correct videoID from the various forms of URLs that identify a youtube video.
40 | const youtubeParser = (url) => {
41 | const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([A-Za-z0-9_-]{11}).*/
42 | const match = url.match(regExp)
43 | return match && match[7].length === 11 ? match[7] : false
44 | }
45 |
46 | const constructYoutubeEmbedURL = (url) => {
47 | const youtubeVideoID = youtubeParser(url.toString())
48 | // If we can't find the videoID, return the empty string which stops the iframe from loading.
49 | return youtubeVideoID ? `https://www.youtube.com/embed/${youtubeVideoID}` : ""
50 | }
51 |
52 | // Vimeo video URLs are simple -- anything after the last "/" in the url indicates the videoID.
53 | const vimeoParser = (url) => {
54 | const regExp = /^.*((player\.)?vimeo\.com\/)(video\/)?(\d{0,15}).*/
55 | const match = url.match(regExp)
56 | return match && match[4] ? match[4] : false
57 | }
58 |
59 | const constructVimeoEmbedURL = (url) => {
60 | const vimeoVideoID = vimeoParser(url.toString())
61 | return vimeoVideoID ? `https://player.vimeo.com/video/${vimeoVideoID}` : ""
62 | }
63 |
64 | const giphyParser = (url) => {
65 | const regExp = /^.*((media\.)?giphy\.com\/(gifs|media|embed|clips)\/)([A-Za-z0-9]+-)*([A-Za-z0-9]{0,20}).*/
66 | const match = url.match(regExp)
67 | return match && match[5] ? match[5] : false
68 | }
69 |
70 | const constructGiphyEmbedURL = (url) => {
71 | const giphyId = giphyParser(url.toString())
72 | return giphyId ? `https://giphy.com/embed/${giphyId}` : ""
73 | }
74 |
75 | const spotifyParser = (url) => {
76 | const regExp = /^.*(open\.)?spotify\.com\/(((embed\/)?(track|artist|playlist|album))|((embed-podcast\/)?(episode|show)))\/([A-Za-z0-9]{0,25}).*/
77 | const match = url.match(regExp)
78 | if (match && match[9]) {
79 | if (match[8]) {
80 | return `embed-podcast/${match[8]}/${match[9]}`
81 | }
82 | if (match[5]) {
83 | return `embed/${match[5]}/${match[9]}`
84 | }
85 | }
86 | return false
87 | }
88 |
89 | const constructSpotifyEmbedURL = (url) => {
90 | const spotifyEmbedSuffix = spotifyParser(url.toString())
91 | return spotifyEmbedSuffix ? `https://open.spotify.com/${spotifyEmbedSuffix}` : ""
92 | }
93 |
94 | const soundCloudParser = (url) => {
95 | const regExp = /^.*(soundcloud.com\/([a-z0-9-_]+)\/(sets\/)?([a-z0-9-_]+)).*/
96 | const match = url.match(regExp)
97 | return match && match[1] ? match[1] : false
98 | }
99 |
100 | const constructSoundCloudEmbedURL = (url) => {
101 | const soundCloudURL = soundCloudParser(url.toString())
102 | return soundCloudURL
103 | ? `https://w.soundcloud.com/player/?url=https://${soundCloudURL}?hide_related=true&show_comments=false`
104 | : ""
105 | }
106 |
107 | const extractTikTokVideoID = (fullTikTokURL) => {
108 | const regExp = /^.*((tiktok\.com\/)(v\/)|(@[A-Za-z0-9_-]{2,24}\/video\/)|(embed\/v2\/))(\d{0,30}).*/
109 | const match = fullTikTokURL.match(regExp)
110 | return match && match[6] ? match[6] : false
111 | }
112 |
113 | const tiktokParser = (url) => {
114 | let tiktokURL
115 | try {
116 | tiktokURL = new URL(url)
117 | } catch (e) {
118 | return undefined
119 | }
120 |
121 | if (tiktokURL.hostname === "vm.tiktok.com") {
122 | const regExp = /^.*(vm\.tiktok\.com\/)([A-Za-z0-9]{6,12}).*/
123 | const match = url.match(regExp)
124 | if (!match || !match[2]) {
125 | return undefined
126 | }
127 |
128 | return extractTikTokVideoID(url)
129 | }
130 | }
131 |
132 | const constructTikTokEmbedURL = (url) => {
133 | const tikTokId = tiktokParser(url.toString())
134 | if (!tikTokId) return undefined
135 |
136 | return `https://www.tiktok.com/embed/v2/${tikTokId}`
137 | }
138 |
139 | const twitchParser = (url) => {
140 | const regExp = /^.*((player\.|clips\.)?twitch\.tv)\/(videos\/(\d{8,12})|\?video=(\d{8,12})|\?channel=([A-Za-z0-9_]{1,30})|collections\/([A-Za-z0-9]{10,20})|\?collection=([A-Za-z0-9]{10,20}(&video=\d{8,12})?)|embed\?clip=([A-Za-z0-9_-]{1,80})|([A-Za-z0-9_]{1,30}(\/clip\/([A-Za-z0-9_-]{1,80}))?)).*/
141 | const match = url.match(regExp)
142 | if (match && match[3]) {
143 | // https://www.twitch.tv/videos/1234567890
144 | if (match[3].startsWith('videos') && match[4]) {
145 | return `player.twitch.tv/?video=${match[4]}`
146 | }
147 | // https://player.twitch.tv/?video=1234567890&parent=www.example.com
148 | if (match[3].startsWith('?video=') && match[5]) {
149 | return `player.twitch.tv/?video=${match[5]}`
150 | }
151 | // https://player.twitch.tv/?channel=xxxyyy123&parent=www.example.com
152 | if (match[3].startsWith('?channel=') && match[6]) {
153 | return `player.twitch.tv/?channel=${match[6]}`
154 | }
155 | // https://www.twitch.tv/xxxyyy123
156 | if (match[3] && match[11] && match[3] === match[11] && !match[12] && !match[13]) {
157 | return `player.twitch.tv/?channel=${match[11]}`
158 | }
159 | // https://www.twitch.tv/xxyy_1234m/clip/AbCD123JMn-rrMMSj1239G7
160 | if (match[12] && match[13]) {
161 | return `clips.twitch.tv/embed?clip=${match[13]}`
162 | }
163 | // https://clips.twitch.tv/embed?clip=AbCD123JMn-rrMMSj1239G7&parent=www.example.com
164 | if (match[10]) {
165 | return `clips.twitch.tv/embed?clip=${match[10]}`
166 | }
167 | // https://www.twitch.tv/collections/11jaabbcc2yM989x?filter=collections
168 | if (match[7]) {
169 | return `player.twitch.tv/?collection=${match[7]}`
170 | }
171 | // https://player.twitch.tv/?collection=11jaabbcc2yM989x&video=1234567890&parent=www.example.com
172 | if (match[8]) {
173 | return `player.twitch.tv/?collection=${match[8]}`
174 | }
175 | }
176 | return false
177 | }
178 |
179 | const constructTwitchEmbedURL = (url) => {
180 | const twitchParsed = twitchParser(url.toString())
181 | return twitchParsed ? `https://${twitchParsed}` : ''
182 | }
183 |
184 | const isTwitchFromURL = (url) => {
185 | const pattern = /\btwitch\.tv$/
186 | return pattern.test(url.hostname)
187 | }
188 |
189 | const getEmbedURL = (embedURL) => {
190 | if (!embedURL) {
191 | return undefined
192 | }
193 | let url
194 | try {
195 | url = new URL(embedURL)
196 | } catch (e) {
197 | // If the embed video URL doesn't start with http(s), try the url with that as a prefix.
198 | if (!embedURL.startsWith("https://") && !embedURL.startsWith("http://")) {
199 | return getEmbedURL(`https://${embedURL}`)
200 | }
201 | return undefined
202 | }
203 | if (isYoutubeFromURL(url)) {
204 | return constructYoutubeEmbedURL(url)
205 | }
206 | if (isVimeoFromURL(url)) {
207 | return constructVimeoEmbedURL(url)
208 | }
209 | if (isTiktokFromURL(url)) {
210 | return constructTikTokEmbedURL(url)
211 | }
212 | if (isGiphyFromURL(url)) {
213 | return constructGiphyEmbedURL(url)
214 | }
215 | if (isSpotifyFromURL(url)) {
216 | return constructSpotifyEmbedURL(url)
217 | }
218 | if (isSoundCloudFromURL(url)) {
219 | return constructSoundCloudEmbedURL(url)
220 | }
221 | if (isTwitchFromURL(url)) {
222 | const embedURL = constructTwitchEmbedURL(url)
223 | return embedURL ? embedURL + `&autoplay=false&parent=${location.hostname}` : ""
224 | }
225 | return undefined
226 | }
227 |
--------------------------------------------------------------------------------
/lib/follow.js:
--------------------------------------------------------------------------------
1 | let blockJwtMsgId
2 | let pendingBlockUser = {}
3 |
4 | const isBlockedUsersUrl = () => {
5 | const segments = new URL(document.location).pathname.split('/')
6 | const params = (new URL(document.location)).searchParams
7 | return segments[1] === 'u' && segments[segments.length - 1] === 'following' && params.get('tab') === 'blocked'
8 | }
9 |
10 | const addFollowsYouBadgeToFollowingItems = (nodes, followerUsernames) => {
11 | nodes.forEach(node => {
12 | const buyLink = node.querySelector('.feed-post__coin-price-holder')
13 | if (!buyLink) return
14 |
15 | const username = buyLink.parentElement.firstElementChild.innerText.trim()
16 | if (followerUsernames.indexOf(username) < 0) return
17 |
18 | const followsYouSpan = createFollowsYouBadge()
19 | buyLink.parentElement.insertBefore(followsYouSpan, buyLink.parentElement.lastElementChild)
20 | })
21 | }
22 |
23 | const observeFollowLists = (page) => {
24 | const loggedInPublicKey = getLoggedInPublicKey()
25 | if (!loggedInPublicKey) return
26 |
27 | const getFilteredSidNodes = (nodes) => Array.from(nodes).filter(node => node.dataset && node.dataset.sid)
28 |
29 | getFollowersByPublicKey(loggedInPublicKey).then(res => res['PublicKeyToProfileEntry']).then(followersMap => {
30 | const listDiv = page.querySelector('[ui-scroll]')
31 | if (!listDiv) return
32 |
33 | const followerValues = Object.values(followersMap)
34 | const followerUsernames = followerValues.map(follower => follower ? follower['Username'] : "")
35 |
36 | // Add to existing list items
37 | const nodes = getFilteredSidNodes(listDiv.childNodes)
38 | addFollowsYouBadgeToFollowingItems(nodes, followerUsernames)
39 |
40 | // Listen for new list items
41 | const observerConfig = { childList: true, subtree: false }
42 | const observer = new MutationObserver(mutations => {
43 | mutations.forEach(mutation => {
44 | const nodes = getFilteredSidNodes(mutation.addedNodes)
45 | addFollowsYouBadgeToFollowingItems(nodes, followerUsernames)
46 | })
47 | })
48 | observer.observe(listDiv, observerConfig)
49 | })
50 | }
51 |
52 | const addBlockedUsersTabToFollows = (page) => {
53 | const loggedInPublicKey = getLoggedInPublicKey()
54 | if (!loggedInPublicKey) return
55 |
56 | const blockedUsersTabClass = 'plus-blocked-users-tab'
57 | if (document.getElementsByClassName(blockedUsersTabClass).length >= 2) return
58 |
59 | const tabSelectors = page.querySelectorAll('tab-selector')
60 | tabSelectors.forEach(tabSelector => {
61 | const tabsInner = tabSelector?.firstElementChild
62 | if (!tabsInner) return
63 |
64 | const username = getUsernameFromUrl()
65 |
66 | // Restore the click handling for the Following tab
67 | tabsInner.children.item(1).onclick = () => window.location.href = `/u/${username}/following`
68 |
69 | const tabText = document.createElement('div')
70 | tabText.className = 'd-flex h-100 align-items-center fs-15px fc-muted'
71 | tabText.innerText = 'Blocked'
72 |
73 | const tabUnderlineActive = document.createElement('div')
74 | tabUnderlineActive.className = 'tab-underline-inactive'
75 | tabUnderlineActive.style.width = '50px'
76 |
77 | const blockedUsersTab = document.createElement('div')
78 | blockedUsersTab.className = blockedUsersTabClass + ' d-flex flex-column align-items-center h-100 pl-15px pr-15px'
79 | blockedUsersTab.appendChild(tabText)
80 | blockedUsersTab.appendChild(tabUnderlineActive)
81 |
82 | blockedUsersTab.onclick = () => window.location.href = `/u/${username}/following?tab=blocked`
83 |
84 | tabsInner.appendChild(blockedUsersTab)
85 | })
86 | }
87 |
88 | const createListFromBlockedUsers = (publicKey, users, blockList) => {
89 | const loggedInPublicKey = getLoggedInPublicKey()
90 | const isLoggedInUser = loggedInPublicKey === publicKey
91 |
92 | const profiles = users.map(user => user['ProfileEntryResponse'])
93 | profiles.sort((a, b) => a['Username'].localeCompare(b['Username'])).forEach(profile => {
94 | const container = document.createElement('div')
95 |
96 | blockList.appendChild(container)
97 |
98 | const row = document.createElement('div')
99 | row.className = 'row no-gutters px-15px border-bottom fs-15px h-100'
100 |
101 | container.appendChild(row)
102 |
103 | const blockPublicKey = profile['PublicKeyBase58Check']
104 | const username = profile['Username']
105 |
106 | const href = `/u/${username}?tab=posts`
107 |
108 | const outerAnchor = document.createElement('div')
109 | outerAnchor.className = 'fs-15px d-flex justify-content-left w-100 border-color-grey p-15px'
110 |
111 | row.appendChild(outerAnchor)
112 |
113 | const avatarContainer = document.createElement('div')
114 | avatarContainer.className = 'manage-follows__avatar-container'
115 |
116 | outerAnchor.appendChild(avatarContainer)
117 |
118 | const avatar = document.createElement('a')
119 | avatar.className = 'manage-follows__avatar br-12px'
120 | avatar.style.backgroundImage = `url("${getProfilePhotoUrlForPublicKey(blockPublicKey)}")`
121 | avatar.href = href
122 |
123 | avatarContainer.appendChild(avatar)
124 |
125 | const textContainer = document.createElement('div')
126 | textContainer.className = 'w-100 d-flex'
127 |
128 | outerAnchor.appendChild(textContainer)
129 |
130 | const textInner = document.createElement('div')
131 | textInner.className = 'w-100 d-flex align-items-center'
132 |
133 | textContainer.appendChild(textInner)
134 |
135 | const textAnchor = document.createElement('a')
136 | textAnchor.className = 'fc-default font-weight-bold flex-grow-1 py-2'
137 | textAnchor.innerText = ' ' + username
138 | textAnchor.href = href
139 |
140 | textInner.appendChild(textAnchor)
141 |
142 | if (isLoggedInUser) {
143 | const buttonContainer = document.createElement('div')
144 | buttonContainer.className = 'ml-auto'
145 |
146 | textInner.appendChild(buttonContainer)
147 |
148 | const button = document.createElement('button')
149 | button.className = 'btn btn-sm btn-danger'
150 | button.innerText = 'Unblock'
151 | button.onclick = () => {
152 | button.classList.remove('btn-danger')
153 | button.classList.add('btn-outline-secondary')
154 | button.disabled = true
155 | pendingBlockUser = {
156 | PublicKeyBase58Check: publicKey,
157 | BlockPublicKeyBase58Check: blockPublicKey,
158 | Unblock: true
159 | }
160 | blockJwtMsgId = uuid()
161 | getJwt(blockJwtMsgId)
162 | }
163 |
164 | buttonContainer.appendChild(button)
165 | }
166 | })
167 | }
168 |
169 | const addBlockedUsersList = (page) => {
170 | const blockListId = 'plus-blocked-users-list'
171 | if (document.getElementById(blockListId)) return
172 |
173 | const activeTabs = document.querySelectorAll('.tab-underline-active')
174 | activeTabs.forEach(activeTab => {
175 | activeTab.className = 'tab-underline-inactive'
176 | })
177 |
178 | const tabs = document.querySelectorAll('.plus-blocked-users-tab > .tab-underline-inactive')
179 | tabs.forEach(tab => {
180 | tab.className = 'tab-underline-active'
181 | })
182 |
183 | const listDiv = page.querySelector('[ui-scroll]')
184 | if (!listDiv) return
185 |
186 | const listParent = listDiv.parentElement
187 | listDiv.remove()
188 |
189 | const footer = page.querySelector('.global__bottom-bar-mobile-height')
190 |
191 | const blockList = document.createElement('div')
192 | blockList.id = blockListId
193 | listParent.insertBefore(blockList, footer)
194 |
195 | const username = getUsernameFromUrl()
196 | getProfileByUsername(username)
197 | .then(profile => {
198 | const publicKey = profile['PublicKeyBase58Check']
199 | return getUserMetadata(publicKey)
200 | .then(userMetadata => {
201 | const blockedPubKeys = userMetadata['BlockedPubKeys']
202 | if (blockedPubKeys && !isEmpty(blockedPubKeys)) {
203 | return getUsers(Object.keys(blockedPubKeys))
204 | }
205 | return new Error(`No blocked users found`)
206 | })
207 | .then(users => createListFromBlockedUsers(publicKey, users, blockList))
208 | })
209 | .catch(console.error)
210 | }
211 |
212 | const blockUser = (jwt) => {
213 | if (!pendingBlockUser) return
214 |
215 | blockPublicKey(pendingBlockUser, jwt).catch(console.error)
216 |
217 | blockJwtMsgId = null
218 | pendingBlockUser = {}
219 | }
--------------------------------------------------------------------------------
/lib/cloutmask.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) Paul Burke 2021
3 | Github: @ipaulpro/bitcloutplus
4 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
5 | */
6 |
7 | "use strict"
8 |
9 | let isLookingUpHasPressedMaskButton
10 |
11 | const hasPressedMaskButton = () => new Promise((resolve) => {
12 | isLookingUpHasPressedMaskButton = true
13 | chrome.storage.local.get(['hasPressedMaskButton'], items => {
14 | isLookingUpHasPressedMaskButton = false
15 | if (chrome.runtime.lastError) return resolve(false)
16 | resolve(items.hasPressedMaskButton)
17 | })
18 | })
19 |
20 | const setHasPressedMaskButton = () => {
21 | chrome.storage.local.set({hasPressedMaskButton: true})
22 | }
23 |
24 | const isMaskedUser = (identityUser) => {
25 | return identityUser && identityUser['isMaskedUser']
26 | }
27 |
28 | const isLoggedInAsMaskedUser = () => {
29 | const loggedInIdentityUser = getCurrentIdentity()
30 | return loggedInIdentityUser && isMaskedUser(loggedInIdentityUser)
31 | }
32 |
33 | const getMaskedIdentityUsers = () => {
34 | const identityUsers = getIdentityUsers()
35 | if (!identityUsers) return
36 |
37 | const maskedUsers = []
38 | for (const key in identityUsers) {
39 | const identityUser = identityUsers[key]
40 | if (identityUser.isMaskedUser) maskedUsers.push(identityUser)
41 | }
42 | return maskedUsers
43 | }
44 |
45 | const addPublicKeyToIdentityUsers = (key) => {
46 | const identityUsers = getIdentityUsers()
47 | if (!identityUsers || identityUsers[key]) return
48 |
49 | const dummyUser = getCurrentIdentity() || {}
50 | dummyUser.isMaskedUser = true
51 | identityUsers[key] = dummyUser
52 |
53 | window.localStorage.setItem('identityUsersV2', JSON.stringify(identityUsers))
54 | window.localStorage.setItem('lastLoggedInUserV2', `"${key}"`)
55 |
56 | window.location.reload()
57 |
58 | setHasPressedMaskButton()
59 | }
60 |
61 | function switchToFirstAccount(identityUsers = getIdentityUsers()) {
62 | if (!identityUsers) return
63 | const firstKey = Object.keys(identityUsers)[0]
64 | if (firstKey) window.localStorage.setItem('lastLoggedInUserV2', `"${firstKey}"`)
65 | window.location.reload()
66 | }
67 |
68 | const removePublicKeyFromIdentityUsers = (key) => {
69 | const identityUsers = getIdentityUsers()
70 | if (!identityUsers || (identityUsers[key] && !identityUsers[key]['isMaskedUser'])) return
71 |
72 | try {
73 | delete identityUsers[key]
74 | window.localStorage.setItem('identityUsersV2', JSON.stringify(identityUsers))
75 | } catch (e) {
76 | return
77 | }
78 |
79 | if (key === getLoggedInPublicKey()) switchToFirstAccount(identityUsers)
80 | }
81 |
82 | const removeAllMaskedUsers = () => {
83 | const identityUsers = getIdentityUsers()
84 | const realUsers = {}
85 | const loggedInAsMaskedUser = isLoggedInAsMaskedUser()
86 |
87 | for (const key in identityUsers) {
88 | const identityUser = identityUsers[key]
89 | if (!identityUser.isMaskedUser) realUsers[key] = identityUser
90 | }
91 |
92 | window.localStorage.setItem('identityUsersV2', JSON.stringify(realUsers))
93 |
94 | if (loggedInAsMaskedUser) switchToFirstAccount(identityUsers)
95 | }
96 |
97 | const createCloutMaskIconElement = () => {
98 | const iconUrl = chrome.runtime.getURL('/images/cloutmask.svg')
99 | const img = document.createElement('img')
100 | img.width = 16
101 | img.height = 16
102 | img.alt = "Mask Logo"
103 | img.src = iconUrl
104 | return img
105 | }
106 |
107 | const addCloutMaskButton = (page) => {
108 | if (isLookingUpHasPressedMaskButton || !page || page.querySelector('#__clout-mask-button')) return
109 |
110 | const publicKeyFromPage = getPublicKeyFromPage(page)
111 | if (!publicKeyFromPage) return
112 |
113 | const identityUsers = getIdentityUsers()
114 | if (!identityUsers) return
115 |
116 | const pageIdentityUser = identityUsers[publicKeyFromPage]
117 | if (pageIdentityUser && !pageIdentityUser['isMaskedUser']) return
118 |
119 | const topBar = page.querySelector('.creator-profile__top-bar')
120 | if (!topBar) return
121 |
122 | hasPressedMaskButton().then(hasPressed => {
123 | topBar.style.justifyContent = 'flex-end'
124 | topBar.style.alignItems = 'center'
125 |
126 | const maskButton = document.createElement('button')
127 | maskButton.id = '__clout-mask-button'
128 | maskButton.className = 'btn btn-sm text-muted fs-14px rounded-pill'
129 | maskButton.classList.add(hasPressed ? 'btn-dark' : 'btn-primary')
130 |
131 | const icon = createCloutMaskIconElement().outerHTML
132 | const userAddedByCloutMask = isMaskedUser(pageIdentityUser)
133 | if (userAddedByCloutMask) {
134 | maskButton.innerHTML = `${icon} Remove account`
135 | maskButton.onclick = () => removePublicKeyFromIdentityUsers(publicKeyFromPage)
136 | topBar.appendChild(maskButton)
137 | } else if (!pageIdentityUser) {
138 | maskButton.setAttribute('bs-toggle', 'tooltip')
139 | maskButton.innerHTML = icon
140 | maskButton.title = "Browse as this user"
141 | maskButton.onclick = () => addPublicKeyToIdentityUsers(publicKeyFromPage)
142 | topBar.appendChild(maskButton)
143 | }
144 | })
145 | }
146 |
147 | const disabledClassName = '__clout-mask-disabled'
148 |
149 | const disableElement = (element) => {
150 | if (!element) return
151 | element.classList.add(disabledClassName)
152 | element.disabled = true
153 | }
154 |
155 | const reEnableElements = () => {
156 | const disabledElements = document.getElementsByClassName(disabledClassName)
157 | Array.from(disabledElements).forEach(element => {
158 | element.classList.remove(disabledClassName)
159 | element.disabled = false
160 | })
161 | }
162 |
163 | const disableFeedPostButtons = () => {
164 | const iconRows = document.querySelectorAll('.js-feed-post-icon-row__container')
165 | iconRows.forEach((row) => {
166 | const postButtons = Array.from(row.children)
167 | postButtons.splice(postButtons.length - 1, 1)
168 | postButtons.forEach(disableElement)
169 | })
170 | }
171 |
172 | const disableKnownLinks = () => {
173 | const anchors = document.querySelectorAll(
174 | "a[href*='/buy'], a[href*='/sell'], a[href*='/transfer'], a[href*='/select-creator-coin'], a[href*='/send-deso'], a[href*='/settings'], a[href*='/buy-deso'], a[href*='/admin'], a[href*='/nft-transfers']"
175 | )
176 | anchors.forEach(disableElement)
177 | }
178 |
179 | const disableClasses = () => {
180 | const elements = document.querySelectorAll(
181 | '.feed-create-post__textarea, .update-profile__image-delete, feed-post-dropdown, app-update-profile-page .btn-primary'
182 | )
183 | elements.forEach(disableElement)
184 | }
185 |
186 | const disablePostButtons = () => {
187 | const createPostElement = document.querySelector('feed-create-post')
188 | if (createPostElement) {
189 | const postButton = createPostElement.querySelector('.btn-primary')
190 | disableElement(postButton)
191 | }
192 | }
193 |
194 | const disableFollowButtons = (mutationsList) => {
195 | for (const mutation of mutationsList) {
196 | if (mutation.type === 'childList') {
197 | const node = mutation.target
198 | if ((node.innerText === 'Unfollow' || node.innerText === 'Follow')) {
199 | disableElement(node)
200 | }
201 | }
202 | }
203 | }
204 |
205 | const addMaskToAccountSelector = () => {
206 | const accountSelectorMaskIconId = '__clout-mask-account-selector-icon'
207 | if (document.getElementById(accountSelectorMaskIconId)) return
208 |
209 | const accountName = document.querySelector('.change-account-selector__ellipsis-restriction')
210 | if (!accountName) return
211 |
212 | const icon = createCloutMaskIconElement()
213 | icon.id = accountSelectorMaskIconId
214 | icon.classList.add('mr-2')
215 |
216 | accountName.innerHTML = `${icon.outerHTML} ${accountName.innerText}`
217 | }
218 |
219 | const addClearAllToAccountSelector = () => {
220 | const id = '__clout-mask-clear-all-item'
221 | if (document.getElementById(id)) return
222 |
223 | const accountsList = document.querySelector('.change-account-selector_list')
224 | if (!accountsList || accountsList.classList.contains('change-account-selector__hover')) return
225 |
226 | const div = document.createElement('div')
227 | div.id = id
228 | div.innerHTML = ` Clear Masked Accts
`
229 | div.onclick = removeAllMaskedUsers
230 |
231 | accountsList.appendChild(div)
232 | }
233 |
234 | const cloutMaskAppRootObserverCallback = (mutationsList) => {
235 | if (isLoggedInAsMaskedUser()) {
236 | disableFollowButtons(mutationsList)
237 | disableFeedPostButtons()
238 | disableKnownLinks()
239 | disableClasses()
240 | disablePostButtons()
241 | } else {
242 | reEnableElements()
243 | }
244 |
245 | if (getMaskedIdentityUsers().length > 0) {
246 | addClearAllToAccountSelector()
247 | }
248 |
249 | const profilePage = document.querySelector('creator-profile-page')
250 | if (profilePage) {
251 | addCloutMaskButton(profilePage)
252 | }
253 | }
254 |
255 | const cloutMaskBodyObserverCallback = () => {
256 | if (isLoggedInAsMaskedUser()) {
257 | addMaskToAccountSelector()
258 | }
259 | }
260 |
261 | const initCloutMask = () => {
262 | const appRoot = document.querySelector('app-root')
263 | if (appRoot) {
264 | const appRootObserverConfig = {childList: true, subtree: true}
265 | const appRootObserver = new MutationObserver(cloutMaskAppRootObserverCallback)
266 | appRootObserver.observe(appRoot, appRootObserverConfig)
267 | }
268 |
269 | const body = document.querySelector('body')
270 | if (body) {
271 | const bodyObserverConfig = { childList: true, subtree: false }
272 | const bodyObserver = new MutationObserver(cloutMaskBodyObserverCallback)
273 | bodyObserver.observe(body, bodyObserverConfig)
274 | }
275 | }
276 |
277 | initCloutMask()
278 |
--------------------------------------------------------------------------------
/lib/api.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) Paul Burke 2021
3 | Github: @ipaulpro/bitcloutplus
4 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
5 | */
6 |
7 | const MIN_FEE_RATE_NANOS_PER_KB = 1000
8 |
9 | let searchAbortController
10 |
11 | let apiBaseUrl
12 |
13 | const getBaseUrl = () => {
14 | if (!apiBaseUrl) {
15 | const node = window.localStorage.getItem('lastLocalNodeV2')
16 | if (!node) apiBaseUrl = `https://${window.location.hostname}`
17 | apiBaseUrl = `https://${node.replace(/['"]+/g, '')}`
18 | }
19 | return apiBaseUrl
20 | }
21 |
22 | const buildRequest = (credentials) => {
23 | return {
24 | 'headers': {
25 | 'accept': 'application/json, text/plain, */*',
26 | 'content-type': 'application/json',
27 | },
28 | 'referrerPolicy': 'no-referrer',
29 | 'method': 'POST',
30 | 'mode': 'cors',
31 | 'credentials': credentials
32 | }
33 | }
34 |
35 | const getUserMetadata = (publicKey) => {
36 | if (!publicKey) return Promise.reject('Missing required parameter publicKey')
37 |
38 | // get-user-metadata is not exposed on bitclout.com/api
39 | return fetch(`https://node.deso.org/api/v0/get-user-metadata/${publicKey}`)
40 | .then(res => res.json())
41 | }
42 |
43 | const getUsers = function (publicKeys) {
44 | if (!publicKeys) return Promise.reject('Missing required parameter publicKeys')
45 |
46 | const request = buildRequest('include')
47 | request.body = JSON.stringify({
48 | PublicKeysBase58Check: publicKeys,
49 | SkipForLeaderboard: true
50 | })
51 |
52 | return fetch(`${getBaseUrl()}/api/v0/get-users-stateless`, request)
53 | .then(res => res.json())
54 | .then(res => res['UserList'])
55 | }
56 |
57 | const getProfileByUsername = function (username) {
58 | if (!username) return Promise.reject('Missing required parameter username')
59 |
60 | const request = buildRequest('include')
61 | request.body = JSON.stringify({
62 | Username: username
63 | })
64 |
65 | return fetch(`${getBaseUrl()}/api/v0/get-single-profile`, request)
66 | .then(res => res.json())
67 | .then(res => res['Profile'])
68 | }
69 |
70 | const getProfileByPublicKey = function (publicKey) {
71 | if (!publicKey) return Promise.reject('Missing required parameter publicKey')
72 |
73 | const request = buildRequest('include')
74 | request.body = JSON.stringify({
75 | PublicKeyBase58Check: publicKey
76 | })
77 |
78 | return fetch(`${getBaseUrl()}/api/v0/get-single-profile`, request)
79 | .then(res => res.json())
80 | .then(res => res['Profile'])
81 | }
82 |
83 | const getFollowersByPublicKey = function (pubKey) {
84 | if (!pubKey) return Promise.reject('Missing required parameter pubKey')
85 |
86 | const request = buildRequest('include')
87 | request.body = JSON.stringify({
88 | PublicKeyBase58Check: pubKey,
89 | GetEntriesFollowingUsername: true,
90 | NumToFetch: 20000
91 | })
92 |
93 | return fetch(`${getBaseUrl()}/api/v0/get-follows-stateless`, request)
94 | .then(res => res.json())
95 | }
96 |
97 | const getFollowingByPublicKey = function (pubKey) {
98 | if (!pubKey) return Promise.reject('Missing required parameter pubKey')
99 |
100 | const request = buildRequest('include')
101 | request.body = JSON.stringify({
102 | PublicKeyBase58Check: pubKey,
103 | GetEntriesFollowingUsername: false,
104 | NumToFetch: 20000
105 | })
106 |
107 | return fetch(`${getBaseUrl()}/api/v0/get-follows-stateless`, request)
108 | .then(res => res.json())
109 | }
110 |
111 | const getHodlersByUsername = function (username) {
112 | if (!username) return Promise.reject('Missing required parameter username')
113 |
114 | const readerPubKey = getLoggedInPublicKey()
115 | if (!readerPubKey) return Promise.reject('No logged in user found')
116 |
117 | const request = buildRequest('omit')
118 | request.body = JSON.stringify({
119 | ReaderPublicKeyBase58Check: readerPubKey,
120 | username: username,
121 | NumToFetch: 10000
122 | })
123 |
124 | return fetch(`${getBaseUrl()}/api/v0/get-hodlers-for-public-key`, request)
125 | .then(res => res.json())
126 | .then(res => res['Hodlers'])
127 | }
128 |
129 | const submitTransaction = (transactionHex) => {
130 | if (!transactionHex) return Promise.reject('Missing required parameter transactionHex')
131 |
132 | const request = buildRequest('omit')
133 | request.body = JSON.stringify({
134 | TransactionHex: transactionHex
135 | })
136 |
137 | return fetch(`${getBaseUrl()}/api/v0/submit-transaction`, request)
138 | .then(res => res.json())
139 | }
140 |
141 | const submitPost = (
142 | pubKey,
143 | bodyText,
144 | images,
145 | videos,
146 | embedUrl,
147 | extraData,
148 | postHashHexToModify,
149 | repostedPostHashHex,
150 | parentStakeId
151 | ) => {
152 | const bodyObj = {
153 | Body: bodyText
154 | }
155 |
156 | if (images) bodyObj.ImageURLs = images
157 |
158 | if (videos) bodyObj.VideoURLs = videos
159 |
160 | const body = {
161 | UpdaterPublicKeyBase58Check: pubKey,
162 | BodyObj: bodyObj,
163 | CreatorBasisPoints: 0,
164 | StakeMultipleBasisPoints: 12500,
165 | IsHidden: false,
166 | MinFeeRateNanosPerKB: MIN_FEE_RATE_NANOS_PER_KB,
167 | PostExtraData: extraData
168 | }
169 |
170 | if (postHashHexToModify) {
171 | body['PostHashHexToModify'] = postHashHexToModify
172 | }
173 |
174 | if (repostedPostHashHex) {
175 | body['RepostedPostHashHex'] = repostedPostHashHex
176 | }
177 |
178 | if (parentStakeId) {
179 | body['ParentStakeID'] = parentStakeId
180 | }
181 |
182 | if (embedUrl) {
183 | const formattedEmbedUrl = getEmbedURL(embedUrl)
184 | if (formattedEmbedUrl) {
185 | if (!body.PostExtraData) body.PostExtraData = {}
186 | body.PostExtraData.EmbedVideoURL = formattedEmbedUrl
187 | } else {
188 | bodyObj.ImageURLs = [embedUrl]
189 | }
190 | }
191 |
192 | if (!extraData['Node']) {
193 | switch (window.location.hostname) {
194 | case 'node.deso.org':
195 | extraData['Node'] = '1'
196 | break;
197 | case 'bitclout.com':
198 | extraData['Node'] = '2'
199 | break;
200 | default:
201 | extraData['Node'] = '0'
202 | break;
203 | }
204 | }
205 |
206 | const request = buildRequest('omit')
207 | request.body = JSON.stringify(body)
208 |
209 | return fetch(`${getBaseUrl()}/api/v0/submit-post`, request)
210 | .then(res => res.json())
211 | .then(res => {
212 | if (res['TransactionHex']) {
213 | return res['TransactionHex']
214 | }
215 | throw new Error(res['error'])
216 | })
217 | }
218 |
219 | const searchUsernames = function (query, cb) {
220 | if (searchAbortController) {
221 | searchAbortController.abort()
222 | }
223 |
224 | const request = buildRequest('omit')
225 | request.body = JSON.stringify({
226 | UsernamePrefix: query,
227 | NumToFetch: 4
228 | })
229 |
230 | searchAbortController = new AbortController()
231 | const { signal } = searchAbortController
232 | request.signal = signal
233 |
234 | return fetch(`${getBaseUrl()}/api/v0/get-profiles`, request)
235 | .then(res => res.json())
236 | .then(res => { cb(res['ProfilesFound']) })
237 | .catch(() => {})
238 | }
239 |
240 | const getProfilePhotoUrlForPublicKey = (pubKey) => {
241 | return `${getBaseUrl()}/api/v0/get-single-profile-picture/${pubKey}?fallback=https://${window.location.hostname}/assets/img/default_profile_pic.png`
242 | }
243 |
244 | const isHoldingPublicKey = (publicKey, isHoldingPublicKey) => {
245 | if (!publicKey || !isHoldingPublicKey) return Promise.reject('Missing required parameter')
246 |
247 | const request = buildRequest('omit')
248 | request.body = JSON.stringify({
249 | PublicKeyBase58Check: publicKey,
250 | IsHodlingPublicKeyBase58Check: isHoldingPublicKey
251 | })
252 |
253 | return fetch(`${getBaseUrl()}/api/v0/is-hodling-public-key`, request)
254 | .then(res => res.json())
255 | }
256 |
257 | const isFollowingPublicKey = (publicKey, isFollowingPublicKey) => {
258 | if (!publicKey || !isFollowingPublicKey) return Promise.reject('Missing required parameter')
259 |
260 | const request = buildRequest('omit')
261 | request.body = JSON.stringify({
262 | PublicKeyBase58Check: publicKey,
263 | IsFollowingPublicKeyBase58Check: isFollowingPublicKey
264 | })
265 |
266 | return fetch(`${getBaseUrl()}/api/v0/is-following-public-key`, request)
267 | .then(res => res.json())
268 | }
269 |
270 | const getSinglePost = (postHashHex, fetchParents = false, commentLimit = 0) => {
271 | if (!postHashHex) return Promise.reject('Missing required parameter')
272 |
273 | const request = buildRequest('omit')
274 | request.body = JSON.stringify({
275 | PostHashHex: postHashHex,
276 | ReaderPublicKeyBase58Check: getLoggedInPublicKey(),
277 | FetchParents: fetchParents,
278 | CommentLimit: commentLimit
279 | })
280 |
281 | return fetch(`${getBaseUrl()}/api/v0/get-single-post`, request)
282 | .then(res => res.json())
283 | }
284 |
285 | const getBidsForNftPost = (publicKey, postHashHex) => {
286 | if (!publicKey || !isFollowingPublicKey) return Promise.reject('Missing required parameter')
287 |
288 | const request = buildRequest('omit')
289 | request.body = JSON.stringify({
290 | ReaderPublicKeyBase58Check: publicKey,
291 | PostHashHex: postHashHex
292 | })
293 |
294 | return fetch(`${getBaseUrl()}/api/v0/get-nft-bids-for-nft-post`, request)
295 | .then(res => res.json())
296 | }
297 |
298 | const getNftEntriesForPostHashHex = (readerPublicKey, postHashHex) => {
299 | if (!readerPublicKey || !isFollowingPublicKey) return Promise.reject('Missing required parameter')
300 |
301 | const request = buildRequest('omit')
302 | request.body = JSON.stringify({
303 | ReaderPublicKeyBase58Check: readerPublicKey,
304 | PostHashHex: postHashHex
305 | })
306 |
307 | return fetch(`${getBaseUrl()}/api/v0/get-nft-entries-for-nft-post`, request)
308 | .then(res => res.json())
309 | .then(res => res['NFTEntryResponses'])
310 | }
311 |
312 | const transferNft = (senderPublicKey, receiverPublicKey, nftPostHashHex, serialNumber, encryptedUnlockableText) => {
313 | if (!senderPublicKey || !nftPostHashHex || !serialNumber) return Promise.reject('Missing required parameter')
314 |
315 | const request = buildRequest('omit')
316 | request.body = JSON.stringify({
317 | SenderPublicKeyBase58Check: senderPublicKey,
318 | ReceiverPublicKeyBase58Check: receiverPublicKey,
319 | NFTPostHashHex: nftPostHashHex,
320 | SerialNumber: serialNumber,
321 | EncryptedUnlockableText: encryptedUnlockableText,
322 | MinFeeRateNanosPerKB: MIN_FEE_RATE_NANOS_PER_KB
323 | })
324 |
325 | return fetch(`${getBaseUrl()}/api/v0/transfer-nft`, request)
326 | .then(res => res.json())
327 | }
328 |
329 | const getTransactionInfo = (publicKey, lastPublicKeyTransactionIndex = -1, limit = 1) => {
330 | if (!publicKey) return Promise.reject('Missing required parameter')
331 |
332 | const request = buildRequest('omit')
333 | request.body = JSON.stringify({
334 | PublicKeyBase58Check: publicKey,
335 | Limit: limit,
336 | LastPublicKeyTransactionIndex: lastPublicKeyTransactionIndex
337 | })
338 |
339 | return fetch(`${getBaseUrl()}/api/v1/transaction-info`, request)
340 | .then(res => res.json())
341 | .then(res => res['Transactions'])
342 | }
343 |
344 | const getAppState = () => {
345 | const request = buildRequest('omit')
346 | request.body = '{}'
347 |
348 | return fetch(`${getBaseUrl()}/api/v0/get-app-state`, request).then(res => res.json())
349 | }
350 |
351 | const getBlockByHash = (blockHashHex) => {
352 | if (!blockHashHex) return Promise.reject('Missing required parameter')
353 |
354 | const request = buildRequest('omit')
355 | request.body = JSON.stringify({
356 | HashHex: blockHashHex
357 | })
358 |
359 | return fetch(`${getBaseUrl()}/api/v1/block`, request)
360 | .then(res => res.json())
361 | }
362 |
363 | const getBlockByHeight = (blockHeight) => {
364 | if (!blockHeight) return Promise.reject('Missing required parameter')
365 |
366 | const request = buildRequest('omit')
367 | request.body = JSON.stringify({
368 | FullBlock: true,
369 | Height: blockHeight
370 | })
371 |
372 | return fetch(`${getBaseUrl()}/api/v1/block`, request)
373 | .then(res => res.json())
374 | }
375 |
376 | const getUnreadNotificationsCount = (publicKey) => {
377 | const request = buildRequest('omit')
378 | request.body = JSON.stringify({
379 | PublicKeyBase58Check: publicKey
380 | })
381 |
382 | return fetch(`${getBaseUrl()}/api/v0/get-unread-notifications-count`, request)
383 | .then(res => res.json())
384 | }
385 |
386 | const setNotificationMetadata = (metadata) => {
387 | const request = buildRequest('omit')
388 | request.body = JSON.stringify(metadata)
389 |
390 | return fetch(`${getBaseUrl()}/api/v0/set-notification-metadata`, request)
391 | }
392 |
393 | const blockPublicKey = (pendingBlockUser, jwt) => {
394 | const request = buildRequest('omit')
395 | request.body = JSON.stringify({
396 | ...pendingBlockUser,
397 | JWT: jwt
398 | })
399 | return fetch(`${getBaseUrl()}/api/v0/block-public-key`, request)
400 | .then(res => res.json())
401 | }
--------------------------------------------------------------------------------
/lib/post.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) Paul Burke 2021
3 | Github: @ipaulpro/bitcloutplus
4 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
5 | */
6 |
7 | const maxPostLength = 15000
8 | const postButtonClass = 'plus-btn-submit-post'
9 |
10 | const extraDataTextAreaId = 'plus_create-post-extra-data'
11 | const extraDataHelpTextId = 'plus_create-post-extra-data-help'
12 | const editPostId = 'plus_edit-post-btn'
13 |
14 | const addNewPostButton = function () {
15 | let addPostButtonId = 'plus-add-new-post'
16 | if (document.getElementById(addPostButtonId)) return
17 |
18 | const globalNavElements = document.getElementsByClassName('global__nav__inner')
19 | try {
20 | const globalNav = globalNavElements.item(0)
21 |
22 | const button = document.createElement('button');
23 | button.id = addPostButtonId
24 | button.type = 'button'
25 | button.className = 'btn btn-secondary font-weight-bold fs-14px ml-3'
26 | button.innerText = 'Create Post'
27 | button.onclick = () => window.location.href = 'posts/new'
28 |
29 | const div = document.createElement('div')
30 | div.className = 'w-100 d-flex pt-3 pl-4 pr-2 pb-4'
31 | div.appendChild(button)
32 |
33 | globalNav.appendChild(div)
34 | } catch (e) {}
35 | }
36 |
37 | const buildTributeUsernameMenuTemplate = item => {
38 | const spotPrice = getSpotPrice()
39 | const bitcloutPrice = item.original['CoinPriceDeSoNanos'] / deSoInNanos
40 |
41 | const priceDiv = document.createElement('div')
42 | priceDiv.className = 'plus-text-muted fs-12px'
43 | priceDiv.innerText = `${dollarFormatter.format(spotPrice * bitcloutPrice)}`
44 |
45 | const verifiedIcon = document.createElement('i')
46 | verifiedIcon.className = 'fas fa-check-circle fa-md ml-1 plus-text-primary'
47 |
48 | const reservedIcon = document.createElement('i')
49 | reservedIcon.className = 'far fa-clock fa-md ml-1 plus-text-muted'
50 |
51 | let icon
52 | if (item.original['IsVerified']) {
53 | icon = verifiedIcon
54 | } else if (item.original['IsReserved']) {
55 | icon = reservedIcon
56 | }
57 |
58 | const usernameSpan = document.createElement('span')
59 | usernameSpan.innerText = item.original['Username']
60 | if (icon) usernameSpan.appendChild(icon)
61 |
62 | const nameDiv = document.createElement('div')
63 | nameDiv.className = 'ml-1 pl-1'
64 | nameDiv.appendChild(usernameSpan)
65 | nameDiv.appendChild(priceDiv)
66 |
67 | const pubKey = item.original['PublicKeyBase58Check']
68 | const img = document.createElement('img')
69 | img.className = 'tribute-avatar'
70 | img.src = getProfilePhotoUrlForPublicKey(pubKey)
71 |
72 | const row = document.createElement('div')
73 | row.className = 'row no-gutters'
74 | row.appendChild(img)
75 | row.appendChild(nameDiv)
76 |
77 | return row.outerHTML
78 | }
79 |
80 | function buildLoadingItemTemplate () {
81 | return `Loading...
`
82 | }
83 |
84 | const addPostUsernameAutocomplete = function () {
85 | const createPostInputs = document.getElementsByClassName('cdk-textarea-autosize')
86 | for (let input of createPostInputs) {
87 | if (input.dataset && !input.dataset.tribute) {
88 | const tribute = new Tribute({
89 | values: (text, cb) => searchUsernames(text, users => cb(users)),
90 | menuItemTemplate: (item) => buildTributeUsernameMenuTemplate(item),
91 | loadingItemTemplate: buildLoadingItemTemplate(),
92 | fillAttr: 'Username',
93 | lookup: 'Username'
94 | })
95 | tribute.attach(input)
96 | }
97 | }
98 | }
99 |
100 | const restorePostDraft = () => {
101 | chrome.storage.local.get(['postDraft'], items => {
102 | const postDraft = items.postDraft
103 | if (postDraft) {
104 | const createPostTextArea = document.querySelector('.feed-create-post__textarea')
105 | if (createPostTextArea) {
106 | createPostTextArea.value = postDraft
107 | chrome.storage.local.remove(['postDraft'])
108 | }
109 | }
110 | })
111 | }
112 |
113 | const getPostButton = (container) => {
114 | const plusButton = container.querySelector(`.${postButtonClass}`)
115 | if (plusButton) return plusButton
116 |
117 | const primaryButtons = container.querySelectorAll('.btn-primary')
118 | let postButton
119 | for (let primaryButton of primaryButtons) {
120 | if (primaryButton.innerText.includes('Post')) {
121 | postButton = primaryButton
122 | break
123 | }
124 | }
125 | return postButton
126 | }
127 |
128 | const disableLongPost = () => {
129 | const container = document.querySelector('feed-create-post')
130 | if (!container) return
131 |
132 | const postTextArea = container.querySelector('textarea')
133 | if (!postTextArea) return
134 |
135 | chrome.storage.local.set({
136 | longPost: false,
137 | postDraft: postTextArea.value
138 | })
139 | window.location.reload(true)
140 | }
141 |
142 | const addPostErrorDiv = (e, container) => {
143 | const btn = document.createElement('button')
144 | btn.className = 'btn btn-danger btn-sm mt-2'
145 | btn.innerText = 'Disable post enhancements'
146 | btn.onclick = () => disableLongPost()
147 |
148 | const textarea = document.createElement('textarea')
149 | textarea.className = 'w-100'
150 | textarea.rows = 6
151 | textarea.innerText = `${(e.stack || e)}`
152 |
153 | const span = document.createElement('span')
154 | span.innerText = 'Trouble posting? Disabling post enhancements may help.'
155 |
156 | const a = document.createElement('a')
157 | a.href = '/u/plus'
158 | a.innerText = '@plus'
159 |
160 | const contact = document.createElement('span')
161 | contact.className = 'd-block my-2'
162 | contact.innerText = 'Please report this to '
163 | contact.appendChild(a)
164 |
165 | const p = document.createElement('p')
166 | p.className = 'plus-text-muted fs-14px'
167 | p.appendChild(span)
168 | p.appendChild(contact)
169 | p.appendChild(textarea)
170 |
171 | const div = document.createElement('div')
172 | div.className = 'p-2'
173 |
174 | div.appendChild(p)
175 | div.appendChild(btn)
176 | container.appendChild(div)
177 | }
178 |
179 | const onPostButtonClick = (postButton) => {
180 | if (!postButton) return
181 |
182 | const restoreButton = () => {
183 | postButton.classList.remove('disabled')
184 | postButton.innerText = 'Post'
185 | }
186 |
187 | const container = document.querySelector('feed-create-post')
188 | if (!container) return
189 |
190 | const postTextArea = container.querySelector('textarea')
191 | if (!postTextArea) return
192 |
193 | const postBody = postTextArea.value
194 | if (!postBody) return
195 |
196 | postButton.classList.add('disabled')
197 |
198 | const spinnerAlt = document.createElement('span')
199 | spinnerAlt.className = 'sr-only'
200 | spinnerAlt.innerText = 'Working...'
201 |
202 | const spinner = document.createElement('div')
203 | spinner.className = 'spinner-border spinner-border-sm text-light'
204 | spinner.dataset.role = 'status'
205 | spinner.appendChild(spinnerAlt)
206 |
207 | postButton.innerText = ''
208 | postButton.appendChild(spinner)
209 |
210 | const onPostSubmitted = (transactionHex) => {
211 | if (!transactionHex) {
212 | return Promise.reject('Error creating submit-post transaction')
213 | }
214 |
215 | const identity = getCurrentIdentity()
216 | if (!identity) {
217 | return Promise.reject('No Identity found')
218 | }
219 |
220 | const id = uuid()
221 | sendSignTransactionMsg(identity, transactionHex, id)
222 | }
223 |
224 | const pubKey = getLoggedInPublicKey()
225 |
226 | const params = new URLSearchParams(window.location.search)
227 | const postHashHexToModify = params.get('edit')
228 | if (postHashHexToModify) {
229 | getSinglePost(postHashHexToModify)
230 | .then(data => data['PostFound'])
231 | .then(post => {
232 | if (pubKey !== post['PosterPublicKeyBase58Check']) return Promise.reject('Cannot edit a post you did not create')
233 | const image = post['ImageURLs']
234 | const video = post['VideoURLs']
235 | const extraData = post['PostExtraData']
236 | if (extraData['DerivedPublicKey']) delete extraData['DerivedPublicKey']
237 | const repostedPostHashHex = post['RepostedPostEntryResponse'] ? post['RepostedPostEntryResponse']['PostHashHex'] : ''
238 | const parentStakeID = post['ParentStakeID']
239 | return submitPost(pubKey, postBody, image, video, null, extraData, postHashHexToModify, repostedPostHashHex, parentStakeID)
240 | })
241 | .then(onPostSubmitted)
242 | .catch(e => {
243 | addPostErrorDiv(e, container)
244 | restoreButton()
245 | })
246 | return
247 | }
248 |
249 | const postImage = container.getElementsByClassName('feed-post__image').item(0)
250 | const hasImage = postImage && postImage.src &&
251 | (postImage.src.includes(`images.${window.location.hostname}`) || postImage.src.includes(`images.deso.org`))
252 | const image = hasImage ? postImage.src : undefined
253 |
254 | const postEmbed = container.querySelector('input[type="url"]')
255 | const embedUrl = postEmbed ? postEmbed.value : undefined
256 |
257 | const video = container.querySelector('.feed-post__video-container')?.firstElementChild?.src
258 |
259 | let extraData = {}
260 | const extraDataTextArea = document.getElementById(extraDataTextAreaId)
261 | if (extraDataTextArea) {
262 | const extraDataTextAreaValue = extraDataTextArea.value
263 | if (extraDataTextAreaValue && extraDataTextAreaValue.length > 0) {
264 | try {
265 | extraData = JSON.parse(extraDataTextAreaValue)
266 | } catch (e) {
267 | Swal.fire({
268 | title: 'Error',
269 | text: 'Invalid Extra Text JSON.'
270 | })
271 | restoreButton()
272 | return
273 | }
274 | }
275 | }
276 |
277 | submitPost(pubKey, postBody, [image], [video], embedUrl, extraData)
278 | .then(onPostSubmitted)
279 | .catch(e => {
280 | addPostErrorDiv(e, container)
281 | restoreButton()
282 | })
283 | }
284 |
285 | const replacePostBtn = () => {
286 | if (!longPostEnabled || document.querySelector(`.${postButtonClass}`)) return
287 |
288 | const form = document.querySelector('create-post-form') || document.querySelector('feed')
289 | const container = form && form.querySelector('feed-create-post')
290 | if (!container) return
291 |
292 | const postButton = getPostButton(container)
293 | if (!postButton) return
294 |
295 | const newButton = postButton.cloneNode(true)
296 | newButton.classList.add(postButtonClass)
297 |
298 | postButton.style.display = 'none'
299 |
300 | const parent = postButton.parentElement
301 | parent.appendChild(newButton)
302 |
303 | newButton.onclick = () => onPostButtonClick(newButton)
304 | }
305 |
306 | function onPostTextAreaInputChange() {
307 | const container = document.querySelector('feed-create-post')
308 | if (!container) return
309 |
310 | const characterCounter = container.querySelector('.feed-create-post__character-counter')
311 | if (!characterCounter) return
312 |
313 | const postTextArea = container.querySelector('textarea')
314 | if (!postTextArea) return
315 |
316 | const characterCount = postTextArea.value.length
317 |
318 | const postButton = getPostButton(container)
319 | if (characterCount > 0) {
320 | postButton.classList.remove('disabled')
321 | } else {
322 | postButton.classList.add('disabled')
323 | }
324 |
325 | if (!characterCounter) return
326 | characterCounter.innerText = `${characterCount} / ${maxPostLength}`
327 | if (characterCount > maxPostLength) {
328 | characterCounter.classList.add('plus-text-red')
329 | characterCounter.classList.remove('text-grey8A')
330 | characterCounter.classList.remove('text-warning')
331 | } else if (characterCount > 280) {
332 | characterCounter.classList.remove('plus-text-red')
333 | characterCounter.classList.remove('text-grey8A')
334 | characterCounter.classList.add('text-warning')
335 | } else {
336 | characterCounter.classList.remove('plus-text-red')
337 | characterCounter.classList.add('text-grey8A')
338 | characterCounter.classList.remove('text-warning')
339 | }
340 | }
341 |
342 | const addPostTextAreaListener = () => {
343 | if (!longPostEnabled) return
344 |
345 | const container = document.querySelector('feed-create-post')
346 | if (!container) return
347 |
348 | const postTextArea = container.querySelector('textarea')
349 | if (!postTextArea) return
350 |
351 | postTextArea.addEventListener('input', () => {
352 | onPostTextAreaInputChange(postTextArea)
353 | })
354 | }
355 |
356 | const enrichCreatePostPage = (page) => {
357 | if (!page) return
358 |
359 | const feedCreatePost = page.querySelector('feed-create-post')
360 | if (!feedCreatePost) return
361 |
362 | const postEmbedContainer = feedCreatePost.lastElementChild
363 | if (!postEmbedContainer) return
364 |
365 | const icons = postEmbedContainer.querySelectorAll('i')
366 | const firstIcon = icons.length > 0 ? icons[0] : null
367 | if (!firstIcon) return
368 |
369 | const textAreaContainerId = 'plus_create-post-extra-data-box'
370 |
371 | const icon = document.createElement('i')
372 | icon.className = 'fas fa-sitemap fa-lg text-grey8A cursor-pointer fs-18px pr-15px'
373 | icon.onclick = () => {
374 | const textArea = document.getElementById(textAreaContainerId)
375 | if (!textArea) return
376 |
377 | if (textArea.classList.contains('d-none')) {
378 | textArea.classList.remove('d-none')
379 | } else {
380 | textArea.classList.add('d-none')
381 | }
382 | }
383 | postEmbedContainer.insertBefore(icon, firstIcon)
384 |
385 | const textarea = document.createElement('textarea')
386 | textarea.id = extraDataTextAreaId
387 | textarea.rows = 5
388 | textarea.spellcheck = false
389 | textarea.className = 'form-control fs-14px'
390 | textarea.placeholder = '{\n "key": "value" // Values may only be strings\n}'
391 | textarea.setAttribute('aria-describedby', extraDataHelpTextId)
392 |
393 | tabOverride.tabSize(4)
394 | tabOverride.autoIndent(true)
395 | tabOverride.set(textarea)
396 |
397 | const label = document.createElement('label')
398 | label.setAttribute('for', extraDataTextAreaId)
399 | label.innerText = 'Extra Data'
400 |
401 | const helpText = document.createElement('small')
402 | helpText.id = extraDataHelpTextId
403 | helpText.className = 'form-text text-muted'
404 | helpText.innerText = 'Extra data allows arbitrary text to be attached to the post, in JSON format. This is useful for things like on-chain NFT attributes.'
405 |
406 | const textAreaContainer = document.createElement('div')
407 | textAreaContainer.id = textAreaContainerId
408 | textAreaContainer.className = 'p-3 d-none'
409 | textAreaContainer.appendChild(label)
410 | textAreaContainer.appendChild(textarea)
411 | textAreaContainer.appendChild(helpText)
412 |
413 | feedCreatePost.insertBefore(textAreaContainer, postEmbedContainer)
414 | }
415 |
416 | const showEditPostButtonIfNeeded = () => {
417 | if (document.getElementById(editPostId)) return
418 | let postHashHex = getPostHashHexFromUrl()
419 | getSinglePost(postHashHex)
420 | .then(data => data['PostFound'])
421 | .then(post => {
422 | if (document.getElementById(editPostId)) return
423 | if (post['PosterPublicKeyBase58Check'] === getLoggedInPublicKey()) {
424 | const hasParent = post['ParentStakeID'].length > 0
425 | const a = document.createElement('a')
426 | a.id = editPostId
427 | a.className = 'fs-14px'
428 | a.href = `/posts/new?edit=${postHashHex}`
429 | a.innerText = hasParent ? 'Edit Comment' : 'Edit Post'
430 | const topBar = document.querySelector(`.global__top-bar`)
431 | topBar.appendChild(a)
432 | }
433 | })
434 | .catch(console.error)
435 | }
436 |
437 | const checkForEditPostQueryParams = (page) => {
438 | const params = new URLSearchParams(window.location.search)
439 | const postHashHex = params.get('edit')
440 | if (postHashHex) {
441 | getSinglePost(postHashHex)
442 | .then(data => data['PostFound'])
443 | .then(post => {
444 | if (post['PosterPublicKeyBase58Check'] === getLoggedInPublicKey()) {
445 | const topBar = page.querySelector('create-post-form')?.firstElementChild
446 | const hasParent = post['ParentStakeID'].length > 0
447 | if (topBar && topBar.firstElementChild) {
448 | topBar.firstElementChild.innerText = hasParent ? ' Edit comment ' : ' Edit post '
449 | }
450 |
451 | const createPostDiv = page.querySelector('feed-create-post')
452 | const textArea = createPostDiv?.querySelector('textarea')
453 | if (textArea) {
454 | textArea.value = post['Body']
455 | onPostTextAreaInputChange()
456 |
457 | const postAttachmentButtons = createPostDiv.lastElementChild.querySelectorAll('i')
458 | postAttachmentButtons.forEach(button => {
459 | button.remove()
460 | })
461 |
462 | const postButton = document.getElementsByClassName(postButtonClass)[0]
463 | postButton.innerText = 'Update'
464 |
465 | const text = document.createElement('span')
466 | text.className = 'flex-grow-1 fs-12px text-muted px-3'
467 | text.innerText = 'Attachments and extra data will remain unchanged'
468 | const container = createPostDiv.lastElementChild
469 | container.insertBefore(text, container.firstElementChild)
470 | }
471 | }
472 | })
473 | .catch(console.error)
474 | }
475 | }
--------------------------------------------------------------------------------
/lib/profile.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) Paul Burke 2021
3 | Github: @ipaulpro/bitcloutplus
4 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
5 | */
6 |
7 | const openInNewTab = url => {
8 | window.open(url, '_blank').focus()
9 | }
10 |
11 | function fixImageLightbox(modalContainer) {
12 | const feedPostImage = modalContainer.querySelector('feed-post-image-modal')
13 | if (feedPostImage) {
14 | const content = modalContainer.querySelector('.modal-content')
15 | content.style.width = 'auto'
16 | content.style.margin = '0 auto'
17 |
18 | const dialog = modalContainer.querySelector('.modal-dialog')
19 | dialog.style.maxWidth = '1140px'
20 | }
21 | }
22 |
23 | const createMenuItem = (id, iconClassName, title) => {
24 | const icon = document.createElement('i')
25 | icon.className = `fas ${iconClassName}`
26 |
27 | const text = document.createElement('span')
28 | text.innerText = ` ${title}`
29 |
30 | const a = document.createElement('a')
31 | a.id = id
32 | a.className = 'dropdown-menu-item d-block p-10px feed-post__dropdown-menu-item fc-default'
33 |
34 | a.appendChild(icon)
35 | a.appendChild(text)
36 |
37 | return a
38 | }
39 |
40 | const addSendDeSoMenuItem = function (menu) {
41 | if (!menu) return
42 |
43 | let sendDeSoId = 'plus-profile-menu-send-deso'
44 | if (document.getElementById(sendDeSoId)) return
45 |
46 |
47 | try {
48 | const a = createMenuItem(sendDeSoId, 'fa-hand-holding-usd', 'Send $DESO')
49 | const publicKey = getPublicKeyFromPage()
50 | a.onclick = () => window.location.href = `send-deso?public_key=${publicKey}`
51 | menu.insertBefore(a, menu.firstElementChild)
52 | } catch (e) {}
53 | }
54 |
55 | const addInsightsMenuItem = function (menu) {
56 | if (!menu) return
57 |
58 | let sendMessageId = 'plus-profile-menu-insights'
59 | if (document.getElementById(sendMessageId)) return
60 |
61 | try {
62 | const a = createMenuItem(sendMessageId, 'fa-chart-bar', 'Insights')
63 | const username = getUsernameFromUrl()
64 | a.onclick = () => openInNewTab(`https://openprosper.com/u/${username}`)
65 | menu.insertBefore(a, menu.firstElementChild)
66 | } catch (e) {}
67 | }
68 |
69 | const addGeoMenuItem = function (menu) {
70 | if (!menu) return
71 |
72 | let walletId = 'plus-profile-menu-geo'
73 | if (document.getElementById(walletId)) return
74 |
75 | try {
76 | const a = createMenuItem(walletId, 'fa-map-marker-alt', 'View Location')
77 | const username = getUsernameFromUrl()
78 | a.onclick = () => openInNewTab(`https://desogeo.com/map?account=${username}`)
79 | menu.insertBefore(a, menu.firstElementChild)
80 | } catch (e) {}
81 | }
82 |
83 | const addNftMenuItem = function (menu) {
84 | if (!menu) return
85 |
86 | let walletId = 'plus-profile-menu-nft'
87 | if (document.getElementById(walletId)) return
88 |
89 | try {
90 | const a = createMenuItem(walletId, 'fa-store', 'Explore NFTs')
91 | const username = getUsernameFromUrl()
92 | a.onclick = () => openInNewTab(`https://${username}.nftz.zone`)
93 | menu.insertBefore(a, menu.firstElementChild)
94 | } catch (e) {}
95 | }
96 |
97 | const getProfileMenu = function () {
98 | const dropdownContainer = document.querySelector('bs-dropdown-container')
99 | if (!dropdownContainer) return undefined
100 |
101 | const menu = dropdownContainer.getElementsByClassName('dropdown-menu')[0]
102 | if (menu.firstElementChild.innerText.includes("Message User")) {
103 | return menu
104 | }
105 | return undefined
106 | }
107 |
108 | const enrichProfile = function () {
109 | let profileDetails = document.querySelector('creator-profile-details')
110 | if (!profileDetails) return
111 |
112 | const profileMenu = getProfileMenu()
113 | addGeoMenuItem(profileMenu)
114 | addNftMenuItem(profileMenu)
115 | addSendDeSoMenuItem(profileMenu)
116 | addInsightsMenuItem(profileMenu)
117 | }
118 |
119 | const addNativeCoinPriceToProfileHeader = (userDataDiv, profile) => {
120 | const nativePriceId = 'plus-profile-native-price'
121 |
122 | if (!userDataDiv || !profile || document.getElementById(nativePriceId)) return
123 |
124 | const priceContainerDiv = userDataDiv.children.item(1)
125 | if (!priceContainerDiv) return
126 |
127 | const priceDiv = priceContainerDiv.firstElementChild
128 |
129 | const coinPriceNanos = profile['CoinPriceDeSoNanos']
130 | const nativePrice = (coinPriceNanos / deSoInNanos).toFixed(2)
131 |
132 | const tooltipAttr = document.createAttribute('data-bs-toggle')
133 | tooltipAttr.value = 'tooltip'
134 |
135 | let span = document.createElement('span')
136 | span.id = nativePriceId
137 | span.className = 'plus-text-muted mr-2 fs-14px'
138 | span.style.fontWeight = '500'
139 | span.innerText = `(${nativePrice} $DESO)`
140 | span.setAttributeNode(tooltipAttr)
141 |
142 | priceDiv.insertBefore(span, priceDiv.lastChild)
143 | }
144 |
145 | const addSellButton = function () {
146 | const sellButtonId = 'plus-profile-sell-btn'
147 | if (document.getElementById(sellButtonId)) return
148 |
149 | let topCardContainerElements = document.getElementsByClassName('js-creator-profile-top-card-container')
150 | try {
151 | if (topCardContainerElements.length > 0) {
152 | const topCardContainer = topCardContainerElements.item(0)
153 | if (topCardContainer) {
154 | let sellButton = document.createElement('a')
155 | sellButton.id = sellButtonId
156 | sellButton.href = document.location.pathname + '/sell'
157 | sellButton.innerText = 'Sell'
158 | sellButton.className = 'btn btn-secondary font-weight-bold ml-10px fs-14px'
159 | sellButton.style.width = '75px'
160 | sellButton.style.height = '36px'
161 | topCardContainerElements.item(0).appendChild(sellButton)
162 | }
163 | }
164 | } catch (e) {}
165 | }
166 |
167 | const addHoldersCount = function (holderCount) {
168 | let profileDetails = document.querySelector('creator-profile-details')
169 | if (!profileDetails) return
170 |
171 | const contentTop = profileDetails.firstElementChild
172 | if (!contentTop) return
173 |
174 | const tabContent = contentTop.lastElementChild
175 | if (!tabContent) return
176 |
177 | const creatorCoinTabHeader = tabContent.firstElementChild
178 | if (!creatorCoinTabHeader) return
179 |
180 | const holderDiv = creatorCoinTabHeader.firstElementChild
181 | if (!holderDiv || !holderDiv.innerText.includes('Holders of')) return
182 |
183 | const holderCountId = 'plus-profile-holder-count'
184 |
185 | let span
186 | const existingSpan = document.getElementById(holderCountId)
187 | if (existingSpan) {
188 | span = existingSpan
189 | } else {
190 | span = document.createElement('span')
191 | span.id = holderCountId
192 | span.className = 'fc-muted fs-16px'
193 | holderDiv.appendChild(span)
194 | }
195 | span.innerText = `(${holderCount})`
196 | }
197 |
198 | function addHolderPositionRank (node, index, userHoldsOwnCoin) {
199 | if (userHoldsOwnCoin && index === 0) return
200 |
201 | node.querySelector('.text-truncate').style.maxWidth = '160px !important'
202 |
203 | const itemId = 'plus-profile-holder-position-' + index
204 | const holderPositionClassName = 'plus-profile-holder-position'
205 |
206 | let i
207 | if (userHoldsOwnCoin) {
208 | i = index
209 | } else {
210 | i = index + 1
211 | }
212 |
213 | try {
214 | let span
215 | const existingSpan = document.getElementById(itemId)
216 | if (existingSpan) {
217 | span = existingSpan
218 | } else {
219 | span = document.createElement('span')
220 | span.id = itemId
221 | span.className = `${holderPositionClassName} fc-muted fs-14px align-items-start d-flex pl-0 pr-2 mr-1`
222 | span.style.minWidth = '2em'
223 |
224 | const avatarAndName = node.firstChild.firstChild.firstChild
225 | avatarAndName.insertBefore(span, avatarAndName.firstElementChild)
226 | }
227 |
228 | span.innerText = `${i}`
229 | } catch (e) { }
230 | }
231 |
232 | function addHolderPercentage (node, index, circulation) {
233 | try {
234 | const heldColumnItem = node.firstChild.firstChild.childNodes.item(1)
235 | const coinsHeld = parseFloat(heldColumnItem.innerText)
236 |
237 | const holderPercentageClassName = 'plus-profile-holder-share'
238 | let span
239 | const existingSpan = node.querySelector(`.${holderPercentageClassName}`)
240 | if (existingSpan) {
241 | span = existingSpan
242 | } else {
243 | span = document.createElement('span')
244 | span.className = `${holderPercentageClassName} fc-muted fs-12px ml-1`
245 | heldColumnItem.appendChild(span)
246 | }
247 | span.innerText = '(' + ((coinsHeld / circulation) * 100).toFixed(1) + '%)'
248 | } catch (e) { }
249 | }
250 |
251 | const highlightUserInHolderList = (node, loggedInUsername) => {
252 | try {
253 | const nameSpan = node.querySelector('.text-truncate')
254 | const holderUsername = nameSpan.innerText
255 | if (loggedInUsername === holderUsername) {
256 | node.className = 'light-grey-divider'
257 | }
258 | } catch (e) { }
259 | };
260 |
261 | const addHolderEnrichments = function (coinsInCirculation) {
262 | const topCard = document.querySelector('creator-profile-top-card')
263 | const creatorProfileHodlers = document.querySelector('creator-profile-hodlers')
264 | if (!creatorProfileHodlers || observingHolders || !topCard) return
265 | const holdersList = creatorProfileHodlers.firstElementChild
266 |
267 | // Before the list loads, it has an "empty" view
268 | if (holdersList.childElementCount === 1) return
269 |
270 | const pageUsername = getUsernameFromUrl()
271 | const loggedInUsername = getLoggedInUsername()
272 |
273 | const firstHodlerNode = holdersList.childNodes.item(1)
274 | const firstHolderName = firstHodlerNode.querySelector('.text-truncate')
275 | const holdsOwnCoin = pageUsername.toLowerCase().startsWith(firstHolderName.innerText.toLowerCase())
276 |
277 | try {
278 | // Only the first few holders items are initially loaded...
279 | const childNodes = holdersList.childNodes
280 | for (let i = 1; i < childNodes.length; i++) {
281 | const node = childNodes.item(i)
282 | if (!node.dataset) continue
283 |
284 | const index = Number(node.dataset.sid)
285 | highlightUserInHolderList(node, loggedInUsername)
286 | addHolderPositionRank(node, index, holdsOwnCoin)
287 | addHolderPercentage(node, index, coinsInCirculation)
288 | }
289 | } catch (e) { }
290 |
291 | // observe the rest
292 | const config = { childList: true, subtree: false }
293 | new MutationObserver((mutations) => {
294 | mutations.forEach(mutation => {
295 | Array.from(mutation.addedNodes, node => {
296 | const index = Number(node.dataset.sid)
297 | highlightUserInHolderList(node, loggedInUsername)
298 | addHolderPositionRank(node, index, holdsOwnCoin)
299 | addHolderPercentage(node, index, coinsInCirculation)
300 | })
301 | })
302 | }).observe(holdersList, config)
303 | observingHolders = true
304 | }
305 |
306 | const createFollowsYouBadge = (id) => {
307 | const text = document.createElement('span')
308 | text.className = 'plus-tooltip-text'
309 | text.innerText = 'Follows you'
310 |
311 | const icon = document.createElement('i')
312 | icon.className = 'fas fa-user-friends'
313 | icon.appendChild(text)
314 |
315 | const followsYouSpan = document.createElement('span')
316 | if (id) followsYouSpan.id = id
317 | followsYouSpan.className = 'badge badge-pill plus-badge plus-badge-icon ml-2 global__tooltip-icon plus-tooltip'
318 | followsYouSpan.appendChild(icon)
319 |
320 | return followsYouSpan
321 | }
322 |
323 | const addFollowsYouBadgeToProfileHeader = function (userDataDiv, following) {
324 | const followsYouBadgeId = 'plus-profile-follows-you-badge'
325 | const alreadyAdded = document.getElementById(followsYouBadgeId)
326 |
327 | if (alreadyAdded || !userDataDiv || !following) return
328 |
329 | const usernameDiv = userDataDiv.firstElementChild
330 | if (!usernameDiv) return
331 |
332 | const followsYouSpan = createFollowsYouBadge(followsYouBadgeId)
333 | usernameDiv.appendChild(followsYouSpan)
334 | }
335 |
336 | const addHodlerBadgeToProfileHeader = function (userDataDiv, isHolding, balanceEntry) {
337 | const holderBadgeId = 'plus-profile-holder-badge'
338 | const alreadyAdded = document.getElementById(holderBadgeId);
339 | if (alreadyAdded || !userDataDiv || !isHolding) return
340 |
341 | const usernameDiv = userDataDiv.firstElementChild
342 | if (!usernameDiv) return
343 |
344 | const holding = balanceEntry['BalanceNanos'] / deSoInNanos
345 | const holdsOrPurchased = balanceEntry['HasPurchased'] ? 'Purchased' : 'Gifted'
346 | const formattedHoldings = parseFloat(holding.toFixed(6))
347 | if (formattedHoldings === 0) return
348 |
349 | const text = document.createElement('span')
350 | text.className = 'plus-tooltip-text'
351 | text.innerText = `${holdsOrPurchased} ${formattedHoldings} of your coin`
352 |
353 | const icon = document.createElement('i')
354 | icon.className = 'fas fa-coins'
355 | icon.appendChild(text)
356 |
357 | const isHodlerSpan = document.createElement('span')
358 | isHodlerSpan.id = holderBadgeId
359 | isHodlerSpan.className = 'badge badge-pill plus-badge plus-badge-icon ml-2 global__tooltip-icon plus-tooltip'
360 | isHodlerSpan.appendChild(icon)
361 |
362 | usernameDiv.appendChild(isHodlerSpan)
363 | }
364 |
365 | const addJumioBadgeToProfileHeader = (userDataDiv) => {
366 | const holderBadgeId = 'plus-profile-jumio-badge'
367 | const alreadyAdded = document.getElementById(holderBadgeId);
368 | if (alreadyAdded || !userDataDiv) return
369 |
370 | const usernameDiv = userDataDiv.firstElementChild
371 | if (!usernameDiv) return
372 |
373 | const text = document.createElement('span')
374 | text.className = 'plus-tooltip-text'
375 | text.innerText = `Verified ID with Jumio`
376 |
377 | const icon = document.createElement('i')
378 | icon.className = 'fas fa-id-card'
379 | icon.appendChild(text)
380 |
381 | const isVerifiedSpan = document.createElement('span')
382 | isVerifiedSpan.id = holderBadgeId
383 | isVerifiedSpan.className = 'badge badge-pill plus-badge plus-badge-icon ml-2 global__tooltip-icon plus-tooltip'
384 | isVerifiedSpan.appendChild(icon)
385 |
386 | usernameDiv.appendChild(isVerifiedSpan)
387 | }
388 |
389 | const getProfileUserDataDiv = function () {
390 | const topCard = document.querySelector('creator-profile-top-card')
391 | if (!topCard) return undefined
392 |
393 | const topCardContent = topCard.firstElementChild
394 | if (!topCardContent) return undefined
395 |
396 | const children = topCardContent.children
397 | if (!children || children.length < 4) return undefined
398 |
399 | return children.item(3)
400 | }
401 |
402 | const profileTabsObserver = new MutationObserver(mutations => {
403 | if (document.querySelector('creator-profile-hodlers')) {
404 | enrichProfileFromApi(mutations[0].target)
405 | }
406 | })
407 |
408 | const observeProfileDetails = (profileDetailsDiv) => {
409 | const observerConfig = { childList: true, subtree: false }
410 | profileTabsObserver.disconnect()
411 | profileTabsObserver.observe(profileDetailsDiv, observerConfig)
412 | }
413 |
414 | const activeIndicatorId = 'plus_active-indicator'
415 |
416 | const createActiveIndicatorElement = (tooltip) => {
417 | const activeIndicator = document.createElement('div')
418 | activeIndicator.id = activeIndicatorId
419 | activeIndicator.className = 'plus-active-indicator rounded-circle bg-success m-1'
420 |
421 | const container = document.createElement('div')
422 | container.className = 'plus-active-indicator-container cursor-pointer plus-tooltip'
423 | container.appendChild(activeIndicator)
424 | container.appendChild(tooltip)
425 |
426 | return container
427 | }
428 |
429 | const createActiveIndicatorTooltip = (innerText) => {
430 | const tooltip = document.createElement('span')
431 | tooltip.className = 'plus-tooltip-text fs-12px'
432 | tooltip.style.width = '180px'
433 | tooltip.style.left = '26px'
434 | tooltip.style.top = '10px'
435 | tooltip.innerText = innerText
436 | return tooltip
437 | }
438 |
439 | const addActiveIndicator = (tooltipText, profileDetailsDiv) => {
440 | const tooltip = createActiveIndicatorTooltip(tooltipText)
441 | const activeIndicator = createActiveIndicatorElement(tooltip)
442 | const avatar = profileDetailsDiv.querySelector('.creator-profile__avatar')
443 | avatar.appendChild(activeIndicator)
444 | }
445 |
446 | const showIndicatorIfActive = (publicKey, profileDetailsDiv) => getTransactionInfo(publicKey, -1, 100)
447 | .then((transactions) => {
448 | const latestTransaction = transactions.reverse()
449 | .find(transaction => transaction['TransactionMetadata']['TransactorPublicKeyBase58Check'] === publicKey)
450 |
451 | if (!latestTransaction) return
452 |
453 | if (document.getElementById(activeIndicatorId)) return
454 |
455 | const blockHashHex = latestTransaction['BlockHashHex']
456 | if (!blockHashHex || (blockHashHex && blockHashHex.length === 0)) {
457 | // This means the transaction is still in the mempool, which means active within the past 5 min
458 | const tooltipText = `Last active < 5 minutes ago`
459 | addActiveIndicator(tooltipText, profileDetailsDiv)
460 | return
461 | }
462 |
463 | getBlockByHash(blockHashHex).then((res) => {
464 | const header = res['Header']
465 | const error = res['Error']
466 | let tooltipText
467 |
468 | // This also means the transaction is still in the mempool
469 | if (error && error.includes('Key not found')) {
470 | tooltipText = `Last active < 5 minutes ago`
471 | } else {
472 | const now = Date.now()
473 | const timestamp = header['TstampSecs'] * 1000
474 | const recentlyActive = now - timestamp < (1000 * 60 * 15)
475 | if (recentlyActive) {
476 | // Block times are ~5 min, so we add that to account for time passed before the transaction was mined
477 | const timeAgo = Math.round((now - timestamp) / 1000 / 60) + 5
478 | tooltipText = `Last active ~${timeAgo} minutes ago`
479 | }
480 | }
481 |
482 | if (tooltipText) {
483 | addActiveIndicator(tooltipText, profileDetailsDiv)
484 | }
485 | })
486 | })
487 |
488 | const enrichProfileFromApi = (profileDetailsDiv) => {
489 | const pageUsername = getUsernameFromUrl()
490 | if (!pageUsername) return
491 |
492 | const loggedInPubKey = getLoggedInPublicKey()
493 | if (!loggedInPubKey) return
494 |
495 | const pagePubKey = getPublicKeyFromPage()
496 | if (!pagePubKey) return
497 |
498 | observeProfileDetails(profileDetailsDiv)
499 |
500 | isFollowingPublicKey(pagePubKey, loggedInPubKey).then(followingRes => {
501 | const userDataDiv = getProfileUserDataDiv()
502 | if (!userDataDiv) return Promise.reject()
503 |
504 | addFollowsYouBadgeToProfileHeader(userDataDiv, followingRes['IsFollowing'])
505 |
506 | if (getUsernameFromUrl() !== pageUsername) return Promise.reject()
507 |
508 | }).then(() => getProfileByUsername(pageUsername)).then(pageProfile => {
509 | const userDataDiv = getProfileUserDataDiv()
510 | if (!userDataDiv) return Promise.reject()
511 |
512 | if (getUsernameFromUrl() !== pageUsername) return Promise.reject()
513 |
514 | addNativeCoinPriceToProfileHeader(userDataDiv, pageProfile)
515 |
516 | const circulation = pageProfile['CoinEntry']['CoinsInCirculationNanos'] / deSoInNanos
517 | addHolderEnrichments(circulation)
518 |
519 | const pubKey = pageProfile['PublicKeyBase58Check']
520 | return Promise.resolve(pubKey)
521 |
522 | }).then(pagePubKey => {
523 | if (!pagePubKey) return Promise.reject()
524 |
525 | return isHoldingPublicKey(pagePubKey, loggedInPubKey).then(res => {
526 | if (getUsernameFromUrl() !== pageUsername) return Promise.reject()
527 |
528 | const userDataDiv = getProfileUserDataDiv()
529 | if (!userDataDiv) return Promise.reject()
530 |
531 | addHodlerBadgeToProfileHeader(userDataDiv, res['IsHodling'], res['BalanceEntry'])
532 | })
533 | }).then(() => getHodlersByUsername(pageUsername)).then(hodlersList => {
534 | addHoldersCount(hodlersList.length)
535 |
536 | const loggedInUserIsHodler = hodlersList.find(hodler => {
537 | return hodler['HODLerPublicKeyBase58Check'] === loggedInPubKey
538 | })
539 | if (loggedInUserIsHodler) addSellButton()
540 |
541 | return getUserMetadata(pagePubKey).catch(() => {})
542 |
543 | }).then(metadata => {
544 | const userDataDiv = getProfileUserDataDiv()
545 | if (!userDataDiv) return Promise.reject()
546 |
547 | if (metadata['JumioVerified']) {
548 | addJumioBadgeToProfileHeader(userDataDiv)
549 | }
550 | })
551 | .then(() => showIndicatorIfActive(pagePubKey, profileDetailsDiv))
552 | .catch(() => {})
553 | }
554 |
555 | const observeProfileInnerContent = (page) => {
556 | const profileDetailsDiv = page.querySelector('creator-profile-details')
557 | if (profileDetailsDiv) {
558 | const observerConfig = { childList: true, subtree: false }
559 | const observer = new MutationObserver(mutations => {
560 | mutations.forEach(mutation => {
561 | Array.from(mutation.addedNodes, node => {
562 | if (node.nodeName !== 'SIMPLE-CENTER-LOADER') enrichProfileFromApi(node)
563 | })
564 | })
565 | })
566 | observer.observe(profileDetailsDiv, observerConfig)
567 | }
568 | }
569 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) Paul Burke 2021
3 | Github: @ipaulpro/bitcloutplus
4 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
5 | */
6 |
7 | const deSoInNanos = 1000000000
8 |
9 | let timer, currentUrl
10 | let identityWindow
11 |
12 | let longPostEnabled = true
13 | let observingHolders = false
14 |
15 | let notificationsJwtMsgId
16 |
17 | const dollarFormatter = new Intl.NumberFormat('en-US', {
18 | style: 'currency',
19 | currency: 'USD'
20 | })
21 |
22 | const addEditProfileButton = function () {
23 | let editProfileButtonId = 'plus-sidebar-edit-profile'
24 | if (document.getElementById(editProfileButtonId)) return
25 |
26 | const leftBarButtons = document.querySelectorAll('left-bar-button')
27 | try {
28 | leftBarButtons.forEach(button => {
29 | const profileDiv = button.firstElementChild.lastElementChild
30 | const profileAnchor = profileDiv.firstElementChild
31 |
32 | if (profileAnchor.innerText.includes('Profile')) {
33 | const a = document.createElement('a')
34 | a.id = editProfileButtonId
35 | a.href = 'update-profile'
36 | a.className = 'fc-muted fs-12px ml-2 pl-1 pr-1'
37 | a.innerText = 'Edit'
38 |
39 | profileDiv.appendChild(a)
40 | }
41 | })
42 | } catch (e) {}
43 | }
44 |
45 | const enrichWallet = function (page) {
46 | try {
47 | const holdingsDiv = page.querySelectorAll('.holdings__divider').item(1)
48 | const holdingsValueDiv = holdingsDiv.lastElementChild.children.item(2)
49 | const holdingsCloutValue = parseFloat(holdingsValueDiv.innerText.replace(/[^0-9.]+/g, ''))
50 |
51 | const container = page.querySelector('.container')
52 | const balanceValuesDiv = container.firstElementChild.lastElementChild
53 | const balanceCloutValue = parseFloat(balanceValuesDiv.firstElementChild.innerText.replace(/[^0-9.]+/g, ''))
54 |
55 | const cloutLabelSpan = document.createElement('span')
56 | cloutLabelSpan.className = 'plus-text-muted fs-12px font-weight-normal ml-2'
57 | cloutLabelSpan.innerText = '$DESO'
58 |
59 | const cloutSpan = document.createElement('span')
60 | cloutSpan.className = 'plus-text-muted fs-14px font-weight-normal'
61 | cloutSpan.innerText = `${(holdingsCloutValue + balanceCloutValue).toFixed(4)}`
62 | cloutSpan.appendChild(cloutLabelSpan)
63 |
64 | const totalDiv = document.createElement('div')
65 | totalDiv.className = 'ml-auto mr-15px'
66 | totalDiv.style.lineHeight = '1.2'
67 | totalDiv.appendChild(cloutSpan)
68 |
69 | const topBar = document.getElementsByClassName('global__top-bar').item(0).children.item(1).children.item(1)
70 | topBar.appendChild(totalDiv)
71 | } catch (e) {}
72 | }
73 |
74 | const formatPriceUsd = function (price) {
75 | return `${dollarFormatter.format(price)} USD`
76 | }
77 |
78 | const enrichBalanceBox = function (profile) {
79 | if (!profile) return
80 |
81 | try {
82 | const nativePrice = (profile['CoinPriceDeSoNanos'] / deSoInNanos).toFixed(2)
83 | const spotPrice = getSpotPrice()
84 | const coinPriceUsd = nativePrice * spotPrice
85 |
86 | const creatorCoinBalanceId = 'plus-creator-coin-balance'
87 | const creatorCoinPriceId = 'plus-creator-coin-price'
88 | const creatorCoinPriceUsdId = 'plus-creator-coin-price-usd'
89 | const existingElement = document.getElementById(creatorCoinBalanceId)
90 | if (existingElement) {
91 | document.getElementById(creatorCoinPriceId).innerText = ` ${nativePrice} $DESO `
92 | document.getElementById(creatorCoinPriceUsdId).innerText = formatPriceUsd(coinPriceUsd)
93 | return
94 | }
95 |
96 | const creatorCoinBalanceContainer = document.createElement('div')
97 | creatorCoinBalanceContainer.id = creatorCoinBalanceId
98 | creatorCoinBalanceContainer.className = 'd-flex justify-content-between pt-10px'
99 |
100 | const coinNameDiv = document.createElement('div')
101 | coinNameDiv.className = 'd-flex'
102 | coinNameDiv.style.textOverflow = 'ellipsis'
103 | coinNameDiv.style.maxWidth = '150px'
104 | coinNameDiv.style.overflow = 'hidden'
105 | coinNameDiv.style.whiteSpace = 'noWrap'
106 | coinNameDiv.innerText = `Your Coin`
107 |
108 | const coinPriceDiv = document.createElement('div')
109 | coinPriceDiv.className = 'd-flex flex-column align-items-end justify-content-end flex-wrap'
110 |
111 | const coinPriceValueDiv = document.createElement('div')
112 | coinPriceValueDiv.id = creatorCoinPriceId
113 | coinPriceValueDiv.innerText = ` ${nativePrice} $DESO `
114 |
115 | const coinPriceConversionDiv = document.createElement('div')
116 | coinPriceConversionDiv.className = 'd-flex plus-text-muted'
117 |
118 | const coinPriceApproximateDiv = document.createElement('div')
119 | coinPriceApproximateDiv.className = 'ml-10px mr-10px'
120 | coinPriceApproximateDiv.innerText = ' ≈ '
121 |
122 | const coinPriceUsdDiv = document.createElement('div')
123 | coinPriceUsdDiv.id = creatorCoinPriceUsdId
124 | coinPriceUsdDiv.innerText = formatPriceUsd(coinPriceUsd)
125 |
126 | coinPriceConversionDiv.appendChild(coinPriceApproximateDiv)
127 | coinPriceConversionDiv.appendChild(coinPriceUsdDiv)
128 | coinPriceDiv.appendChild(coinPriceValueDiv)
129 | coinPriceDiv.appendChild(coinPriceConversionDiv)
130 | creatorCoinBalanceContainer.appendChild(coinNameDiv)
131 | creatorCoinBalanceContainer.appendChild(coinPriceDiv)
132 |
133 | const balanceBox = document.getElementsByClassName('right-bar-creators__balance-box').item(0)
134 | balanceBox.appendChild(creatorCoinBalanceContainer)
135 | } catch (e) { }
136 | }
137 |
138 | const checkForNotifications = () => {
139 | const publicKey = getLoggedInPublicKey()
140 | if (!publicKey) return
141 |
142 | const leftBarButtons = document.querySelectorAll('left-bar-button')
143 | if (!leftBarButtons || leftBarButtons.length === 0) return
144 |
145 | const sidebar = leftBarButtons[0].parentElement
146 |
147 | const dividers = sidebar.querySelectorAll('.p-15px')
148 | if (!dividers) return
149 |
150 | const notificationsAnchor = sidebar.querySelector("a[href*='/notifications']")
151 | if (!notificationsAnchor) return
152 |
153 | const menuItem = notificationsAnchor.parentElement.parentElement
154 | if (menuItem.tagName !== 'LEFT-BAR-BUTTON') return
155 |
156 | getUnreadNotificationsCount(publicKey)
157 | .then(data => {
158 | const notificationsCount = data['NotificationsCount']
159 | const id = 'plus-notifications-count'
160 | if (notificationsCount > 0) {
161 | let countElement
162 | const existingCountElement = document.getElementById(id)
163 | if (existingCountElement) {
164 | countElement = existingCountElement
165 | } else {
166 | countElement = document.createElement('div')
167 | countElement.id = id
168 | countElement.className = 'ml-5px p-5x fs-15px notification'
169 | const div = menuItem.firstElementChild.lastElementChild
170 | div.appendChild(countElement)
171 | }
172 | countElement.innerText = String(notificationsCount)
173 | } else {
174 | const countElement = document.getElementById(id)
175 | if (countElement) countElement.remove()
176 | }
177 | })
178 | }
179 |
180 | const markNotificationsRead = (jwt) => {
181 | const publicKey = getLoggedInPublicKey()
182 | if (!publicKey) return
183 |
184 | getUnreadNotificationsCount(publicKey)
185 | .then(data => {
186 | const index = data['LastUnreadNotificationIndex']
187 | const metadata = {
188 | PublicKeyBase58Check: publicKey,
189 | LastSeenIndex: index,
190 | LastUnreadNotificationIndex: index,
191 | UnreadNotifications: 0,
192 | JWT: jwt
193 | }
194 | return setNotificationMetadata(metadata)
195 | })
196 | .then(() => {
197 | const countElement = document.getElementById('plus-notifications-count')
198 | if (countElement) countElement.remove()
199 | })
200 | .finally(() => {
201 | notificationsJwtMsgId = null
202 | })
203 | }
204 |
205 | const extractTransactors = (block) => {
206 | const transactors = new Set()
207 | const transactions = block['Transactions']
208 | transactions.forEach(transaction => {
209 | const transactor = transaction['TransactionMetadata']['TransactorPublicKeyBase58Check']
210 | transactors.add(transactor)
211 | })
212 | return [...transactors]
213 | }
214 |
215 | const getMempoolTransactors = () => new Promise((resolve, reject) =>
216 | chrome.runtime.sendMessage({type: 'get-mempool-transactors'}, response => {
217 | if (response.mempoolTransactors) {
218 | resolve(response.mempoolTransactors)
219 | } else {
220 | reject(chrome.runtime.lastError)
221 | }
222 | })
223 | )
224 |
225 | const addMempoolTransactors = (transactors) => {
226 | return getMempoolTransactors()
227 | .then(mempoolTransactors => transactors.concat(mempoolTransactors))
228 | .catch(() => transactors)
229 | }
230 |
231 | const findFollowingInTransactors = (transactors) => {
232 | const publicKey = getLoggedInPublicKey()
233 | return getFollowingByPublicKey(publicKey)
234 | .then(res => {
235 | const followerMap = res['PublicKeyToProfileEntry']
236 | return Object.values(followerMap)
237 | .filter(follower => transactors.includes(follower['PublicKeyBase58Check']))
238 | })
239 | }
240 |
241 | const getOnlineFollowing = () =>
242 | getAppState()
243 | .then(appState => appState['BlockHeight'])
244 | .then(getBlockByHeight)
245 | .then(extractTransactors)
246 | .then(addMempoolTransactors)
247 | .then(findFollowingInTransactors)
248 |
249 | const createRecentlyActiveListItem = (user) => {
250 | const username = user['Username']
251 |
252 | const listItem = document.createElement('a')
253 | listItem.className = 'link--unstyled d-flex align-items-center text-grey5 fs-15px py-2'
254 | listItem.href = `/u/${username}`
255 |
256 | const profilePhotoUrl = getProfilePhotoUrlForPublicKey(user['PublicKeyBase58Check'])
257 | const avatar = document.createElement('div')
258 | avatar.className = 'right-bar-creators-leaderboard__creator-avatar'
259 | avatar.style.backgroundImage = `url("${profilePhotoUrl}")`
260 |
261 | const text = document.createElement('span')
262 | text.innerText = username
263 |
264 | const textContainer = document.createElement('div')
265 | textContainer.className = 'flex-grow-1'
266 | textContainer.appendChild(text)
267 |
268 | const messageIcon = document.createElement('i')
269 | messageIcon.className = 'fas fa-envelope mr-2'
270 |
271 | const messageLink = document.createElement('a')
272 | messageLink.className = 'plus-message-link'
273 | messageLink.href = `/inbox?username=${username}`
274 | messageLink.appendChild(messageIcon)
275 |
276 | listItem.appendChild(avatar)
277 | listItem.appendChild(textContainer)
278 | listItem.appendChild(messageLink)
279 |
280 | return listItem
281 | }
282 |
283 | const addOnlineUsersRightBar = () => {
284 | const boxId = 'plus-online-users'
285 | if (document.getElementById(boxId)) return
286 |
287 | const rightBar = document.querySelector('right-bar-creators')
288 | if (!rightBar) return
289 |
290 | getOnlineFollowing()
291 | .then(onlineUsers => {
292 | if (document.getElementById(boxId)) return
293 |
294 | const listItems = []
295 | onlineUsers.sort((a, b) => a['Username'].localeCompare(b['Username']))
296 | .forEach(user => {
297 | const listItem = createRecentlyActiveListItem(user)
298 | listItems.push(listItem)
299 | })
300 |
301 | const list = document.createElement('div')
302 | list.id = 'plus-online-users-list'
303 | list.className = 'd-flex flex-column overflow-auto'
304 | list.style.maxHeight = '300px'
305 |
306 | if (listItems.length > 0) {
307 | listItems.forEach(listItem => list.appendChild(listItem))
308 | } else {
309 | const emptyItem = document.createElement('div')
310 | emptyItem.className = 'text-muted'
311 | emptyItem.innerText = '(No users found)'
312 | list.appendChild(emptyItem)
313 | }
314 |
315 | const title = document.createElement('p')
316 | title.className = 'font-weight-bold fs-15px text-white mb-2'
317 | title.innerText = `Recently Active (${onlineUsers.length})`
318 |
319 | const box = document.createElement('div')
320 | box.id = boxId
321 | box.className = 'right-bar-creators__balance-box br-12px p-15px mb-30px fs-13px text-grey5'
322 | box.appendChild(title)
323 | box.append(list)
324 |
325 | const sidebarInner = rightBar.querySelector(':scope > .global__sidebar__inner')
326 | const balanceBox = sidebarInner.querySelector(':scope > .right-bar-creators__balance-box')
327 | const index = Array.from(sidebarInner.children).indexOf(balanceBox)
328 | sidebarInner.insertBefore(box, sidebarInner.children.item(index + 1))
329 | })
330 | .catch(console.error)
331 | }
332 |
333 | const addGlobalEnrichments = function () {
334 | addEditProfileButton()
335 | addNewPostButton()
336 | }
337 |
338 | const removeUnfollowLinksInPosts = () => {
339 | const followButtons = document.querySelectorAll('feed-post follow-button')
340 | Array.from(followButtons).forEach(node => {
341 | if (node.innerText === 'Unfollow') node.remove()
342 | })
343 | }
344 |
345 | // Callback function to execute when body mutations are observed
346 | const appRootObserverCallback = function () {
347 | if (currentUrl !== window.location.href) {
348 | observingHolders = false
349 | currentUrl = window.location.href
350 | }
351 |
352 | addGlobalEnrichments()
353 | removeUnfollowLinksInPosts()
354 | addLogoutButtons()
355 |
356 | const profilePage = document.querySelector('creator-profile-page')
357 | if (profilePage) {
358 | enrichProfile()
359 | return
360 | }
361 |
362 | const nftPostPage = document.querySelector('nft-post-page')
363 | if (nftPostPage) {
364 | enrichNftPostPage(nftPostPage)
365 | return
366 | }
367 |
368 | const postThreadPage = document.querySelector('post-thread-page')
369 | if (postThreadPage) {
370 | showEditPostButtonIfNeeded()
371 | }
372 | }
373 |
374 | const updateUserCreatorCoinPrice = function () {
375 | const key = getLoggedInPublicKey()
376 | getProfileByPublicKey(key).then(profile => {
377 | enrichBalanceBox(profile)
378 | }).catch(() => {})
379 | }
380 |
381 | const getJwt = (id) => {
382 | const identity = getCurrentIdentity()
383 | if (!identity) return
384 |
385 | const payload = {
386 | accessLevel: identity.accessLevel,
387 | accessLevelHmac: identity.accessLevelHmac,
388 | encryptedSeedHex: identity.encryptedSeedHex
389 | }
390 |
391 | postIdentityMessage(id, 'jwt', payload)
392 | }
393 |
394 | const addLogoutButtons = () => {
395 | const accountSelectorMaskIconId = '__clout-mask-account-selector-icon'
396 | if (document.getElementById(accountSelectorMaskIconId)) return
397 |
398 | if (document.querySelectorAll('.plus-account-logout-icon').length > 0) return
399 |
400 | const listItems = document.querySelectorAll('.change-account-selector_list-item')
401 | listItems.forEach(listItem => {
402 | const avatar = listItem.querySelector(':scope > .change-account-selector__account-image')
403 |
404 | const icon = document.createElement('i')
405 | icon.className = 'plus-account-logout-icon fas fa-times-circle'
406 |
407 | const button = document.createElement('button')
408 | button.className = 'btn btn-link py-0 px-1 text-muted'
409 | button.onclick = () => {
410 | const backgroundImage = avatar.style.backgroundImage
411 | const start = backgroundImage.indexOf('BC1YL')
412 | const end = backgroundImage.indexOf('?', start)
413 | const publicKey = backgroundImage.substring(start, end)
414 | identityWindow = window.open(`https://identity.deso.org/logout?publicKey=${publicKey}`, null,
415 | 'toolbar=no, width=800, height=1000, top=0, left=0')
416 | }
417 | button.appendChild(icon)
418 |
419 | const div = document.createElement('div')
420 | div.className = 'plus-account-logout d-flex flex-row-reverse flex-grow-1'
421 | div.appendChild(button)
422 |
423 | listItem.appendChild(div)
424 | })
425 | }
426 |
427 | const globalContainerObserverCallback = function () {
428 | updateUserCreatorCoinPrice()
429 | addPostUsernameAutocomplete()
430 | addPostTextAreaListener()
431 | restorePostDraft()
432 | replacePostBtn()
433 |
434 | const notifications = document.querySelector('app-notifications-page')
435 | if (!notifications) {
436 | checkForNotifications()
437 | } else {
438 | notificationsJwtMsgId = uuid()
439 | getJwt(notificationsJwtMsgId)
440 | }
441 |
442 | const profilePage = document.querySelector('creator-profile-page')
443 | if (profilePage) {
444 | observeProfileInnerContent(profilePage)
445 | return
446 | }
447 |
448 | const wallet = document.querySelector('wallet')
449 | if (wallet) {
450 | enrichWallet(wallet)
451 | return
452 | }
453 |
454 | const follows = document.querySelector('manage-follows')
455 | if (follows) {
456 | observeFollowLists(follows)
457 | addBlockedUsersTabToFollows(follows)
458 |
459 | if (isBlockedUsersUrl()) {
460 | addBlockedUsersList(follows)
461 | }
462 | return
463 | }
464 |
465 | if (isTransferNftUrl()) {
466 | createTransferNftPage()
467 | return
468 | }
469 |
470 | const newPost = document.querySelector('app-create-post-page')
471 | if (newPost) {
472 | enrichCreatePostPage(newPost)
473 | checkForEditPostQueryParams(newPost)
474 | }
475 | }
476 |
477 | const bodyObserverCallback = function () {
478 | addOnlineUsersRightBar()
479 |
480 | const modalContainer = document.querySelector('modal-container')
481 | if (modalContainer) {
482 | addPostUsernameAutocomplete()
483 | fixImageLightbox(modalContainer)
484 | }
485 | }
486 |
487 | const onTransactionSigned = (payload) => {
488 | if (!payload) return
489 |
490 | const transactionHex = payload['signedTransactionHex']
491 | if (!transactionHex) return
492 |
493 | pendingTransactionHex = null
494 |
495 | submitTransaction(transactionHex).then(res => {
496 | const response = res['PostEntryResponse']
497 | if (response && response['PostHashHex']) {
498 | window.location.href = `posts/${response['PostHashHex']}`
499 | } else {
500 | const metadata = res['Transaction']['TxnMeta']
501 | const nftPostHash = metadata['NFTPostHash']
502 | if (nftPostHash) {
503 | if (new URL(window.location).pathname.includes('nft-transfers')) {
504 | window.location.reload()
505 | } else {
506 | const postHashHex = Buffer.from(nftPostHash).toString('hex')
507 | window.location.href = `/nft/${postHashHex}`
508 | }
509 | } else {
510 | window.location.href = window`u/${getLoggedInUsername()}`
511 | }
512 | }
513 | }).catch(() => {})
514 | }
515 |
516 | const handleLogin = (payload) => {
517 | if (identityWindow) {
518 | identityWindow.close()
519 | identityWindow = null
520 | }
521 |
522 | if (payload['signedTransactionHex']) {
523 | onTransactionSigned(payload)
524 | } else if (payload['users']) {
525 | // After logout
526 | const users = JSON.stringify(payload['users'])
527 | window.localStorage.setItem('identityUsersV2', users)
528 | switchToFirstAccount()
529 | }
530 | }
531 |
532 | const handleSignTransactionResponse = (payload) => {
533 | if (!payload) return
534 |
535 | if (payload['approvalRequired'] && pendingTransactionHex) {
536 | const identityServiceUrl = window.localStorage.getItem('lastIdentityServiceURLV2')
537 | identityWindow = window.open(
538 | `${identityServiceUrl}/approve?tx=${pendingTransactionHex}`, null,
539 | 'toolbar=no, width=800, height=1000, top=0, left=0')
540 | } else if (payload['signedTransactionHex']) {
541 | onTransactionSigned(payload)
542 | }
543 | }
544 |
545 | const handleMessage = (message) => {
546 | const { data: { id: id, method: method, payload: payload } } = message
547 |
548 | if (method === 'login') {
549 | handleLogin(payload)
550 | } else if (payload) {
551 | const jwt = payload['jwt']
552 | if (jwt) {
553 | if (id === notificationsJwtMsgId) {
554 | markNotificationsRead(jwt)
555 | } else if (id === blockJwtMsgId) {
556 | blockUser(jwt)
557 | }
558 | }
559 | else if (id === pendingIdentityMessageId && payload) {
560 | if (payload['encryptedMessage']) {
561 | const encryptedMessage = payload['encryptedMessage']
562 | if (encryptedMessage) onNftTransferUnlockableEncrypted(encryptedMessage)
563 | } else if (payload['decryptedHexes']) {
564 | const unlockableText = Object.values(payload['decryptedHexes'])[0]
565 | if (unlockableText) onNftTransferUnlockableDecrypted(unlockableText)
566 | } else {
567 | handleSignTransactionResponse(payload)
568 | }
569 | pendingIdentityMessageId = null
570 | }
571 | }
572 | }
573 |
574 | const init = function () {
575 | window.addEventListener('message', handleMessage)
576 |
577 | chrome.storage.local.get(['longPost'], items => {
578 | if (items.longPost === undefined) {
579 | chrome.storage.local.set({ longPost: true }).catch()
580 | } else {
581 | longPostEnabled = items.longPost
582 | }
583 | })
584 |
585 | // app-root is dynamically loaded, so we observe changes to the child list
586 | const appRoot = document.querySelector('app-root')
587 | if (appRoot) {
588 | const appRootObserverConfig = { childList: true, subtree: true }
589 | const appRootObserver = new MutationObserver(appRootObserverCallback)
590 | appRootObserver.observe(appRoot, appRootObserverConfig)
591 | }
592 |
593 | const globalContainer = document.getElementsByClassName('global__container')[0]
594 | if (globalContainer) {
595 | const globalObserverConfig = { childList: true, subtree: false }
596 | const globalObserver = new MutationObserver(globalContainerObserverCallback)
597 | globalObserver.observe(globalContainer, globalObserverConfig)
598 | }
599 |
600 | const body = document.getElementsByTagName('body')[0]
601 | if (body) {
602 | const bodyObserverConfig = { childList: true, subtree: false }
603 | const bodyObserver = new MutationObserver(bodyObserverCallback)
604 | bodyObserver.observe(body, bodyObserverConfig)
605 | }
606 |
607 | if (timer) clearInterval(timer)
608 | timer = setInterval(updateUserCreatorCoinPrice, 5 * 60 * 1000)
609 | }
610 |
611 | init()
612 |
--------------------------------------------------------------------------------
/vendor/buffer/buffer.min.js:
--------------------------------------------------------------------------------
1 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).Buffer=t()}}((function(){return function t(r,e,n){function i(f,u){if(!e[f]){if(!r[f]){var s="function"==typeof require&&require;if(!u&&s)return s(f,!0);if(o)return o(f,!0);var h=new Error("Cannot find module '"+f+"'");throw h.code="MODULE_NOT_FOUND",h}var a=e[f]={exports:{}};r[f][0].call(a.exports,(function(t){return i(r[f][1][t]||t)}),a,a.exports,t,r,e,n)}return e[f].exports}for(var o="function"==typeof require&&require,f=0;ft.from(r).toString("hex")}).call(this)}).call(this,t("buffer").Buffer)},{buffer:3}],2:[function(t,r,e){"use strict";e.byteLength=function(t){var r=h(t),e=r[0],n=r[1];return 3*(e+n)/4-n},e.toByteArray=function(t){var r,e,n=h(t),f=n[0],u=n[1],s=new o(function(t,r,e){return 3*(r+e)/4-e}(0,f,u)),a=0,c=u>0?f-4:f;for(e=0;e>16&255,s[a++]=r>>8&255,s[a++]=255&r;2===u&&(r=i[t.charCodeAt(e)]<<2|i[t.charCodeAt(e+1)]>>4,s[a++]=255&r);1===u&&(r=i[t.charCodeAt(e)]<<10|i[t.charCodeAt(e+1)]<<4|i[t.charCodeAt(e+2)]>>2,s[a++]=r>>8&255,s[a++]=255&r);return s},e.fromByteArray=function(t){for(var r,e=t.length,i=e%3,o=[],f=16383,u=0,s=e-i;us?s:u+f));1===i?(r=t[e-1],o.push(n[r>>2]+n[r<<4&63]+"==")):2===i&&(r=(t[e-2]<<8)+t[e-1],o.push(n[r>>10]+n[r>>4&63]+n[r<<2&63]+"="));return o.join("")};for(var n=[],i=[],o="undefined"!=typeof Uint8Array?Uint8Array:Array,f="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",u=0,s=f.length;u0)throw new Error("Invalid string. Length must be a multiple of 4");var e=t.indexOf("=");return-1===e&&(e=r),[e,e===r?0:4-e%4]}function a(t,r,e){for(var i,o,f=[],u=r;u>18&63]+n[o>>12&63]+n[o>>6&63]+n[63&o]);return f.join("")}i["-".charCodeAt(0)]=62,i["_".charCodeAt(0)]=63},{}],3:[function(t,r,e){(function(r){(function(){
2 | /*!
3 | * The buffer module from node.js, for the browser.
4 | *
5 | * @author Feross Aboukhadijeh
6 | * @license MIT
7 | */
8 | "use strict";var r=t("base64-js"),n=t("ieee754");e.Buffer=f,e.SlowBuffer=function(t){+t!=t&&(t=0);return f.alloc(+t)},e.INSPECT_MAX_BYTES=50;var i=2147483647;function o(t){if(t>i)throw new RangeError('The value "'+t+'" is invalid for option "size"');var r=new Uint8Array(t);return r.__proto__=f.prototype,r}function f(t,r,e){if("number"==typeof t){if("string"==typeof r)throw new TypeError('The "string" argument must be of type string. Received type number');return h(t)}return u(t,r,e)}function u(t,r,e){if("string"==typeof t)return function(t,r){"string"==typeof r&&""!==r||(r="utf8");if(!f.isEncoding(r))throw new TypeError("Unknown encoding: "+r);var e=0|p(t,r),n=o(e),i=n.write(t,r);i!==e&&(n=n.slice(0,i));return n}(t,r);if(ArrayBuffer.isView(t))return a(t);if(null==t)throw TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof t);if(D(t,ArrayBuffer)||t&&D(t.buffer,ArrayBuffer))return function(t,r,e){if(r<0||t.byteLength=i)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+i.toString(16)+" bytes");return 0|t}function p(t,r){if(f.isBuffer(t))return t.length;if(ArrayBuffer.isView(t)||D(t,ArrayBuffer))return t.byteLength;if("string"!=typeof t)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof t);var e=t.length,n=arguments.length>2&&!0===arguments[2];if(!n&&0===e)return 0;for(var i=!1;;)switch(r){case"ascii":case"latin1":case"binary":return e;case"utf8":case"utf-8":return P(t).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*e;case"hex":return e>>>1;case"base64":return j(t).length;default:if(i)return n?-1:P(t).length;r=(""+r).toLowerCase(),i=!0}}function l(t,r,e){var n=!1;if((void 0===r||r<0)&&(r=0),r>this.length)return"";if((void 0===e||e>this.length)&&(e=this.length),e<=0)return"";if((e>>>=0)<=(r>>>=0))return"";for(t||(t="utf8");;)switch(t){case"hex":return S(this,r,e);case"utf8":case"utf-8":return U(this,r,e);case"ascii":return T(this,r,e);case"latin1":case"binary":return I(this,r,e);case"base64":return B(this,r,e);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return C(this,r,e);default:if(n)throw new TypeError("Unknown encoding: "+t);t=(t+"").toLowerCase(),n=!0}}function y(t,r,e){var n=t[r];t[r]=t[e],t[e]=n}function g(t,r,e,n,i){if(0===t.length)return-1;if("string"==typeof e?(n=e,e=0):e>2147483647?e=2147483647:e<-2147483648&&(e=-2147483648),q(e=+e)&&(e=i?0:t.length-1),e<0&&(e=t.length+e),e>=t.length){if(i)return-1;e=t.length-1}else if(e<0){if(!i)return-1;e=0}if("string"==typeof r&&(r=f.from(r,n)),f.isBuffer(r))return 0===r.length?-1:w(t,r,e,n,i);if("number"==typeof r)return r&=255,"function"==typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(t,r,e):Uint8Array.prototype.lastIndexOf.call(t,r,e):w(t,[r],e,n,i);throw new TypeError("val must be string, number or Buffer")}function w(t,r,e,n,i){var o,f=1,u=t.length,s=r.length;if(void 0!==n&&("ucs2"===(n=String(n).toLowerCase())||"ucs-2"===n||"utf16le"===n||"utf-16le"===n)){if(t.length<2||r.length<2)return-1;f=2,u/=2,s/=2,e/=2}function h(t,r){return 1===f?t[r]:t.readUInt16BE(r*f)}if(i){var a=-1;for(o=e;ou&&(e=u-s),o=e;o>=0;o--){for(var c=!0,p=0;pi&&(n=i):n=i;var o=r.length;n>o/2&&(n=o/2);for(var f=0;f>8,i=e%256,o.push(i),o.push(n);return o}(r,t.length-e),t,e,n)}function B(t,e,n){return 0===e&&n===t.length?r.fromByteArray(t):r.fromByteArray(t.slice(e,n))}function U(t,r,e){e=Math.min(t.length,e);for(var n=[],i=r;i239?4:h>223?3:h>191?2:1;if(i+c<=e)switch(c){case 1:h<128&&(a=h);break;case 2:128==(192&(o=t[i+1]))&&(s=(31&h)<<6|63&o)>127&&(a=s);break;case 3:o=t[i+1],f=t[i+2],128==(192&o)&&128==(192&f)&&(s=(15&h)<<12|(63&o)<<6|63&f)>2047&&(s<55296||s>57343)&&(a=s);break;case 4:o=t[i+1],f=t[i+2],u=t[i+3],128==(192&o)&&128==(192&f)&&128==(192&u)&&(s=(15&h)<<18|(63&o)<<12|(63&f)<<6|63&u)>65535&&s<1114112&&(a=s)}null===a?(a=65533,c=1):a>65535&&(a-=65536,n.push(a>>>10&1023|55296),a=56320|1023&a),n.push(a),i+=c}return function(t){var r=t.length;if(r<=_)return String.fromCharCode.apply(String,t);var e="",n=0;for(;nr&&(t+=" ... "),""},f.prototype.compare=function(t,r,e,n,i){if(D(t,Uint8Array)&&(t=f.from(t,t.offset,t.byteLength)),!f.isBuffer(t))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof t);if(void 0===r&&(r=0),void 0===e&&(e=t?t.length:0),void 0===n&&(n=0),void 0===i&&(i=this.length),r<0||e>t.length||n<0||i>this.length)throw new RangeError("out of range index");if(n>=i&&r>=e)return 0;if(n>=i)return-1;if(r>=e)return 1;if(this===t)return 0;for(var o=(i>>>=0)-(n>>>=0),u=(e>>>=0)-(r>>>=0),s=Math.min(o,u),h=this.slice(n,i),a=t.slice(r,e),c=0;c>>=0,isFinite(e)?(e>>>=0,void 0===n&&(n="utf8")):(n=e,e=void 0)}var i=this.length-r;if((void 0===e||e>i)&&(e=i),t.length>0&&(e<0||r<0)||r>this.length)throw new RangeError("Attempt to write outside buffer bounds");n||(n="utf8");for(var o=!1;;)switch(n){case"hex":return d(this,t,r,e);case"utf8":case"utf-8":return v(this,t,r,e);case"ascii":return b(this,t,r,e);case"latin1":case"binary":return m(this,t,r,e);case"base64":return E(this,t,r,e);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return A(this,t,r,e);default:if(o)throw new TypeError("Unknown encoding: "+n);n=(""+n).toLowerCase(),o=!0}},f.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var _=4096;function T(t,r,e){var n="";e=Math.min(t.length,e);for(var i=r;in)&&(e=n);for(var i="",o=r;oe)throw new RangeError("Trying to access beyond buffer length")}function R(t,r,e,n,i,o){if(!f.isBuffer(t))throw new TypeError('"buffer" argument must be a Buffer instance');if(r>i||rt.length)throw new RangeError("Index out of range")}function x(t,r,e,n,i,o){if(e+n>t.length)throw new RangeError("Index out of range");if(e<0)throw new RangeError("Index out of range")}function M(t,r,e,i,o){return r=+r,e>>>=0,o||x(t,0,e,4),n.write(t,r,e,i,23,4),e+4}function O(t,r,e,i,o){return r=+r,e>>>=0,o||x(t,0,e,8),n.write(t,r,e,i,52,8),e+8}f.prototype.slice=function(t,r){var e=this.length;(t=~~t)<0?(t+=e)<0&&(t=0):t>e&&(t=e),(r=void 0===r?e:~~r)<0?(r+=e)<0&&(r=0):r>e&&(r=e),r>>=0,r>>>=0,e||L(t,r,this.length);for(var n=this[t],i=1,o=0;++o>>=0,r>>>=0,e||L(t,r,this.length);for(var n=this[t+--r],i=1;r>0&&(i*=256);)n+=this[t+--r]*i;return n},f.prototype.readUInt8=function(t,r){return t>>>=0,r||L(t,1,this.length),this[t]},f.prototype.readUInt16LE=function(t,r){return t>>>=0,r||L(t,2,this.length),this[t]|this[t+1]<<8},f.prototype.readUInt16BE=function(t,r){return t>>>=0,r||L(t,2,this.length),this[t]<<8|this[t+1]},f.prototype.readUInt32LE=function(t,r){return t>>>=0,r||L(t,4,this.length),(this[t]|this[t+1]<<8|this[t+2]<<16)+16777216*this[t+3]},f.prototype.readUInt32BE=function(t,r){return t>>>=0,r||L(t,4,this.length),16777216*this[t]+(this[t+1]<<16|this[t+2]<<8|this[t+3])},f.prototype.readIntLE=function(t,r,e){t>>>=0,r>>>=0,e||L(t,r,this.length);for(var n=this[t],i=1,o=0;++o=(i*=128)&&(n-=Math.pow(2,8*r)),n},f.prototype.readIntBE=function(t,r,e){t>>>=0,r>>>=0,e||L(t,r,this.length);for(var n=r,i=1,o=this[t+--n];n>0&&(i*=256);)o+=this[t+--n]*i;return o>=(i*=128)&&(o-=Math.pow(2,8*r)),o},f.prototype.readInt8=function(t,r){return t>>>=0,r||L(t,1,this.length),128&this[t]?-1*(255-this[t]+1):this[t]},f.prototype.readInt16LE=function(t,r){t>>>=0,r||L(t,2,this.length);var e=this[t]|this[t+1]<<8;return 32768&e?4294901760|e:e},f.prototype.readInt16BE=function(t,r){t>>>=0,r||L(t,2,this.length);var e=this[t+1]|this[t]<<8;return 32768&e?4294901760|e:e},f.prototype.readInt32LE=function(t,r){return t>>>=0,r||L(t,4,this.length),this[t]|this[t+1]<<8|this[t+2]<<16|this[t+3]<<24},f.prototype.readInt32BE=function(t,r){return t>>>=0,r||L(t,4,this.length),this[t]<<24|this[t+1]<<16|this[t+2]<<8|this[t+3]},f.prototype.readFloatLE=function(t,r){return t>>>=0,r||L(t,4,this.length),n.read(this,t,!0,23,4)},f.prototype.readFloatBE=function(t,r){return t>>>=0,r||L(t,4,this.length),n.read(this,t,!1,23,4)},f.prototype.readDoubleLE=function(t,r){return t>>>=0,r||L(t,8,this.length),n.read(this,t,!0,52,8)},f.prototype.readDoubleBE=function(t,r){return t>>>=0,r||L(t,8,this.length),n.read(this,t,!1,52,8)},f.prototype.writeUIntLE=function(t,r,e,n){(t=+t,r>>>=0,e>>>=0,n)||R(this,t,r,e,Math.pow(2,8*e)-1,0);var i=1,o=0;for(this[r]=255&t;++o>>=0,e>>>=0,n)||R(this,t,r,e,Math.pow(2,8*e)-1,0);var i=e-1,o=1;for(this[r+i]=255&t;--i>=0&&(o*=256);)this[r+i]=t/o&255;return r+e},f.prototype.writeUInt8=function(t,r,e){return t=+t,r>>>=0,e||R(this,t,r,1,255,0),this[r]=255&t,r+1},f.prototype.writeUInt16LE=function(t,r,e){return t=+t,r>>>=0,e||R(this,t,r,2,65535,0),this[r]=255&t,this[r+1]=t>>>8,r+2},f.prototype.writeUInt16BE=function(t,r,e){return t=+t,r>>>=0,e||R(this,t,r,2,65535,0),this[r]=t>>>8,this[r+1]=255&t,r+2},f.prototype.writeUInt32LE=function(t,r,e){return t=+t,r>>>=0,e||R(this,t,r,4,4294967295,0),this[r+3]=t>>>24,this[r+2]=t>>>16,this[r+1]=t>>>8,this[r]=255&t,r+4},f.prototype.writeUInt32BE=function(t,r,e){return t=+t,r>>>=0,e||R(this,t,r,4,4294967295,0),this[r]=t>>>24,this[r+1]=t>>>16,this[r+2]=t>>>8,this[r+3]=255&t,r+4},f.prototype.writeIntLE=function(t,r,e,n){if(t=+t,r>>>=0,!n){var i=Math.pow(2,8*e-1);R(this,t,r,e,i-1,-i)}var o=0,f=1,u=0;for(this[r]=255&t;++o>0)-u&255;return r+e},f.prototype.writeIntBE=function(t,r,e,n){if(t=+t,r>>>=0,!n){var i=Math.pow(2,8*e-1);R(this,t,r,e,i-1,-i)}var o=e-1,f=1,u=0;for(this[r+o]=255&t;--o>=0&&(f*=256);)t<0&&0===u&&0!==this[r+o+1]&&(u=1),this[r+o]=(t/f>>0)-u&255;return r+e},f.prototype.writeInt8=function(t,r,e){return t=+t,r>>>=0,e||R(this,t,r,1,127,-128),t<0&&(t=255+t+1),this[r]=255&t,r+1},f.prototype.writeInt16LE=function(t,r,e){return t=+t,r>>>=0,e||R(this,t,r,2,32767,-32768),this[r]=255&t,this[r+1]=t>>>8,r+2},f.prototype.writeInt16BE=function(t,r,e){return t=+t,r>>>=0,e||R(this,t,r,2,32767,-32768),this[r]=t>>>8,this[r+1]=255&t,r+2},f.prototype.writeInt32LE=function(t,r,e){return t=+t,r>>>=0,e||R(this,t,r,4,2147483647,-2147483648),this[r]=255&t,this[r+1]=t>>>8,this[r+2]=t>>>16,this[r+3]=t>>>24,r+4},f.prototype.writeInt32BE=function(t,r,e){return t=+t,r>>>=0,e||R(this,t,r,4,2147483647,-2147483648),t<0&&(t=4294967295+t+1),this[r]=t>>>24,this[r+1]=t>>>16,this[r+2]=t>>>8,this[r+3]=255&t,r+4},f.prototype.writeFloatLE=function(t,r,e){return M(this,t,r,!0,e)},f.prototype.writeFloatBE=function(t,r,e){return M(this,t,r,!1,e)},f.prototype.writeDoubleLE=function(t,r,e){return O(this,t,r,!0,e)},f.prototype.writeDoubleBE=function(t,r,e){return O(this,t,r,!1,e)},f.prototype.copy=function(t,r,e,n){if(!f.isBuffer(t))throw new TypeError("argument should be a Buffer");if(e||(e=0),n||0===n||(n=this.length),r>=t.length&&(r=t.length),r||(r=0),n>0&&n=this.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("sourceEnd out of bounds");n>this.length&&(n=this.length),t.length-r=0;--o)t[o+r]=this[o+e];else Uint8Array.prototype.set.call(t,this.subarray(e,n),r);return i},f.prototype.fill=function(t,r,e,n){if("string"==typeof t){if("string"==typeof r?(n=r,r=0,e=this.length):"string"==typeof e&&(n=e,e=this.length),void 0!==n&&"string"!=typeof n)throw new TypeError("encoding must be a string");if("string"==typeof n&&!f.isEncoding(n))throw new TypeError("Unknown encoding: "+n);if(1===t.length){var i=t.charCodeAt(0);("utf8"===n&&i<128||"latin1"===n)&&(t=i)}}else"number"==typeof t&&(t&=255);if(r<0||this.length>>=0,e=void 0===e?this.length:e>>>0,t||(t=0),"number"==typeof t)for(o=r;o55295&&e<57344){if(!i){if(e>56319){(r-=3)>-1&&o.push(239,191,189);continue}if(f+1===n){(r-=3)>-1&&o.push(239,191,189);continue}i=e;continue}if(e<56320){(r-=3)>-1&&o.push(239,191,189),i=e;continue}e=65536+(i-55296<<10|e-56320)}else i&&(r-=3)>-1&&o.push(239,191,189);if(i=null,e<128){if((r-=1)<0)break;o.push(e)}else if(e<2048){if((r-=2)<0)break;o.push(e>>6|192,63&e|128)}else if(e<65536){if((r-=3)<0)break;o.push(e>>12|224,e>>6&63|128,63&e|128)}else{if(!(e<1114112))throw new Error("Invalid code point");if((r-=4)<0)break;o.push(e>>18|240,e>>12&63|128,e>>6&63|128,63&e|128)}}return o}function j(t){return r.toByteArray(function(t){if((t=(t=t.split("=")[0]).trim().replace(k,"")).length<2)return"";for(;t.length%4!=0;)t+="=";return t}(t))}function z(t,r,e,n){for(var i=0;i=r.length||i>=t.length);++i)r[i+e]=t[i];return i}function D(t,r){return t instanceof r||null!=t&&null!=t.constructor&&null!=t.constructor.name&&t.constructor.name===r.name}function q(t){return t!=t}}).call(this)}).call(this,t("buffer").Buffer)},{"base64-js":2,buffer:3,ieee754:4}],4:[function(t,r,e){
9 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */
10 | e.read=function(t,r,e,n,i){var o,f,u=8*i-n-1,s=(1<>1,a=-7,c=e?i-1:0,p=e?-1:1,l=t[r+c];for(c+=p,o=l&(1<<-a)-1,l>>=-a,a+=u;a>0;o=256*o+t[r+c],c+=p,a-=8);for(f=o&(1<<-a)-1,o>>=-a,a+=n;a>0;f=256*f+t[r+c],c+=p,a-=8);if(0===o)o=1-h;else{if(o===s)return f?NaN:1/0*(l?-1:1);f+=Math.pow(2,n),o-=h}return(l?-1:1)*f*Math.pow(2,o-n)},e.write=function(t,r,e,n,i,o){var f,u,s,h=8*o-i-1,a=(1<>1,p=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,l=n?0:o-1,y=n?1:-1,g=r<0||0===r&&1/r<0?1:0;for(r=Math.abs(r),isNaN(r)||r===1/0?(u=isNaN(r)?1:0,f=a):(f=Math.floor(Math.log(r)/Math.LN2),r*(s=Math.pow(2,-f))<1&&(f--,s*=2),(r+=f+c>=1?p/s:p*Math.pow(2,1-c))*s>=2&&(f++,s/=2),f+c>=a?(u=0,f=a):f+c>=1?(u=(r*s-1)*Math.pow(2,i),f+=c):(u=r*Math.pow(2,c-1)*Math.pow(2,i),f=0));i>=8;t[e+l]=255&u,l+=y,u/=256,i-=8);for(f=f<0;t[e+l]=255&f,l+=y,f/=256,h-=8);t[e+l-y]|=128*g}},{}]},{},[1])(1)}));
11 |
--------------------------------------------------------------------------------
/lib/nft.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) Paul Burke 2021
3 | Github: @ipaulpro/bitcloutplus
4 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
5 | */
6 |
7 | const DESO_PUBLIC_KEY_PREFIX = 'BC1YL'
8 |
9 | const transferNftButtonId = 'plus-nft-transfer-button'
10 |
11 | let isRequestingNftEntries = false,
12 | transferNftUnlockableText = null,
13 | transferNftPostEntry = null,
14 | transferNftOwnedEntries = []
15 |
16 | const isTransferNftUrl = () => {
17 | const segments = new URL(document.location).pathname.split('/')
18 | return segments[1] === 'nft' && segments[segments.length - 1] === 'transfer'
19 | }
20 |
21 | const getNftPostPageFooter = (nftPostPage) => {
22 | const nftPost = nftPostPage.querySelector('nft-post')
23 |
24 | const feedPostElement = nftPost.querySelector('feed-post')
25 | if (!feedPostElement) return
26 |
27 | return feedPostElement.firstElementChild.lastElementChild
28 | }
29 |
30 | const enrichNftPostPage = (nftPostPage) => {
31 | if (!nftPostPage || isRequestingNftEntries) return
32 |
33 | if (document.getElementById(transferNftButtonId)) return
34 |
35 | const publicKey = getLoggedInPublicKey()
36 | const postHashHex = getPostHashHexFromUrl()
37 | if (!publicKey || !postHashHex) return
38 |
39 | const footerElement = getNftPostPageFooter(nftPostPage)
40 | if (!footerElement) return
41 |
42 | isRequestingNftEntries = true
43 |
44 | addNftExtraDataToNftPage(nftPostPage)
45 |
46 | getNftEntriesForPostHashHex(publicKey, postHashHex)
47 | .then(nftEntries => {
48 | if (document.getElementById(transferNftButtonId)) return
49 |
50 | const ownedEntries = nftEntries.filter(entry => entry['OwnerPublicKeyBase58Check'] === publicKey)
51 | if (ownedEntries.length === 0 || !isRequestingNftEntries) return
52 |
53 | const container = footerElement.firstElementChild
54 | container.firstElementChild.classList.add('flex-grow-1')
55 |
56 | const transferButton = createTransferNftButton(postHashHex)
57 | container.appendChild(transferButton)
58 |
59 | const ownedEntriesForSale = ownedEntries.filter(entry => entry['IsForSale'] === true)
60 | if (ownedEntriesForSale.length === ownedEntries.length) {
61 | transferButton.disabled = true
62 | transferButton.title = 'You cannot transfer an NFT that is for sale'
63 | }
64 | })
65 | .finally(() => {
66 | isRequestingNftEntries = false
67 | })
68 | }
69 |
70 | const signTransaction = (res) => {
71 | const transactionHex = res['TransactionHex']
72 | if (!transactionHex) {
73 | return new Error('Error creating transaction')
74 | }
75 |
76 | const identity = getCurrentIdentity()
77 | if (!identity) {
78 | return new Error('No Identity found')
79 | }
80 |
81 | const id = uuid()
82 | sendSignTransactionMsg(identity, transactionHex, id)
83 | }
84 |
85 | const onSerialNumberSelected = () => {
86 | const serialNumber = getSerialNumber()
87 |
88 | transferNftUnlockableText = null
89 |
90 | const nftEntry = transferNftOwnedEntries.find(entry => entry['SerialNumber'] === serialNumber)
91 | if (!nftEntry) return
92 |
93 | const encryptedUnlockableText = nftEntry['EncryptedUnlockableText']
94 | const lastOwnerPublicKey = nftEntry['LastOwnerPublicKeyBase58Check']
95 |
96 | if (encryptedUnlockableText) {
97 | sendDecryptMsg(encryptedUnlockableText, lastOwnerPublicKey)
98 | }
99 |
100 | const hasUnlockable = transferNftPostEntry['HasUnlockable']
101 | if (hasUnlockable) {
102 | const unlockableTextDiv = document.getElementById('plus-nft-transfer-unlockable-text')
103 | unlockableTextDiv.style.display = 'inherit'
104 | unlockableTextDiv.onclick = () => showUnlockableTextDialog().then((res) => {
105 | if (res.isConfirmed) {
106 | transferNftUnlockableText = res.value
107 | }
108 | })
109 | }
110 | }
111 |
112 | const createSerialNumberSelector = () => {
113 | if (transferNftOwnedEntries.length === 0) {
114 | throw new Error('User doesn\'t own any entries for this NFT.')
115 | }
116 |
117 | const serialNumberSelector = document.createElement('select')
118 | serialNumberSelector.id = 'plus_nft-serial-number-selector'
119 | serialNumberSelector.className = 'form-control w-auto'
120 | serialNumberSelector.addEventListener('change', onSerialNumberSelected)
121 |
122 | const options = []
123 | transferNftOwnedEntries.forEach(entry => {
124 | const option = document.createElement('option')
125 | option.value = entry['SerialNumber']
126 | option.innerText = `Serial #${entry['SerialNumber']}`
127 | options.push(option)
128 | })
129 | options.forEach(option => serialNumberSelector.appendChild(option))
130 |
131 | const unlockableTextIcon = document.createElement('i')
132 | unlockableTextIcon.className = 'fas fa-unlock-alt'
133 |
134 | const unlockableTextSpan = document.createElement('span')
135 | unlockableTextSpan.innerText = 'Unlockable text'
136 | unlockableTextSpan.className = 'ml-2'
137 |
138 | const encryptedUnlockableTextDiv = document.createElement('div')
139 | encryptedUnlockableTextDiv.id = 'plus-nft-transfer-unlockable-text'
140 | encryptedUnlockableTextDiv.className = 'ml-3 cursor-pointer align-items-center'
141 | encryptedUnlockableTextDiv.style.display = 'none'
142 | encryptedUnlockableTextDiv.appendChild(unlockableTextIcon)
143 | encryptedUnlockableTextDiv.appendChild(unlockableTextSpan)
144 |
145 | const container = document.createElement('div')
146 | container.className = 'flex-grow-1 d-flex align-items-center'
147 | container.appendChild(serialNumberSelector)
148 | container.appendChild(encryptedUnlockableTextDiv)
149 |
150 | return container
151 | }
152 |
153 | const createNftPostElement = (postEntry, notFoundElement, username, buttonElements, serialNumberElement, singleEntryDisplayed = false) => {
154 | const postDiv = document.createElement('div')
155 | postDiv.id = `plus-nft-post-${postEntry['PostHashHex']}`
156 | postDiv.className = 'feed-post__container js-feed-post-hover border d-flex justify-content-left w-100 px-15px pb-15px pt-15px feed-post__parent-post-font-size cursor-pointer'
157 | postDiv.onclick = () => window.location.href = `/nft/${postEntry['PostHashHex']}`
158 | if (singleEntryDisplayed) postDiv.classList.add('feed-post__blue-border')
159 |
160 | const avatarAnchor = document.createElement('a')
161 | avatarAnchor.className = 'feed-post__avatar br-12px'
162 | avatarAnchor.style.backgroundImage = `url("${getProfilePhotoUrlForPublicKey(postEntry['PosterPublicKeyBase58Check'])}")`
163 |
164 | const avatarContainer = document.createElement('div')
165 | avatarContainer.className = 'feed-post__avatar-container'
166 | avatarContainer.appendChild(avatarAnchor)
167 | postDiv.appendChild(avatarContainer)
168 |
169 | const contentInnerDiv = document.createElement('div')
170 | contentInnerDiv.className = 'roboto-regular mt-1'
171 | contentInnerDiv.style.overflowWrap = 'anywhere'
172 | contentInnerDiv.style.wordBreak = 'break-word'
173 | contentInnerDiv.style.outline = 'none'
174 | contentInnerDiv.innerText = postEntry['Body']
175 |
176 | const imageUrls = postEntry['ImageURLs']
177 | if (imageUrls && imageUrls.length > 0) {
178 | const contentImage = document.createElement('img')
179 | contentImage.className = 'feed-post__image'
180 | contentImage.src = imageUrls[0]
181 |
182 | const contentImageDiv = document.createElement('div')
183 | contentImageDiv.className = 'feed-post__image-container'
184 | contentImageDiv.appendChild(contentImage)
185 | contentInnerDiv.appendChild(contentImageDiv)
186 | }
187 |
188 | const usernameDiv = document.createElement('div')
189 | usernameDiv.className = 'fc-default font-weight-bold'
190 | usernameDiv.innerText = username
191 |
192 | const contentOuterDiv = document.createElement('div')
193 | contentOuterDiv.className = 'w-100'
194 | contentOuterDiv.appendChild(usernameDiv)
195 | contentOuterDiv.appendChild(contentInnerDiv)
196 | postDiv.appendChild(contentOuterDiv)
197 |
198 | const postFooterContentDiv = document.createElement('div')
199 | postFooterContentDiv.className = 'd-flex justify-content-between align-items-center'
200 | postFooterContentDiv.appendChild(serialNumberElement)
201 |
202 | if (buttonElements) {
203 | buttonElements.forEach(button => {
204 | postFooterContentDiv.appendChild(button)
205 | })
206 | }
207 |
208 | const postFooterDiv = document.createElement('div')
209 | postFooterDiv.className = 'p-15px fs-15px w-100 background-color-grey'
210 | if (singleEntryDisplayed) postFooterDiv.classList.add('feed-post__blue-border')
211 | postFooterDiv.appendChild(postFooterContentDiv)
212 | notFoundElement.appendChild(postDiv)
213 | notFoundElement.appendChild(postFooterDiv)
214 |
215 | const padding = document.createElement('div')
216 | padding.className = 'w-100'
217 | padding.classList.add(singleEntryDisplayed ? 'p-35px' : 'p-1')
218 |
219 | const postContainerDiv = document.createElement('div')
220 | postContainerDiv.className = 'feed-post__container w-100 px-15px'
221 | if (!singleEntryDisplayed) postContainerDiv.classList.add('pt-15px')
222 | postContainerDiv.appendChild(postDiv)
223 | postContainerDiv.appendChild(postFooterDiv)
224 | postContainerDiv.appendChild(padding)
225 |
226 | notFoundElement.appendChild(postContainerDiv)
227 | }
228 |
229 | const setCustomPageTopBarTitle = (title) => {
230 | const topBar = document.querySelector(`top-bar-mobile-navigation-control`)
231 | if (!topBar) return
232 |
233 | const titleElement = topBar.parentElement
234 | titleElement.innerText = title
235 | }
236 |
237 | const getCustomPageNotFoundElement = () => {
238 | const notFoundElement = document.querySelector(`not-found`)
239 | const notFoundContentContainer = document.querySelector(`.not-found__content-container`)
240 | notFoundElement.removeChild(notFoundContentContainer)
241 | return notFoundElement
242 | }
243 |
244 | function showSpinner(button) {
245 | const spinnerAlt = document.createElement('span')
246 | spinnerAlt.className = 'sr-only'
247 | spinnerAlt.innerText = 'Working...'
248 |
249 | const spinner = document.createElement('div')
250 | spinner.className = 'spinner-border spinner-border-sm text-light'
251 | spinner.dataset.role = 'status'
252 | spinner.appendChild(spinnerAlt)
253 |
254 | button.disabled = true
255 | button.innerText = ''
256 | button.appendChild(spinner)
257 | }
258 |
259 | const getSerialNumber = () => {
260 | const serialNumberSelector = document.getElementById('plus_nft-serial-number-selector')
261 | if (!serialNumberSelector) return undefined
262 |
263 | return Number(serialNumberSelector.value)
264 | }
265 |
266 | const createTransferNftButton = (postHashHex) => {
267 | const button = document.createElement('button')
268 | button.id = transferNftButtonId
269 | button.type = 'button'
270 | button.className = 'btn btn-info font-weight-bold br-8px fs-13px mx-3'
271 | button.innerText = 'Transfer NFT'
272 | button.onclick = () => window.location.href = `/nft/${postHashHex}/transfer`
273 | return button
274 | }
275 |
276 | function createCustomPageHeaderElement(text) {
277 | const headerElement = document.createElement('div')
278 | headerElement.className = 'd-flex align-items-center fs-15px fc-muted p-15px background-color-light-grey'
279 | headerElement.innerText = text
280 | return headerElement
281 | }
282 |
283 | function addPostToBody(post, confirmTextElement, notFoundElement, buttons, singleEntryDisplayed) {
284 | getProfileByPublicKey(post['PosterPublicKeyBase58Check'])
285 | .then(profile => {
286 | const username = profile['Username']
287 | try {
288 | const serialNumberSelector = createSerialNumberSelector()
289 | createNftPostElement(post, notFoundElement, username, buttons, serialNumberSelector, singleEntryDisplayed)
290 | onSerialNumberSelected()
291 | } catch (e) {
292 | confirmTextElement.innerText = 'You don\'t own this NFT'
293 | }
294 | })
295 | }
296 |
297 | const search = (text, cb) => {
298 | if (text.startsWith(DESO_PUBLIC_KEY_PREFIX)) {
299 | return getProfileByPublicKey(text).then(profile => {
300 | const profiles = [profile]
301 | return cb(profiles)
302 | })
303 | } else {
304 | return searchUsernames(text, profiles => {
305 | return cb(profiles)
306 | })
307 | }
308 | }
309 |
310 | const getLookupKey = (item, text) => {
311 | if (text.startsWith(DESO_PUBLIC_KEY_PREFIX)) {
312 | return item['PublicKeyBase58Check']
313 | } else {
314 | return item['Username']
315 | }
316 | }
317 |
318 | const addAutocomplete = (input) => {
319 | const tribute = new Tribute({
320 | autocompleteMode: true,
321 | replaceTextSuffix: '',
322 | values: (text, cb) => search(text, cb),
323 | menuItemTemplate: (item) => buildTributeUsernameMenuTemplate(item),
324 | loadingItemTemplate: buildLoadingItemTemplate(),
325 | fillAttr: 'Username',
326 | lookup: (item, text) => getLookupKey(item, text)
327 | })
328 | tribute.attach(input)
329 | }
330 |
331 | const createNftTransfersSearchArea = () => {
332 | const searchBarIcon = document.createElement('i')
333 | searchBarIcon.className = 'icon-search'
334 |
335 | const searchBarIconSpan = document.createElement('span')
336 | searchBarIconSpan.className = 'input-group-text search-bar__icon'
337 | searchBarIconSpan.style.borderTopLeftRadius = '0.25rem'
338 | searchBarIconSpan.style.borderBottomLeftRadius = '0.25rem'
339 | searchBarIconSpan.appendChild(searchBarIcon)
340 |
341 | const input = document.createElement('input')
342 | input.id = 'plus-nft-recipient-input'
343 | input.type = 'text'
344 | input.placeholder = 'Search'
345 | input.className = 'form-control shadow-none search-bar__fix-active'
346 | input.style.fontSize = '15px'
347 | input.style.paddingLeft = '0'
348 | input.style.borderLeftColor = 'rgba(0, 0, 0, 0)'
349 |
350 | const inputGroupPrepend = document.createElement('div')
351 | inputGroupPrepend.className = 'input-group-prepend w-100'
352 | inputGroupPrepend.appendChild(searchBarIconSpan)
353 | inputGroupPrepend.appendChild(input)
354 |
355 | const inputGroup = document.createElement('div')
356 | inputGroup.className = 'input-group'
357 | inputGroup.appendChild(inputGroupPrepend)
358 |
359 | const innerDiv = document.createElement('div')
360 | innerDiv.className = 'd-flex align-items-center w-100 text-grey8A fs-15px global__top-bar__height'
361 | innerDiv.appendChild(inputGroup)
362 |
363 | const searchBar = document.createElement('div')
364 | searchBar.className = 'w-100 global__top-bar__height'
365 | searchBar.appendChild(innerDiv)
366 |
367 | const userSelectDiv = document.createElement('div')
368 | userSelectDiv.className = 'fs-15px font-weight-bold mt-4 px-15px'
369 | userSelectDiv.innerText = 'Recipient public key or username'
370 | userSelectDiv.appendChild(searchBar)
371 |
372 | addAutocomplete(input)
373 |
374 | return userSelectDiv
375 | }
376 |
377 | const showConfirmTransferDialog = (text) => {
378 | return Swal.fire({
379 | title: 'Transfer NFT?',
380 | icon: 'warning',
381 | text: `Are you sure you want to transfer this NFT to ${text}? This cannot be undone.`,
382 | confirmButtonText: 'Transfer',
383 | showCancelButton: true,
384 | reverseButtons: true,
385 | customClass: {
386 | confirmButton: 'btn btn-light',
387 | cancelButton: 'btn btn-light no'
388 | }
389 | })
390 | }
391 |
392 | const showUnlockableTextDialog = () => {
393 | let config = {
394 | title: 'Unlockable content',
395 | showConfirmButton: true,
396 | customClass: {
397 | confirmButton: 'btn btn-light',
398 | cancelButton: 'btn btn-light no'
399 | }
400 | }
401 |
402 | config = {
403 | ...config,
404 | input: 'textarea',
405 | inputLabel: 'Add or edit the unlockable content.',
406 | inputPlaceholder: 'Enter URL, code to redeem, link, etc...',
407 | inputValue: transferNftUnlockableText,
408 | confirmButtonText: 'Set',
409 | showCancelButton: true,
410 | reverseButtons: true,
411 | focusConfirm: false,
412 | inputValidator: (value) => {
413 | if (!value) {
414 | return 'You need to write something!'
415 | }
416 | }
417 | }
418 |
419 | return Swal.fire(config)
420 | }
421 |
422 | const sendEncryptMsg = (recipientPublicKey, text) => {
423 | const identity = getCurrentIdentity()
424 |
425 | const payload = {
426 | recipientPublicKey: recipientPublicKey,
427 | message: text
428 | }
429 |
430 | if (identity) {
431 | payload.accessLevel = identity.accessLevel
432 | payload.accessLevelHmac = identity.accessLevelHmac
433 | payload.encryptedSeedHex = identity.encryptedSeedHex
434 | }
435 |
436 | pendingIdentityMessageId = uuid()
437 |
438 | postIdentityMessage(pendingIdentityMessageId, 'encrypt', payload)
439 | }
440 |
441 | const sendDecryptMsg = (text, lastOwnerPublicKey) => {
442 | const identity = getCurrentIdentity()
443 |
444 | const payload = {
445 | encryptedMessages: [{
446 | EncryptedHex: text,
447 | PublicKey: lastOwnerPublicKey
448 | }]
449 | }
450 |
451 | if (identity) {
452 | payload.accessLevel = identity.accessLevel
453 | payload.accessLevelHmac = identity.accessLevelHmac
454 | payload.encryptedSeedHex = identity.encryptedSeedHex
455 | }
456 |
457 | pendingIdentityMessageId = uuid()
458 | postIdentityMessage(pendingIdentityMessageId, 'decrypt', payload)
459 | }
460 |
461 | const getNftTransferRecipientFromInput = () => {
462 | const input = document.getElementById('plus-nft-recipient-input')
463 | if (!input) return undefined
464 |
465 | return input.value && input.value.trim()
466 | }
467 |
468 | const getRecipientProfileFromInput = () => {
469 | const recipient = getNftTransferRecipientFromInput()
470 | if (!recipient || recipient.length === 0) return undefined
471 | const textIsPublicKey = recipient.startsWith(DESO_PUBLIC_KEY_PREFIX)
472 | return textIsPublicKey ? getProfileByPublicKey(recipient) : getProfileByUsername(recipient)
473 | }
474 |
475 | const restoreTransferButton = () => {
476 | const button = document.getElementById(transferNftButtonId)
477 | button.disabled = false
478 | button.innerText = 'Transfer'
479 | }
480 |
481 | const initTransfer = (senderPublicKey, postHashHex, recipient, encryptedUnlockableText) => {
482 | showConfirmTransferDialog(recipient)
483 | .then((res) => {
484 | if (res.isConfirmed) {
485 | const textIsPublicKey = recipient.startsWith(DESO_PUBLIC_KEY_PREFIX)
486 | return textIsPublicKey ? getProfileByPublicKey(recipient) : getProfileByUsername(recipient)
487 | } else {
488 | restoreTransferButton()
489 | }
490 | })
491 | .then(profile => {
492 | const recipientPublicKey = profile['PublicKeyBase58Check']
493 | if (!recipientPublicKey) return Promise.reject(`Unable to retrieve profile for ${recipient}`)
494 | const serialNumber = getSerialNumber()
495 | return transferNft(senderPublicKey, recipientPublicKey, postHashHex, serialNumber, encryptedUnlockableText)
496 | })
497 | .then(signTransaction)
498 | .catch(err => {
499 | console.error(err)
500 | restoreTransferButton()
501 | })
502 | }
503 |
504 | const onNftTransferUnlockableEncrypted = (encryptedUnlockableText) => {
505 | const recipient = getNftTransferRecipientFromInput()
506 | if (!recipient || recipient.length === 0) return
507 |
508 | const publicKey = getLoggedInPublicKey()
509 | const postHashHex = getPostHashHexFromUrl()
510 | if (!publicKey || !postHashHex) return
511 |
512 | initTransfer(publicKey, postHashHex, recipient, encryptedUnlockableText)
513 | }
514 |
515 | const onNftTransferUnlockableDecrypted = (unlockableText) => {
516 | transferNftUnlockableText = unlockableText
517 | }
518 |
519 | const createTransferButton = (senderPublicKey, postHashHex) => {
520 | const transferButton = document.createElement('button')
521 | transferButton.id = transferNftButtonId
522 | transferButton.type = 'button'
523 | transferButton.className = 'btn btn-primary font-weight-bold br-8px fs-13px ml-3'
524 | transferButton.innerText = 'Transfer'
525 | transferButton.onclick = () => {
526 | const recipient = getNftTransferRecipientFromInput()
527 | if (!recipient || recipient.length === 0) return
528 |
529 | showSpinner(transferButton)
530 |
531 | const hasUnlockable = transferNftPostEntry['HasUnlockable']
532 |
533 | if (hasUnlockable) {
534 | getRecipientProfileFromInput().then(profile => {
535 | const recipientPublicKey = profile['PublicKeyBase58Check']
536 | if (transferNftUnlockableText) {
537 | // Already have decrypted unlockable text
538 | sendEncryptMsg(recipientPublicKey, transferNftUnlockableText)
539 | } else {
540 | // Entry requires unlockable text, but we don't have any
541 | showUnlockableTextDialog().then(res => {
542 | if (res.isConfirmed) {
543 | sendEncryptMsg(recipientPublicKey, res.value)
544 | } else {
545 | restoreTransferButton()
546 | }
547 | })
548 | }
549 | })
550 | } else {
551 | initTransfer(senderPublicKey, postHashHex, recipient)
552 | }
553 | }
554 |
555 | return transferButton
556 | }
557 |
558 | const createTransferNftPage = () => {
559 | transferNftUnlockableText = null
560 | transferNftPostEntry = null
561 | transferNftOwnedEntries = []
562 |
563 | setCustomPageTopBarTitle('Transfer an NFT')
564 |
565 | const notFoundElement = getCustomPageNotFoundElement()
566 |
567 | const headerElement = createCustomPageHeaderElement(
568 | 'The recipient may accept the transfer, return it, or burn the NFT to reject it. You cannot undo this.'
569 | )
570 | notFoundElement.appendChild(headerElement)
571 |
572 | const userSelectDiv = createNftTransfersSearchArea()
573 | notFoundElement.appendChild(userSelectDiv)
574 |
575 | const postHashHex = getPostHashHexFromUrl()
576 | const publicKey = getLoggedInPublicKey()
577 | if (!publicKey || !postHashHex) return
578 |
579 | getBidsForNftPost(publicKey, postHashHex)
580 | .then(res => {
581 | transferNftPostEntry = res['PostEntryResponse']
582 | transferNftOwnedEntries = res['NFTEntryResponses'].filter(entry => entry['OwnerPublicKeyBase58Check'] === publicKey)
583 | const transferButton = createTransferButton(publicKey, postHashHex)
584 | addPostToBody(transferNftPostEntry, headerElement, notFoundElement, [transferButton], true)
585 |
586 | let params = (new URL(document.location)).searchParams
587 | let recipientPublicKey = params.get("publicKey")
588 | if (recipientPublicKey) return getProfileByPublicKey(recipientPublicKey)
589 | })
590 | .then(profile => {
591 | if (profile) {
592 | const input = document.getElementById('plus-nft-recipient-input')
593 | if (input) input.value = profile['Username']
594 | }
595 | })
596 | }
597 |
598 | const addNftExtraDataToNftPage = (nftPostPage) => {
599 | const extraDataLinkId = 'plus_nft-extra-data-link'
600 | if (document.getElementById(extraDataLinkId)) return
601 |
602 | const postHashHex = getPostHashHexFromUrl()
603 | getSinglePost(postHashHex)
604 | .then(data => data['PostFound']['PostExtraData'])
605 | .then(postExtraData => {
606 | if (!postExtraData || isEmpty(postExtraData) || document.getElementById(extraDataLinkId)) return
607 |
608 | const json = document.createElement('pre')
609 | json.innerText = JSON.stringify(postExtraData, null, 2)
610 | json.className = 'plus-text-primary d-none p-3 rounded mt-2 border-secondary'
611 | json.style.border = '1px solid'
612 | json.style.whiteSpace = 'pre'
613 |
614 | const link = document.createElement('div')
615 | link.id = extraDataLinkId
616 | link.className = 'cursor-pointer'
617 | link.innerText = "Show Extra Data"
618 | link.onclick = () => {
619 | if (json.classList.contains('d-none')) {
620 | json.classList.remove('d-none')
621 | link.innerText = "Hide Extra Data"
622 | } else {
623 | json.classList.add('d-none')
624 | link.innerText = "Show Extra Data"
625 | }
626 | }
627 |
628 | const footer = getNftPostPageFooter(nftPostPage)
629 | footer?.appendChild(link)
630 | footer?.appendChild(json)
631 | })
632 | .catch()
633 | }
634 |
--------------------------------------------------------------------------------
/vendor/tribute/tribute.min.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).Tribute=t()}(this,(function(){"use strict";function e(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function t(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,i=new Array(t);n>>0,r=arguments[1],o=0;o container for the click");n.selectItemAtIndex(i.getAttribute("data-index"),t),n.hideMenu()}else n.current.element&&!n.current.externalTrigger&&(n.current.externalTrigger=!1,setTimeout((function(){return n.hideMenu()})))}},{key:"keyup",value:function(e,t){if(e.inputEvent&&(e.inputEvent=!1),e.updateSelection(this),27!==t.keyCode){if(!e.tribute.allowSpaces&&e.tribute.hasTrailingSpace)return e.tribute.hasTrailingSpace=!1,e.commandEvent=!0,void e.callbacks().space(t,this);if(!e.tribute.isActive)if(e.tribute.autocompleteMode)e.callbacks().triggerChar(t,this,"");else{var n=e.getKeyCode(e,this,t);if(isNaN(n)||!n)return;var i=e.tribute.triggers().find((function(e){return e.charCodeAt(0)===n}));void 0!==i&&e.callbacks().triggerChar(t,this,i)}e.tribute.current.mentionText.length=r.current.collection.menuShowMinLength&&r.inputEvent&&r.showMenuFor(n,!0)},enter:function(t,n){e.tribute.isActive&&e.tribute.current.filteredItems&&(t.preventDefault(),t.stopPropagation(),setTimeout((function(){e.tribute.selectItemAtIndex(e.tribute.menuSelected,t),e.tribute.hideMenu()}),0))},escape:function(t,n){e.tribute.isActive&&(t.preventDefault(),t.stopPropagation(),e.tribute.isActive=!1,e.tribute.hideMenu())},tab:function(t,n){e.callbacks().enter(t,n)},space:function(t,n){e.tribute.isActive&&(e.tribute.spaceSelectsMatch?e.callbacks().enter(t,n):e.tribute.allowSpaces||(t.stopPropagation(),setTimeout((function(){e.tribute.hideMenu(),e.tribute.isActive=!1}),0)))},up:function(t,n){if(e.tribute.isActive&&e.tribute.current.filteredItems){t.preventDefault(),t.stopPropagation();var i=e.tribute.current.filteredItems.length,r=e.tribute.menuSelected;i>r&&r>0?(e.tribute.menuSelected--,e.setActiveLi()):0===r&&(e.tribute.menuSelected=i-1,e.setActiveLi(),e.tribute.menu.scrollTop=e.tribute.menu.scrollHeight)}},down:function(t,n){if(e.tribute.isActive&&e.tribute.current.filteredItems){t.preventDefault(),t.stopPropagation();var i=e.tribute.current.filteredItems.length-1,r=e.tribute.menuSelected;i>r?(e.tribute.menuSelected++,e.setActiveLi()):i===r&&(e.tribute.menuSelected=0,e.setActiveLi(),e.tribute.menu.scrollTop=0)}},delete:function(t,n){e.tribute.isActive&&e.tribute.current.mentionText.length<1?e.tribute.hideMenu():e.tribute.isActive&&e.tribute.showMenuFor(n)}}}},{key:"setActiveLi",value:function(e){var t=this.tribute.menu.querySelectorAll("li"),n=t.length>>>0;e&&(this.tribute.menuSelected=parseInt(e));for(var i=0;iu.bottom){var l=o.bottom-u.bottom;this.tribute.menu.scrollTop+=l}else if(o.topi.width&&(r.left||r.right),u=window.innerHeight>i.height&&(r.top||r.bottom);(o||u)&&(n.tribute.menu.style.cssText="display: none",n.positionMenuAtCaret(e))}),0)}else this.tribute.menu.style.cssText="display: none"}},{key:"selectElement",value:function(e,t,n){var i,r=e;if(t)for(var o=0;o=0&&(t=i.substring(0,r))}}else{var o=this.tribute.current.element;if(o){var u=o.selectionStart;o.value&&u>=0&&(t=o.value.substring(0,u))}}return t}},{key:"getLastWordInText",value:function(e){var t;return e=e.replace(/\u00A0/g," "),(t=this.tribute.autocompleteSeparator?e.split(this.tribute.autocompleteSeparator):e.split(/\s+/))[t.length-1].trim()}},{key:"getTriggerInfo",value:function(e,t,n,i,r){var o,u,l,a=this,s=this.tribute.current;if(this.isContentEditable(s.element)){var c=this.getContentEditableSelectedPath(s);c&&(o=c.selected,u=c.path,l=c.offset)}else o=this.tribute.current.element;var h=this.getTextPrecedingCurrentSelection(),d=this.getLastWordInText(h);if(r)return{mentionPosition:h.length-d.length,mentionText:d,mentionSelectedElement:o,mentionSelectedPath:u,mentionSelectedOffset:l};if(null!=h){var f,m=-1;if(this.tribute.collection.forEach((function(e){var t=e.trigger,i=e.requireLeadingSpace?a.lastIndexWithLeadingSpace(h,t):h.lastIndexOf(t);i>m&&(m=i,f=t,n=e.requireLeadingSpace)})),m>=0&&(0===m||!n||/[\xA0\s]/g.test(h.substring(m-1,m)))){var p=h.substring(m+f.length,h.length);f=h.substring(m,m+f.length);var v=p.substring(0,1),g=p.length>0&&(" "===v||" "===v);t&&(p=p.trim());var b=i?/[^\S ]/g:/[\xA0\s]/g;if(this.tribute.hasTrailingSpace=b.test(p),!g&&(e||!b.test(p)))return{mentionPosition:m,mentionText:p,mentionSelectedElement:o,mentionSelectedPath:u,mentionSelectedOffset:l,mentionTriggerChar:f}}}}},{key:"lastIndexWithLeadingSpace",value:function(e,t){for(var n=e.split("").reverse().join(""),i=-1,r=0,o=e.length;r=0;s--)if(t[s]!==n[r-s]){a=!1;break}if(a&&(u||l)){i=e.length-1-r;break}}return i}},{key:"isContentEditable",value:function(e){return"INPUT"!==e.nodeName&&"TEXTAREA"!==e.nodeName}},{key:"isMenuOffScreen",value:function(e,t){var n=window.innerWidth,i=window.innerHeight,r=document.documentElement,o=(window.pageXOffset||r.scrollLeft)-(r.clientLeft||0),u=(window.pageYOffset||r.scrollTop)-(r.clientTop||0),l="number"==typeof e.top?e.top:u+i-e.bottom-t.height,a="number"==typeof e.right?e.right:e.left+t.width,s="number"==typeof e.bottom?e.bottom:e.top+t.height,c="number"==typeof e.left?e.left:o+n-e.right-t.width;return{top:lMath.ceil(o+n),bottom:s>Math.ceil(u+i),left:cparseInt(u.height)&&(o.overflowY="scroll")):o.overflow="hidden",r.textContent=e.value.substring(0,t),"INPUT"===e.nodeName&&(r.textContent=r.textContent.replace(/\s/g," "));var l=this.getDocument().createElement("span");l.textContent=e.value.substring(t)||".",r.appendChild(l);var a=e.getBoundingClientRect(),s=document.documentElement,c=(window.pageXOffset||s.scrollLeft)-(s.clientLeft||0),h=(window.pageYOffset||s.scrollTop)-(s.clientTop||0),d=0,f=0;this.menuContainerIsBody&&(d=a.top,f=a.left);var m={top:d+h+l.offsetTop+parseInt(u.borderTopWidth)+parseInt(u.fontSize)-e.scrollTop,left:f+c+l.offsetLeft+parseInt(u.borderLeftWidth)},p=window.innerWidth,v=window.innerHeight,g=this.getMenuDimensions(),b=this.isMenuOffScreen(m,g);b.right&&(m.right=p-m.left,m.left="auto");var y=this.tribute.menuContainer?this.tribute.menuContainer.offsetHeight:this.getDocument().body.offsetHeight;if(b.bottom){var w=y-(v-(this.tribute.menuContainer?this.tribute.menuContainer.getBoundingClientRect():this.getDocument().body.getBoundingClientRect()).top);m.bottom=w+(v-a.top-l.offsetTop),m.top="auto"}return(b=this.isMenuOffScreen(m,g)).left&&(m.left=p>g.width?c+p-g.width:c,delete m.right),b.top&&(m.top=v>g.height?h+v-g.height:h,delete m.bottom),this.getDocument().body.removeChild(r),m}},{key:"getContentEditableCaretPosition",value:function(e){var t,n=this.getWindowSelection();(t=this.getDocument().createRange()).setStart(n.anchorNode,e),t.setEnd(n.anchorNode,e),t.collapse(!1);var i=t.getBoundingClientRect(),r=document.documentElement,o=(window.pageXOffset||r.scrollLeft)-(r.clientLeft||0),u=(window.pageYOffset||r.scrollTop)-(r.clientTop||0),l={left:i.left+o,top:i.top+i.height+u},a=window.innerWidth,s=window.innerHeight,c=this.getMenuDimensions(),h=this.isMenuOffScreen(l,c);h.right&&(l.left="auto",l.right=a-i.left-o);var d=this.tribute.menuContainer?this.tribute.menuContainer.offsetHeight:this.getDocument().body.offsetHeight;if(h.bottom){var f=d-(s-(this.tribute.menuContainer?this.tribute.menuContainer.getBoundingClientRect():this.getDocument().body.getBoundingClientRect()).top);l.top="auto",l.bottom=f+(s-i.top)}return(h=this.isMenuOffScreen(l,c)).left&&(l.left=a>c.width?o+a-c.width:o,delete l.right),h.top&&(l.top=s>c.height?u+s-c.height:u,delete l.bottom),this.menuContainerIsBody||(l.left=l.left?l.left-this.tribute.menuContainer.offsetLeft:l.left,l.top=l.top?l.top-this.tribute.menuContainer.offsetTop:l.top),l}},{key:"scrollIntoView",value:function(e){var t,n=this.menu;if(void 0!==n){for(;void 0===t||0===t.height;)if(0===(t=n.getBoundingClientRect()).height&&(void 0===(n=n.childNodes[0])||!n.getBoundingClientRect))return;var i=t.top,r=i+t.height;if(i<0)window.scrollTo(0,window.pageYOffset+t.top-20);else if(r>window.innerHeight){var o=window.pageYOffset+t.top-20;o-window.pageYOffset>100&&(o=window.pageYOffset+100);var u=window.pageYOffset-(window.innerHeight-r);u>o&&(u=o),window.scrollTo(0,u)}}}},{key:"menuContainerIsBody",get:function(){return this.tribute.menuContainer===document.body||!this.tribute.menuContainer}}]),t}(),s=function(){function t(n){e(this,t),this.tribute=n,this.tribute.search=this}return n(t,[{key:"simpleFilter",value:function(e,t){var n=this;return t.filter((function(t){return n.test(e,t)}))}},{key:"test",value:function(e,t){return null!==this.match(e,t)}},{key:"match",value:function(e,t,n){n=n||{};t.length;var i=n.pre||"",r=n.post||"",o=n.caseSensitive&&t||t.toLowerCase();if(n.skip)return{rendered:t,score:0};e=n.caseSensitive&&e||e.toLowerCase();var u=this.traverse(o,e,0,0,[]);return u?{rendered:this.render(t,u.cache,i,r),score:u.score}:null}},{key:"traverse",value:function(e,t,n,i,r){if(this.tribute.autocompleteSeparator&&(t=t.split(this.tribute.autocompleteSeparator).splice(-1)[0]),t.length===i)return{score:this.calculateScore(r),cache:r.slice()};if(!(e.length===n||t.length-i>e.length-n)){for(var o,u,l=t[i],a=e.indexOf(l,n);a>-1;){if(r.push(a),u=this.traverse(e,t,a+1,i+1,r),r.pop(),!u)return o;(!o||o.score0&&(e[r-1]+1===i?n+=n+1:n=1),t+=n})),t}},{key:"render",value:function(e,t,n,i){var r=e.substring(0,t[0]);return t.forEach((function(o,u){r+=n+e[o]+i+e.substring(o+1,t[u+1]?t[u+1]:e.length)})),r}},{key:"filter",value:function(e,t,n){var i=this;return n=n||{},t.reduce((function(t,r,o,u){var l=r;n.extract&&((l=n.extract(r))||(l=""));var a=i.match(e,l,n);return null!=a&&(t[t.length]={string:a.rendered,score:a.score,index:o,original:r}),t}),[]).sort((function(e,t){var n=t.score-e.score;return n||e.index-t.index}))}}]),t}();return function(){function t(n){var i,r=this,o=n.values,c=void 0===o?null:o,h=n.loadingItemTemplate,d=void 0===h?null:h,f=n.iframe,m=void 0===f?null:f,p=n.selectClass,v=void 0===p?"highlight":p,g=n.containerClass,b=void 0===g?"tribute-container":g,y=n.itemClass,w=void 0===y?"":y,T=n.trigger,C=void 0===T?"@":T,S=n.autocompleteMode,E=void 0!==S&&S,k=n.autocompleteSeparator,x=void 0===k?null:k,M=n.selectTemplate,A=void 0===M?null:M,L=n.menuItemTemplate,I=void 0===L?null:L,N=n.lookup,O=void 0===N?"key":N,D=n.fillAttr,P=void 0===D?"value":D,R=n.collection,W=void 0===R?null:R,H=n.menuContainer,B=void 0===H?null:H,F=n.noMatchTemplate,_=void 0===F?null:F,j=n.requireLeadingSpace,Y=void 0===j||j,z=n.allowSpaces,K=void 0!==z&&z,q=n.replaceTextSuffix,U=void 0===q?null:q,X=n.positionMenu,Q=void 0===X||X,V=n.spaceSelectsMatch,$=void 0!==V&&V,G=n.searchOpts,J=void 0===G?{}:G,Z=n.menuItemLimit,ee=void 0===Z?null:Z,te=n.menuShowMinLength,ne=void 0===te?0:te;if(e(this,t),this.autocompleteMode=E,this.autocompleteSeparator=x,this.menuSelected=0,this.current={},this.inputEvent=!1,this.isActive=!1,this.menuContainer=B,this.allowSpaces=K,this.replaceTextSuffix=U,this.positionMenu=Q,this.hasTrailingSpace=!1,this.spaceSelectsMatch=$,this.autocompleteMode&&(C="",K=!1),c)this.collection=[{trigger:C,iframe:m,selectClass:v,containerClass:b,itemClass:w,selectTemplate:(A||t.defaultSelectTemplate).bind(this),menuItemTemplate:(I||t.defaultMenuItemTemplate).bind(this),noMatchTemplate:(i=_,"string"==typeof i?""===i.trim()?null:i:"function"==typeof i?i.bind(r):_||function(){return"No Match Found! "}.bind(r)),lookup:O,fillAttr:P,values:c,loadingItemTemplate:d,requireLeadingSpace:Y,searchOpts:J,menuItemLimit:ee,menuShowMinLength:ne}];else{if(!W)throw new Error("[Tribute] No collection specified.");this.autocompleteMode&&console.warn("Tribute in autocomplete mode does not work for collections"),this.collection=W.map((function(e){return{trigger:e.trigger||C,iframe:e.iframe||m,selectClass:e.selectClass||v,containerClass:e.containerClass||b,itemClass:e.itemClass||w,selectTemplate:(e.selectTemplate||t.defaultSelectTemplate).bind(r),menuItemTemplate:(e.menuItemTemplate||t.defaultMenuItemTemplate).bind(r),noMatchTemplate:function(e){return"string"==typeof e?""===e.trim()?null:e:"function"==typeof e?e.bind(r):_||function(){return"No Match Found! "}.bind(r)}(_),lookup:e.lookup||O,fillAttr:e.fillAttr||P,values:e.values,loadingItemTemplate:e.loadingItemTemplate,requireLeadingSpace:e.requireLeadingSpace,searchOpts:e.searchOpts||J,menuItemLimit:e.menuItemLimit||ee,menuShowMinLength:e.menuShowMinLength||ne}}))}new a(this),new u(this),new l(this),new s(this)}return n(t,[{key:"triggers",value:function(){return this.collection.map((function(e){return e.trigger}))}},{key:"attach",value:function(e){if(!e)throw new Error("[Tribute] Must pass in a DOM node or NodeList.");if("undefined"!=typeof jQuery&&e instanceof jQuery&&(e=e.get()),e.constructor===NodeList||e.constructor===HTMLCollection||e.constructor===Array)for(var t=e.length,n=0;n",post:n.current.collection.searchOpts.post||"",skip:n.current.collection.searchOpts.skip,extract:function(e){if("string"==typeof n.current.collection.lookup)return e[n.current.collection.lookup];if("function"==typeof n.current.collection.lookup)return n.current.collection.lookup(e,n.current.mentionText);throw new Error("Invalid lookup attribute, lookup must be string or function.")}});n.current.collection.menuItemLimit&&(r=r.slice(0,n.current.collection.menuItemLimit)),n.current.filteredItems=r;var o=n.menu.querySelector("ul");if(n.range.positionMenuAtCaret(t),!r.length){var u=new CustomEvent("tribute-no-match",{detail:n.menu});return n.current.element.dispatchEvent(u),void("function"==typeof n.current.collection.noMatchTemplate&&!n.current.collection.noMatchTemplate()||!n.current.collection.noMatchTemplate?n.hideMenu():"function"==typeof n.current.collection.noMatchTemplate?o.innerHTML=n.current.collection.noMatchTemplate():o.innerHTML=n.current.collection.noMatchTemplate)}o.innerHTML="";var l=n.range.getDocument().createDocumentFragment();r.forEach((function(e,t){var r=n.range.getDocument().createElement("li");r.setAttribute("data-index",t),r.className=n.current.collection.itemClass,r.addEventListener("mousemove",(function(e){var t=i(n._findLiTarget(e.target),2),r=(t[0],t[1]);0!==e.movementY&&n.events.setActiveLi(r)})),n.menuSelected===t&&r.classList.add(n.current.collection.selectClass),r.innerHTML=n.current.collection.menuItemTemplate(e),l.appendChild(r)})),o.appendChild(l)}};"function"==typeof this.current.collection.values?(this.current.collection.loadingItemTemplate&&(this.menu.querySelector("ul").innerHTML=this.current.collection.loadingItemTemplate,this.range.positionMenuAtCaret(t)),this.current.collection.values(this.current.mentionText,r)):r(this.current.collection.values)}}},{key:"_findLiTarget",value:function(e){if(!e)return[];var t=e.getAttribute("data-index");return t?[e,t]:this._findLiTarget(e.parentNode)}},{key:"showMenuForCollection",value:function(e,t){e!==document.activeElement&&this.placeCaretAtEnd(e),this.current.collection=this.collection[t||0],this.current.externalTrigger=!0,this.current.element=e,e.isContentEditable?this.insertTextAtCursor(this.current.collection.trigger):this.insertAtCaret(e,this.current.collection.trigger),this.showMenuFor(e)}},{key:"placeCaretAtEnd",value:function(e){if(e.focus(),void 0!==window.getSelection&&void 0!==document.createRange){var t=document.createRange();t.selectNodeContents(e),t.collapse(!1);var n=window.getSelection();n.removeAllRanges(),n.addRange(t)}else if(void 0!==document.body.createTextRange){var i=document.body.createTextRange();i.moveToElementText(e),i.collapse(!1),i.select()}}},{key:"insertTextAtCursor",value:function(e){var t,n;(n=(t=window.getSelection()).getRangeAt(0)).deleteContents();var i=document.createTextNode(e);n.insertNode(i),n.selectNodeContents(i),n.collapse(!1),t.removeAllRanges(),t.addRange(n)}},{key:"insertAtCaret",value:function(e,t){var n=e.scrollTop,i=e.selectionStart,r=e.value.substring(0,i),o=e.value.substring(e.selectionEnd,e.value.length);e.value=r+t+o,i+=t.length,e.selectionStart=i,e.selectionEnd=i,e.focus(),e.scrollTop=n}},{key:"hideMenu",value:function(){this.menu&&(this.menu.style.cssText="display: none;",this.isActive=!1,this.menuSelected=0,this.current={})}},{key:"selectItemAtIndex",value:function(e,t){if("number"==typeof(e=parseInt(e))&&!isNaN(e)){var n=this.current.filteredItems[e],i=this.current.collection.selectTemplate(n);null!==i&&this.replaceText(i,t,n)}}},{key:"replaceText",value:function(e,t,n){this.range.replaceTriggerText(e,!0,!0,t,n)}},{key:"_append",value:function(e,t,n){if("function"==typeof e.values)throw new Error("Unable to append to values, as it is a function.");e.values=n?t:e.values.concat(t)}},{key:"append",value:function(e,t,n){var i=parseInt(e);if("number"!=typeof i)throw new Error("please provide an index for the collection to update.");var r=this.collection[i];this._append(r,t,n)}},{key:"appendCurrent",value:function(e,t){if(!this.isActive)throw new Error("No active state. Please use append instead and pass an index.");this._append(this.current.collection,e,t)}},{key:"detach",value:function(e){if(!e)throw new Error("[Tribute] Must pass in a DOM node or NodeList.");if("undefined"!=typeof jQuery&&e instanceof jQuery&&(e=e.get()),e.constructor===NodeList||e.constructor===HTMLCollection||e.constructor===Array)for(var t=e.length,n=0;n'+(this.current.collection.trigger+e.original[this.current.collection.fillAttr])+"":this.current.collection.trigger+e.original[this.current.collection.fillAttr]}},{key:"defaultMenuItemTemplate",value:function(e){return e.string}},{key:"inputTypes",value:function(){return["TEXTAREA","INPUT"]}}]),t}()}));
2 | //# sourceMappingURL=tribute.min.js.map
3 |
--------------------------------------------------------------------------------