diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..a8b6f1a4 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": ["eslint:recommended", "plugin:lit/recommended", "plugin:wc/recommended"], + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "rules": { + "no-mixed-spaces-and-tabs": 0 + } +} diff --git a/.gitignore b/.gitignore index 6cf26473..02e71ef5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ yarn.lock qortal-ui-plugins/plugins/core/**/*.js !*.src.js qortal-ui-core/src/redux/app/version.js -!qortal-ui-plugins/plugins/core/components/*.js +!qortal-ui-plugins/plugins/core/components/**/*.js # Node modules node_modules/ diff --git a/img/badges/level-0.png b/img/badges/level-0.png new file mode 100644 index 00000000..74e9099c Binary files /dev/null and b/img/badges/level-0.png differ diff --git a/img/badges/level-1.png b/img/badges/level-1.png new file mode 100644 index 00000000..9955cc9f Binary files /dev/null and b/img/badges/level-1.png differ diff --git a/img/badges/level-2.png b/img/badges/level-2.png new file mode 100644 index 00000000..627517ad Binary files /dev/null and b/img/badges/level-2.png differ diff --git a/img/badges/level-3.png b/img/badges/level-3.png new file mode 100644 index 00000000..e7a196ac Binary files /dev/null and b/img/badges/level-3.png differ diff --git a/img/badges/level-4.png b/img/badges/level-4.png new file mode 100644 index 00000000..045d4803 Binary files /dev/null and b/img/badges/level-4.png differ diff --git a/img/badges/level-5.png b/img/badges/level-5.png new file mode 100644 index 00000000..b7477290 Binary files /dev/null and b/img/badges/level-5.png differ diff --git a/img/chain.png b/img/chain.png new file mode 100644 index 00000000..79eb8e15 Binary files /dev/null and b/img/chain.png differ diff --git a/img/qchat-send-message-icon.svg b/img/qchat-send-message-icon.svg new file mode 100644 index 00000000..ee6cb8bf --- /dev/null +++ b/img/qchat-send-message-icon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/img/qortal-chat-logo.png b/img/qortal-chat-logo.png new file mode 100644 index 00000000..2f7b6b55 Binary files /dev/null and b/img/qortal-chat-logo.png differ diff --git a/package.json b/package.json index bcb1f351..9ddfeac8 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,12 @@ "install_link:all": "(cd qortal-ui-core && yarn install && yarn link) && (cd qortal-ui-plugins && yarn install && yarn link) && (cd qortal-ui-crypto && yarn install && yarn link) && (yarn link qortal-ui-core && yarn link qortal-ui-plugins && yarn link qortal-ui-crypto)", "dev": "node server.js", "prebuild": "node -p \"'export const UI_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > qortal-ui-core/src/redux/app/version.js", - "build-dev": "node build.js", - "build": "NODE_ENV=production node build.js", - "server": "NODE_ENV=production node server.js", - "watch": "node watch.js", - "watch-inline": "node watch-inline.js", - "start-electron": "NODE_ENV=production electron .", + "build-dev": "node --max-old-space-size=8192 build.js", + "build": "NODE_ENV=production node --max-old-space-size=8192 build.js", + "server": "NODE_ENV=production node --max-old-space-size=8192 server.js", + "watch": "node --max-old-space-size=8192 watch.js", + "watch-inline": "node --max-old-space-size=8192 watch-inline.js", + "start-electron": "NODE_ENV=production electron --js-flags=--max-old-space-size=8192 .", "build-electron": "electron-builder build --publish never", "deploy-electron": "electron-builder build --win --publish never", "release": "NODE_ENV=production electron-builder build --publish never", @@ -40,7 +40,8 @@ "electron": "22.0.2", "electron-builder": "23.6.0", "electron-packager": "17.1.1", - "@electron/notarize": "1.2.3", + "eslint-plugin-lit": "1.8.0", + "eslint-plugin-wc": "1.4.0", "shelljs": "0.8.5" }, "engines": { diff --git a/qortal-ui-core/font/KoHo.ttf b/qortal-ui-core/font/KoHo.ttf new file mode 100644 index 00000000..72841e9d Binary files /dev/null and b/qortal-ui-core/font/KoHo.ttf differ diff --git a/qortal-ui-core/font/Livvic.ttf b/qortal-ui-core/font/Livvic.ttf new file mode 100644 index 00000000..f477b285 Binary files /dev/null and b/qortal-ui-core/font/Livvic.ttf differ diff --git a/qortal-ui-core/font/MaterialSymbolsOutlined.ttf b/qortal-ui-core/font/MaterialSymbolsOutlined.ttf new file mode 100644 index 00000000..06c7bd3e Binary files /dev/null and b/qortal-ui-core/font/MaterialSymbolsOutlined.ttf differ diff --git a/qortal-ui-core/font/MaterialSymbolsOutlined.woff2 b/qortal-ui-core/font/MaterialSymbolsOutlined.woff2 new file mode 100644 index 00000000..d7dd3210 Binary files /dev/null and b/qortal-ui-core/font/MaterialSymbolsOutlined.woff2 differ diff --git a/qortal-ui-core/font/Montserrat.ttf b/qortal-ui-core/font/Montserrat.ttf new file mode 100644 index 00000000..656db666 Binary files /dev/null and b/qortal-ui-core/font/Montserrat.ttf differ diff --git a/qortal-ui-core/font/Raleway.ttf b/qortal-ui-core/font/Raleway.ttf new file mode 100644 index 00000000..424fb0e8 Binary files /dev/null and b/qortal-ui-core/font/Raleway.ttf differ diff --git a/qortal-ui-core/font/material-icons.css b/qortal-ui-core/font/material-icons.css index 2270c09d..1af6891e 100644 --- a/qortal-ui-core/font/material-icons.css +++ b/qortal-ui-core/font/material-icons.css @@ -2,19 +2,57 @@ font-family: 'Material Icons'; font-style: normal; font-weight: 400; - src: url(MaterialIcons-Regular.eot); /* For IE6-8 */ + src: url(MaterialIcons-Regular.eot); + /* For IE6-8 */ src: local('Material Icons'), - local('MaterialIcons-Regular'), - url(MaterialIcons-Regular.woff2) format('woff2'), - url(MaterialIcons-Regular.woff) format('woff'), - url(MaterialIcons-Regular.ttf) format('truetype'); + local('MaterialIcons-Regular'), + url(MaterialIcons-Regular.woff2) format('woff2'), + url(MaterialIcons-Regular.woff) format('woff'), + url(MaterialIcons-Regular.ttf) format('truetype'); +} + +@font-face { + font-family: 'Material Symbols Outlined'; + font-style: normal; + src: local('MaterialSymbolsOutlined'), + url(MaterialSymbolsOutlined.ttf) format('truetype'), + url(MaterialSymbolsOutlined.woff2) format('woff2') +} + +@font-face { + font-family: 'Montserrat'; + src: local('Montserrat'), + local('Montserrat'), + url(Montserrat.ttf) format('truetype'); +} + +@font-face { + font-family: 'Raleway'; + src: local('Raleway'), + local('Raleway'), + url(Raleway.ttf) format('truetype'); +} + +@font-face { + font-family: 'KoHo'; + src: local('KoHo'), + local('KoHo'), + url(KoHo.ttf) format('truetype'); +} + +@font-face { + font-family: 'Livvic'; + src: local('Livvic'), + local('Livvic'), + url(Livvic.ttf) format('truetype'); } .material-icons { font-family: 'Material Icons'; font-weight: normal; font-style: normal; - font-size: 24px; /* Preferred icon size */ + font-size: 24px; + /* Preferred icon size */ display: inline-block; line-height: 1; text-transform: none; @@ -34,3 +72,17 @@ /* Support for IE. */ font-feature-settings: 'liga'; } + +.material-symbols-outlined { + font-family: 'Material Symbols Outlined'; + font-weight: normal; + font-style: normal; + font-size: 24px; /* Preferred icon size */ + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; +} \ No newline at end of file diff --git a/qortal-ui-core/font/switch-theme.css b/qortal-ui-core/font/switch-theme.css index bb48fc95..6c78e322 100644 --- a/qortal-ui-core/font/switch-theme.css +++ b/qortal-ui-core/font/switch-theme.css @@ -7,6 +7,17 @@ html { --border: #d0d6de; --border2: #dde2e8; --copybutton: #707584; + --chat-group: #080808; + --chat-bubble: #9f9f9f0a; + --chat-bubble-bg: #e6e6e6; + --chat-bubble-msg-color: #080808; + --reaction-bubble-outline: #6b6969; + --chat-menu-bg: #ffffff; + --chat-menu-outline: #dad9d9; + --chat-menu-icon: #3b3b3c; + --chat-menu-icon-hover: #dad9d9; + --block-user-bg-hover: #dad9d9; + --paperclip-icon: #494949; --sectxt: #576374; --vdicon: #707b8a; --tradehead: #6a6c75; @@ -17,11 +28,12 @@ html { --relaynodetxt: #646464; --menuhover: #eeeeee; --menuactive: #ebebeb; - --mainmenutext:#080808; - --mainmenutexthover:#080808; + --menuactivergb: 235, 235, 235; + --mainmenutext: #080808; + --mainmenutexthover: #080808; --switchbackground: #666666; --switchborder: #333333; - --sidetopbar: #ffffff; + --sidetopbar: #ffffff; --nav-selected-color: #dddddd; --nav-selected-color-text: #333333; --nav-color-active: #d1d1d1; @@ -32,6 +44,13 @@ html { --nav-border-selected-color: #03a9f4; --error: #d50000; --background: url("/img/qortal_background_light_.jpg"); + --chatHeadBg: #ebebeb; + --chatHeadBgActive: #ebebeb; + --chatHeadText: #080808; + --chatHeadTextActive: #080808; + --lightChatHeadHover: #1e1f201a; + --group-header: #929292; + --group-drop-shadow: rgb(17 17 26 / 10%) 0px 1px 0px; } html[theme="dark"] { @@ -43,6 +62,17 @@ html[theme="dark"] { --border: #0b305e; --border2: #0b305e; --copybutton: #d0d6de; + --chat-group: #ffffff; + --chat-bubble: #9694941a; + --chat-bubble-bg: #2d3749; + --chat-bubble-msg-color: #ffffff; + --reaction-bubble-outline: #ffffff; + --chat-menu-bg: #32394c; + --chat-menu-outline: #32394c; + --chat-menu-icon: #ffffff; + --chat-menu-icon-hover: #a49a9a36; + --block-user-bg-hover: #121a2f; + --paperclip-icon: #d0c9c9; --sectxt: #bbc3cd; --vdicon: #d0d6de; --tradehead: #008fd5; @@ -53,11 +83,12 @@ html[theme="dark"] { --relaynodetxt: #d4d4d4; --menuhover: #008fd5; --menuactive: #008fd5; - --mainmenutext:#008fd5; - --mainmenutexthover:#0f1a2e; + --menuactivergb: 0, 143, 213; + --mainmenutext: #008fd5; + --mainmenutexthover: #0f1a2e; --switchbackground: #eeeeee; --switchborder: #03a9f4; - --sidetopbar: #070d19; + --sidetopbar: #070d19; --nav-selected-color: #0f1a2e; --nav-selected-color-text: #76c8f5; --nav-color-active: #d1d1d1; @@ -68,4 +99,11 @@ html[theme="dark"] { --nav-border-selected-color: #76c8f5; --error: #d50000; --background: url("/img/qortal_background_dark_.jpg"); -} + --chatHeadBg: #008fd5; + --chatHeadBgActive: #0f1a2e; + --chatHeadText: #ffffff; + --chatHeadTextActive: #ffffff; + --lightChatHeadHover: #e0e1e31a; + --group-header: #c8c8c8; + --group-drop-shadow: rgb(191 191 191 / 32%) 0px 1px 0px +} \ No newline at end of file diff --git a/qortal-ui-core/language/us.json b/qortal-ui-core/language/us.json index e0444893..1e47f84e 100644 --- a/qortal-ui-core/language/us.json +++ b/qortal-ui-core/language/us.json @@ -1,818 +1,866 @@ { - "selectmenu":{ - "selectlanguage":"Select language", - "languageflag":"us", - "english":"English", - "chinese1":"Chinese (Simplified)", - "chinese2":"Chinese (Traditional)", - "german":"German", - "french":"French", - "polish":"Polish", - "spanish":"Spanish", - "hindi":"Hindi", - "croatian":"Croatian", - "portuguese":"Portuguese", - "hungarian":"Hungarian", - "serbian":"Serbian", - "italian":"Italian", - "russian":"Russian", - "norwegian":"Norwegian", - "romanian":"Romanian", - "korean":"Korean" + "selectmenu": { + "selectlanguage": "Select language", + "languageflag": "us", + "english": "English", + "chinese1": "Chinese (Simplified)", + "chinese2": "Chinese (Traditional)", + "german": "German", + "french": "French", + "polish": "Polish", + "spanish": "Spanish", + "hindi": "Hindi", + "croatian": "Croatian", + "portuguese": "Portuguese", + "hungarian": "Hungarian", + "serbian": "Serbian", + "italian": "Italian", + "russian": "Russian", + "norwegian": "Norwegian", + "romanian": "Romanian", + "korean": "Korean" }, - "sidemenu":{ - "minting":"MINTING", - "mintingdetails":"MINTING DETAILS", - "becomeAMinter":"BECOME A MINTER", - "wallets":"WALLETS", - "tradeportal":"TRADE PORTAL", - "rewardshare":"REWARD SHARE", - "nameregistration":"NAME REGISTRATION", - "websites":"WEBSITES", - "management":"MANAGEMENT", - "datamanagement":"DATA MANAGEMENT", - "qchat":"Q-CHAT", - "groupmanagement":"GROUP MANAGEMENT", - "puzzles":"PUZZLES", - "nodemanagement":"NODE MANAGEMENT", - "trading":"TRADING", - "groups":"GROUPS" + "sidemenu": { + "minting": "MINTING", + "mintingdetails": "MINTING DETAILS", + "becomeAMinter": "BECOME A MINTER", + "wallets": "WALLETS", + "tradeportal": "TRADE PORTAL", + "rewardshare": "REWARD SHARE", + "nameregistration": "NAME REGISTRATION", + "websites": "WEBSITES", + "management": "MANAGEMENT", + "datamanagement": "DATA MANAGEMENT", + "qchat": "Q-CHAT", + "groupmanagement": "GROUP MANAGEMENT", + "puzzles": "PUZZLES", + "nodemanagement": "NODE MANAGEMENT", + "trading": "TRADING", + "groups": "GROUPS" }, - "login":{ - "login":"Login", - "createaccount":"Create Account", - "name":"Name", - "address":"Address", - "password":"Password", - "youraccounts":"Your accounts", - "clickto":"Click your account to login with it", - "needcreate":"You need to create or save an account before you can log in!", - "upload":"Upload your qortal backup", - "howlogin":"How would you like to login?", - "seed":"Seedphrase", - "seedphrase":"seedphrase", - "saved":"Saved account", - "qora":"Qora address seed", - "backup":"Qortal wallet backup", - "decrypt":"Decrypt backup", - "save":"Save in this browser.", - "prepare":"Preparing Your Account", - "areyousure":"Are you sure you want to remove this wallet from saved wallets?", - "error1":"Backup must be valid JSON", - "error2":"Login option not selected", - "createwelcome":"Welcome to Qortal, you will find it to be similar to that of an RPG game, you, as a minter on the Qortal network (if you choose to become one) will have the chance to level your account up, giving you both more of the QORT block reward and also larger influence over the network in terms of voting on decisions for the platform.", - "createa":"A", - "click":"Click to view seedphrase", - "confirmpass":"Confirm Password", - "willbe":"will be randomly generated in background. This is used as your private key generator for your blockchain account in Qortal.", - "clicknext":"Create your Qortal account by clicking NEXT below.", - "ready":"Your account is now ready to be created. It will be saved in this browser. If you do not want your new account to be saved in your browser, you can uncheck the box below. You will still be able to login with your new account(after logging out), using your wallet backup file that you MUST download once you create your account.", - "welmessage":"Welcome to Qortal", - "pleaseenter":"Please enter a Password!", - "notmatch":"Passwords not match!", - "lessthen8":"Your password is less than 8 characters! This is not recommended. You can continue to ignore this warning.", - "lessthen8-2":"Your password is less than 8 characters!", - "entername":"Please enter a Name!", - "downloaded":"Your Wallet BackUp file get downloaded!", - "loading":"Loading, Please wait...", - "createdseed":"Your created Seedphrase", - "saveseed":"Save Seedphrase", - "savein":"Save in browser", - "backup2":"This file is the ONLY way to access your account on a system that doesn't have it saved to the app/browser. BE SURE TO BACKUP THIS FILE IN MULTIPLE PLACES. The file is encrypted very securely and decrypted with your local password you created in the previous step. You can save it anywhere securely, but be sure to do that in multiple locations.", - "savewallet":"Save Wallet BackUp File", - "created1":"Your account is now created", - "created2":" and will be saved in this browser.", - "downloadbackup":"Download Wallet BackUp File", - "passwordhint":"A password must be at least 8 characters." + "login": { + "login": "Login", + "createaccount": "Create Account", + "name": "Name", + "address": "Address", + "password": "Password", + "youraccounts": "Your accounts", + "clickto": "Click your account to login with it", + "needcreate": "You need to create or save an account before you can log in!", + "upload": "Upload your qortal backup", + "howlogin": "How would you like to login?", + "seed": "Seedphrase", + "seedphrase": "seedphrase", + "saved": "Saved account", + "qora": "Qora address seed", + "backup": "Qortal wallet backup", + "decrypt": "Decrypt backup", + "save": "Save in this browser.", + "prepare": "Preparing Your Account", + "areyousure": "Are you sure you want to remove this wallet from saved wallets?", + "error1": "Backup must be valid JSON", + "error2": "Login option not selected", + "createwelcome": "Welcome to Qortal, you will find it to be similar to that of an RPG game, you, as a minter on the Qortal network (if you choose to become one) will have the chance to level your account up, giving you both more of the QORT block reward and also larger influence over the network in terms of voting on decisions for the platform.", + "createa": "A", + "click": "Click to view seedphrase", + "confirmpass": "Confirm Password", + "willbe": "will be randomly generated in background. This is used as your private key generator for your blockchain account in Qortal.", + "clicknext": "Create your Qortal account by clicking NEXT below.", + "ready": "Your account is now ready to be created. It will be saved in this browser. If you do not want your new account to be saved in your browser, you can uncheck the box below. You will still be able to login with your new account(after logging out), using your wallet backup file that you MUST download once you create your account.", + "welmessage": "Welcome to Qortal", + "pleaseenter": "Please enter a Password!", + "notmatch": "Passwords not match!", + "lessthen8": "Your password is less than 8 characters! This is not recommended. You can continue to ignore this warning.", + "lessthen8-2": "Your password is less than 8 characters!", + "entername": "Please enter a Name!", + "downloaded": "Your Wallet BackUp file get downloaded!", + "loading": "Loading, Please wait...", + "createdseed": "Your created Seedphrase", + "saveseed": "Save Seedphrase", + "savein": "Save in browser", + "backup2": "This file is the ONLY way to access your account on a system that doesn't have it saved to the app/browser. BE SURE TO BACKUP THIS FILE IN MULTIPLE PLACES. The file is encrypted very securely and decrypted with your local password you created in the previous step. You can save it anywhere securely, but be sure to do that in multiple locations.", + "savewallet": "Save Wallet BackUp File", + "created1": "Your account is now created", + "created2": " and will be saved in this browser.", + "downloadbackup": "Download Wallet BackUp File", + "passwordhint": "A password must be at least 8 characters." }, - "logout":{ - "logout":"LOGOUT", - "confirmlogout":"Are you sure you want to logout?" + "logout": { + "logout": "LOGOUT", + "confirmlogout": "Are you sure you want to logout?" }, - "fragfile":{ - "selectfile":"Select file", - "dragfile":"Drag and drop backup here" + "fragfile": { + "selectfile": "Select file", + "dragfile": "Drag and drop backup here" }, - "settings":{ - "generalinfo":"General Account Info", - "address":"Address", - "publickey":"Public Key", - "settings":"Settings", - "account":"Account", - "security":"Security", - "qr_login_menu_item":"QR Login", - "qr_login_description_1":"Scan this code to unlock your wallet on other device using the same password which you logged in with.", - "qr_login_description_2":"Choose a password which you will use to unlock your wallet on other device after scanning the QR code.", - "qr_login_button_1":"Show login QR code", - "qr_login_button_2":"Generate login QR code", - "notifications":"Notifications", - "accountsecurity":"Account Security", - "password":"Password", - "download":"Download Backup File", - "choose":"Please choose a password to encrypt your backup with. (This can be the same as the one you logged in with, or different)", - "block":"Block Notifications (Coming Soon...)", - "playsound":"Play Sound", - "shownotifications":"Show Notifications", - "nodeurl":"Node Url", - "nodehint":"Select a node from the default list of nodes above or add a custom node to the list above by clicking on the button below", - "addcustomnode":"Add Custom Node", - "addandsave":"Add And Save", - "protocol":"Protocol", - "domain":"Domain", - "port":"Port", - "import":"Import Nodes", - "export":"Export Nodes", - "deletecustomnode":"Remove All Custom Nodes", - "warning":"Your existing nodes will be deleted and from backup new created.", - "snack1":"Successfully deleted and added standard nodes", - "snack2":"UI conected to node", - "snack3":"Successfully added and saved custom node", - "snack4":"Nodes successfully saved as", - "snack5":"Nodes successfully imported", - "exp1":"Export Private Master Key", - "exp2":"Export Master Key", - "exp3":"Export", - "exp4":"Please choose a wallet to backup the private master key." + "settings": { + "generalinfo": "General Account Info", + "address": "Address", + "publickey": "Public Key", + "settings": "Settings", + "account": "Account", + "security": "Security", + "qr_login_menu_item": "QR Login", + "qr_login_description_1": "Scan this code to unlock your wallet on other device using the same password which you logged in with.", + "qr_login_description_2": "Choose a password which you will use to unlock your wallet on other device after scanning the QR code.", + "qr_login_button_1": "Show login QR code", + "qr_login_button_2": "Generate login QR code", + "notifications": "Notifications", + "accountsecurity": "Account Security", + "password": "Password", + "download": "Download Backup File", + "choose": "Please choose a password to encrypt your backup with. (This can be the same as the one you logged in with, or different)", + "block": "Block Notifications (Coming Soon...)", + "playsound": "Play Sound", + "shownotifications": "Show Notifications", + "nodeurl": "Node Url", + "nodehint": "Select a node from the default list of nodes above or add a custom node to the list above by clicking on the button below", + "addcustomnode": "Add Custom Node", + "addandsave": "Add And Save", + "protocol": "Protocol", + "domain": "Domain", + "port": "Port", + "import": "Import Nodes", + "export": "Export Nodes", + "deletecustomnode": "Remove All Custom Nodes", + "warning": "Your existing nodes will be deleted and from backup new created.", + "snack1": "Successfully deleted and added standard nodes", + "snack2": "UI conected to node", + "snack3": "Successfully added and saved custom node", + "snack4": "Nodes successfully saved as", + "snack5": "Nodes successfully imported", + "exp1": "Export Private Master Key", + "exp2": "Export Master Key", + "exp3": "Export", + "exp4": "Please choose a wallet to backup the private master key." }, - "appinfo":{ - "blockheight":"Block Height", - "uiversion":"UI Version", - "coreversion":"Core Version", - "minting":"(Minting)", - "synchronizing":"Synchronizing", - "peers":"Connected Peers" + "appinfo": { + "blockheight": "Block Height", + "uiversion": "UI Version", + "coreversion": "Core Version", + "minting": "(Minting)", + "synchronizing": "Synchronizing", + "peers": "Connected Peers" }, - "walletprofile":{ - "minterlevel":"Minter Level", - "blocksminted":"Blocks Minted" + "walletprofile": { + "minterlevel": "Minter Level", + "blocksminted": "Blocks Minted" }, - "general":{ - "yes":"Yes", - "no":"No", - "confirm":"Confirm", - "decline":"Decline", - "open":"Open", - "close":"Close", - "back":"Back", - "next":"Next", - "create":"Create", - "continue":"Continue", - "save":"Save", - "balance":"Balance", - "balances":"YOUR WALLET BALANCES", - "update":"UPDATE WALLET BALANCES" + "general": { + "yes": "Yes", + "no": "No", + "confirm": "Confirm", + "decline": "Decline", + "open": "Open", + "close": "Close", + "back": "Back", + "next": "Next", + "create": "Create", + "continue": "Continue", + "save": "Save", + "balance": "Balance", + "balances": "YOUR WALLET BALANCES", + "update": "UPDATE WALLET BALANCES" }, - "startminting":{ - "smchange1":"Cannot fetch minting accounts", - "smchange2":"Failed to remove key", - "smchange3":"Failed to add minting key", - "smchange4":"Cannot create sponsorship key", - "smchange5":"Creating relationship", - "smchange6":"Awaiting confirmation on blockchain", - "smchange7":"Finishing up relationship", - "smchange8":"Adding minting key to node", - "smchange9":"Complete", - "smchange10":"Only 2 minting keys are allowed per node, you are attempting to assign 3 keys, please go to management - node management, and remove the key you do not want to assign to this node, thank you!" + "startminting": { + "smchange1": "Cannot fetch minting accounts", + "smchange2": "Failed to remove key", + "smchange3": "Failed to add minting key", + "smchange4": "Cannot create sponsorship key", + "smchange5": "Creating relationship", + "smchange6": "Awaiting confirmation on blockchain", + "smchange7": "Finishing up relationship", + "smchange8": "Adding minting key to node", + "smchange9": "Complete", + "smchange10": "Only 2 minting keys are allowed per node, you are attempting to assign 3 keys, please go to management - node management, and remove the key you do not want to assign to this node, thank you!" }, - "mintingpage":{ - "mchange1":"General Minting Details", - "mchange2":"Blockchain Statistics", - "mchange3":"Avg. Qortal Blocktime", - "mchange4":"Avg. Blocks Per Day", - "mchange5":"Avg. Created QORT Per Day", - "mchange6":"Minting Account Details", - "mchange7":"Not A Minter", - "mchange8":"Minting", - "mchange9":"Not Minting", - "mchange10":"Activate Account Details", - "mchange11":"Not Activated", - "mchange12":"Activate Your Account", - "mchange13":"Introduction", - "mchange14":"To activate your account, an OUTGOING transaction needs to take place. Name Registration is the most common method. You can ask someone in Q-Chat to send you a small amount of QORT so that you may activate your account, or buy QORT within the Trade Portal then make an OUTGOING transaction of any kind and secure your public key on the blockchain. Until you do this, your public key is only known by you, in your UI, and no one else can pull your public key from the chain.", - "mchange15":"Current Status", - "mchange16":"Current Level", - "mchange17":"Blocks To Next Level", - "mchange18":"If you continue minting 24/7 you will reach level", - "mchange19":"Minting Rewards Info", - "mchange20":"Current Tier", - "mchange21":"Total Minters in The Tier", - "mchange22":"Tier Share Per Block", - "mchange23":"Est. Reward Per Block", - "mchange24":"Est. Reward Per Day", - "mchange25":"Seconds", - "mchange26":"Blocks", - "mchange27":"Level", - "mchange28":"Tier", - "mchange29":"days", - "mchange30":"Minters", - "mchange31":"Press for help", - "mchange32":"Become A Minter", - "mchange33":"Introduction", - "mchange34":"In Qortal, in order to become a minter and begin earning QORT rewards with your increase in Minter Level, you must first become ‘sponsored’. A sponsor in Qortal is any other minter of level 5 or higher, or a Qortal Founder. You will obtain a sponsorship key from the sponsor, and use that key to get to level 1. Once you have reached level 1, you will be able to create your own minting key and start earning rewards for helping secure the Qortal Blockchain.", - "mchange35":"Sponsorship", - "mchange36":"Your sponsor will issue you a ‘Sponsorship Key’ which you will use to add to your node, and begin minting (for no rewards until reaching level 1.) Once you reach level 1, you create/assign your own ‘Minting Key’ and begin earning rewards.", - "mchange37":"Simply reach out to a minter in Qortal who is high enough level to issue a sponsorship key, obtain that key, then come back here and input the key to begin your minting journey !", - "mchange38":"in" + "mintingpage": { + "mchange1": "General Minting Details", + "mchange2": "Blockchain Statistics", + "mchange3": "Avg. Qortal Blocktime", + "mchange4": "Avg. Blocks Per Day", + "mchange5": "Avg. Created QORT Per Day", + "mchange6": "Minting Account Details", + "mchange7": "Not A Minter", + "mchange8": "Minting", + "mchange9": "Not Minting", + "mchange10": "Activate Account Details", + "mchange11": "Not Activated", + "mchange12": "Activate Your Account", + "mchange13": "Introduction", + "mchange14": "To activate your account, an OUTGOING transaction needs to take place. Name Registration is the most common method. You can ask someone in Q-Chat to send you a small amount of QORT so that you may activate your account, or buy QORT within the Trade Portal then make an OUTGOING transaction of any kind and secure your public key on the blockchain. Until you do this, your public key is only known by you, in your UI, and no one else can pull your public key from the chain.", + "mchange15": "Current Status", + "mchange16": "Current Level", + "mchange17": "Blocks To Next Level", + "mchange18": "If you continue minting 24/7 you will reach level", + "mchange19": "Minting Rewards Info", + "mchange20": "Current Tier", + "mchange21": "Total Minters in The Tier", + "mchange22": "Tier Share Per Block", + "mchange23": "Est. Reward Per Block", + "mchange24": "Est. Reward Per Day", + "mchange25": "Seconds", + "mchange26": "Blocks", + "mchange27": "Level", + "mchange28": "Tier", + "mchange29": "days", + "mchange30": "Minters", + "mchange31": "Press for help", + "mchange32": "Become A Minter", + "mchange33": "Introduction", + "mchange34": "In Qortal, in order to become a minter and begin earning QORT rewards with your increase in Minter Level, you must first become ‘sponsored’. A sponsor in Qortal is any other minter of level 5 or higher, or a Qortal Founder. You will obtain a sponsorship key from the sponsor, and use that key to get to level 1. Once you have reached level 1, you will be able to create your own minting key and start earning rewards for helping secure the Qortal Blockchain.", + "mchange35": "Sponsorship", + "mchange36": "Your sponsor will issue you a ‘Sponsorship Key’ which you will use to add to your node, and begin minting (for no rewards until reaching level 1.) Once you reach level 1, you create/assign your own ‘Minting Key’ and begin earning rewards.", + "mchange37": "Simply reach out to a minter in Qortal who is high enough level to issue a sponsorship key, obtain that key, then come back here and input the key to begin your minting journey !", + "mchange38": "in" }, - "becomeMinterPage":{ - "bchange7":"Enter Sponsorship Key", - "bchange8":"Input key from your sponsor here", - "bchange10":"Current Sponsorship Status", - "bchange12":"Minting with sponsor key", - "bchange13":"Blocks Remaining in Sponsorship Period", - "bchange15":"Sponsorship Relationship", - "bchange16":"Sponsor Account", - "bchange17":"Copy Sponsorship Key", - "bchange18":"Start Minting", - "bchange19":"Success! You are currently minting." + "becomeMinterPage": { + "bchange7": "Enter Sponsorship Key", + "bchange8": "Input key from your sponsor here", + "bchange10": "Current Sponsorship Status", + "bchange12": "Minting with sponsor key", + "bchange13": "Blocks Remaining in Sponsorship Period", + "bchange15": "Sponsorship Relationship", + "bchange16": "Sponsor Account", + "bchange17": "Copy Sponsorship Key", + "bchange18": "Start Minting", + "bchange19": "Success! You are currently minting." }, - "walletpage":{ - "wchange1":"Fetching balance ...", - "wchange2":"Current Wallet", - "wchange3":"Copy wallet address to clipboard", - "wchange4":"Address copied to clipboard", - "wchange5":"Transaction Details", - "wchange6":"Transaction Type", - "wchange7":"OUT", - "wchange8":"IN", - "wchange9":"Sender", - "wchange10":"Receiver", - "wchange11":"Amount", - "wchange12":"Transaction Fee", - "wchange13":"Block", - "wchange14":"Time", - "wchange15":"Transaction Signature", - "wchange16":"Transaction Hash", - "wchange17":"Send", - "wchange18":"From address", - "wchange19":"Available balance", - "wchange20":"To (address or name)", - "wchange21":"Current static fee:", - "wchange22":"Wallets", - "wchange23":"To (address)", - "wchange24":"Current fee per byte", - "wchange25":"Low fees may result in slow or unconfirmed transactions.", - "wchange26":"Insufficient Funds!", - "wchange27":"Invalid Amount!", - "wchange28":"Receiver cannot be empty!", - "wchange29":"Invalid Receiver!", - "wchange30":"Transaction Successful!", - "wchange31":"Transaction Failed!", - "wchange32":"Failed to Fetch QORT Balance. Try again!", - "wchange33":"Failed to Fetch", - "wchange34":"Balance. Try again!", - "wchange35":"Type", - "wchange36":"Fee", - "wchange37":"Total Amount", - "wchange38":"Address has no transactions yet.", - "wchange39":"Unable to copy address.", - "wchange40":"PAYMENT", - "wchange41":"Status", - "wchange42":"Confirmations", - "wchange43":"Your transaction will not show until confirmed, be patient...", - "wchange44":"Please try again...", - "wchange45":"Send all", - "wchange46":"Send to this address", - "wchange47":"Address Book", - "wchange48":"This Address Book is empty !", - "wchange49":"Add to Address Book", - "wchange50":"Name cannot be empty!", - "wchange51":"Address cannot be empty!", - "wchange52":"Successfully added!", - "wchange53":"Import Address Book", - "wchange54":"Export Address Book", - "wchange55":"Your existing address book will be deleted and from backup new created.", - "wchange56":"WARNING!", - "wchange57":"Memo" + "walletpage": { + "wchange1": "Fetching balance ...", + "wchange2": "Current Wallet", + "wchange3": "Copy wallet address to clipboard", + "wchange4": "Address copied to clipboard", + "wchange5": "Transaction Details", + "wchange6": "Transaction Type", + "wchange7": "OUT", + "wchange8": "IN", + "wchange9": "Sender", + "wchange10": "Receiver", + "wchange11": "Amount", + "wchange12": "Transaction Fee", + "wchange13": "Block", + "wchange14": "Time", + "wchange15": "Transaction Signature", + "wchange16": "Transaction Hash", + "wchange17": "Send", + "wchange18": "From address", + "wchange19": "Available balance", + "wchange20": "To (address or name)", + "wchange21": "Current static fee:", + "wchange22": "Wallets", + "wchange23": "To (address)", + "wchange24": "Current fee per byte", + "wchange25": "Low fees may result in slow or unconfirmed transactions.", + "wchange26": "Insufficient Funds!", + "wchange27": "Invalid Amount!", + "wchange28": "Receiver cannot be empty!", + "wchange29": "Invalid Receiver!", + "wchange30": "Transaction Successful!", + "wchange31": "Transaction Failed!", + "wchange32": "Failed to Fetch QORT Balance. Try again!", + "wchange33": "Failed to Fetch", + "wchange34": "Balance. Try again!", + "wchange35": "Type", + "wchange36": "Fee", + "wchange37": "Total Amount", + "wchange38": "Address has no transactions yet.", + "wchange39": "Unable to copy address.", + "wchange40": "PAYMENT", + "wchange41": "Status", + "wchange42": "Confirmations", + "wchange43": "Your transaction will not show until confirmed, be patient...", + "wchange44": "Please try again...", + "wchange45": "Send all", + "wchange46": "Send to this address", + "wchange47": "Address Book", + "wchange48": "This Address Book is empty !", + "wchange49": "Add to Address Book", + "wchange50": "Name cannot be empty!", + "wchange51": "Address cannot be empty!", + "wchange52": "Successfully added!", + "wchange53": "Import Address Book", + "wchange54": "Export Address Book", + "wchange55": "Your existing address book will be deleted and from backup new created.", + "wchange56": "WARNING!", + "wchange57": "Memo" }, - "tradepage":{ - "tchange1":"Trade Portal", - "tchange2":"Select Trading Pair", - "tchange3":"HISTORIC MARKET TRADES", - "tchange4":"MY TRADE HISTORY", - "tchange5":"OPEN MARKET SELL ORDERS", - "tchange6":"MY ORDERS", - "tchange7":"Stuck Offers", - "tchange8":"Amount", - "tchange9":"Price", - "tchange10":"Total", - "tchange11":"Date", - "tchange12":"Status", - "tchange13":"Seller", - "tchange14":"Price Each", - "tchange15":"Clear Form", - "tchange16":"You have", - "tchange17":"Action", - "tchange18":"BUY", - "tchange19":"SELL", - "tchange20":"Failed to Create Trade. Try again!", - "tchange21":"Failed to Create Trade. Error Code", - "tchange22":"Insufficient Funds!", - "tchange23":"Buy Request Successful!", - "tchange24":"Buy Request Existing!", - "tchange25":"Failed to Create Trade. Error Code", - "tchange26":"Trade Cancelling In Progress!", - "tchange27":"Failed to Cancel Trade. Try again!", - "tchange28":"Failed to Cancel Trade. Error Code", - "tchange29":"CANCEL", - "tchange30":"Failed to Fetch Balance. Try again!", - "tchange31":"SOLD", - "tchange32":"BOUGHT", - "tchange33":"Average", - "tchange34":"Amount can not be 0", - "tchange35":"Price can not be 0", - "tchange36":"PENDING AUTO BUY", - "tchange37":"No auto buy order found !", - "tchange38":"ADD", - "tchange39":"AUTO BUY ORDER", - "tchange40":"Price", - "tchange41":"Successfully removed auto buy order!", - "tchange42":"MARKET OPEN SELL ORDERS", - "tchange43":"MY BUY HISTORY", - "tchange44":"Successfully added auto buy order!", - "tchange45":"AUTO BUY WITH", - "tchange46":"AUTO BUY", - "tchange47":"Sell for this price", - "tchange48":"NOT ENOUGH", - "tchange49":"Price Chart" + "tradepage": { + "tchange1": "Trade Portal", + "tchange2": "Select Trading Pair", + "tchange3": "HISTORIC MARKET TRADES", + "tchange4": "MY TRADE HISTORY", + "tchange5": "OPEN MARKET SELL ORDERS", + "tchange6": "MY ORDERS", + "tchange7": "Stuck Offers", + "tchange8": "Amount", + "tchange9": "Price", + "tchange10": "Total", + "tchange11": "Date", + "tchange12": "Status", + "tchange13": "Seller", + "tchange14": "Price Each", + "tchange15": "Clear Form", + "tchange16": "You have", + "tchange17": "Action", + "tchange18": "BUY", + "tchange19": "SELL", + "tchange20": "Failed to Create Trade. Try again!", + "tchange21": "Failed to Create Trade. Error Code", + "tchange22": "Insufficient Funds!", + "tchange23": "Buy Request Successful!", + "tchange24": "Buy Request Existing!", + "tchange25": "Failed to Create Trade. Error Code", + "tchange26": "Trade Cancelling In Progress!", + "tchange27": "Failed to Cancel Trade. Try again!", + "tchange28": "Failed to Cancel Trade. Error Code", + "tchange29": "CANCEL", + "tchange30": "Failed to Fetch Balance. Try again!", + "tchange31": "SOLD", + "tchange32": "BOUGHT", + "tchange33": "Average", + "tchange34": "Amount can not be 0", + "tchange35": "Price can not be 0", + "tchange36": "PENDING AUTO BUY", + "tchange37": "No auto buy order found !", + "tchange38": "ADD", + "tchange39": "AUTO BUY ORDER", + "tchange40": "Price", + "tchange41": "Successfully removed auto buy order!", + "tchange42": "MARKET OPEN SELL ORDERS", + "tchange43": "MY BUY HISTORY", + "tchange44": "Successfully added auto buy order!", + "tchange45": "AUTO BUY WITH", + "tchange46": "AUTO BUY", + "tchange47": "Sell for this price", + "tchange48": "NOT ENOUGH", + "tchange49": "Price Chart" }, - "rewardsharepage":{ - "rchange1":"Rewardshares", - "rchange2":"Create reward share", - "rchange3":"Rewardshares Involving In This Account", - "rchange4":"Minting Account", - "rchange5":"Share Percent", - "rchange6":"Recipient", - "rchange7":"Action", - "rchange8":"Type", - "rchange9":"Level 1 - 4 can create a Self Share and Level 5 or above can create a Reward Share!", - "rchange10":"Recipient Public Key", - "rchange11":"Reward share percentage", - "rchange12":"Doing something delicious", - "rchange13":"Adding minting account", - "rchange14":"Add", - "rchange15":"Account is not involved in any reward shares", - "rchange16":"Own Rewardshare", - "rchange17":"Remove", - "rchange18":"Cannot Create Multiple Reward Shares!", - "rchange19":"Cannot Create Multiple Self Shares!", - "rchange20":"CANNOT CREATE REWARD SHARE! at level", - "rchange21":"Reward Share Successful!", - "rchange22":"Reward Share Removed Successfully!" + "rewardsharepage": { + "rchange1": "Rewardshares", + "rchange2": "Create reward share", + "rchange3": "Rewardshares Involving In This Account", + "rchange4": "Minting Account", + "rchange5": "Share Percent", + "rchange6": "Recipient", + "rchange7": "Action", + "rchange8": "Type", + "rchange9": "Level 1 - 4 can create a Self Share and Level 5 or above can create a Reward Share!", + "rchange10": "Recipient Public Key", + "rchange11": "Reward share percentage", + "rchange12": "Doing something delicious", + "rchange13": "Adding minting account", + "rchange14": "Add", + "rchange15": "Account is not involved in any reward shares", + "rchange16": "Own Rewardshare", + "rchange17": "Remove", + "rchange18": "Cannot Create Multiple Reward Shares!", + "rchange19": "Cannot Create Multiple Self Shares!", + "rchange20": "CANNOT CREATE REWARD SHARE! at level", + "rchange21": "Reward Share Successful!", + "rchange22": "Reward Share Removed Successfully!" }, - "registernamepage":{ - "nchange1":"Name Registration", - "nchange2":"Register Name", - "nchange3":"Registered Names", - "nchange4":"Avatar", - "nchange5":"Name", - "nchange6":"Owner", - "nchange7":"Action", - "nchange8":"No names registered by this account!", - "nchange9":"Register a Name!", - "nchange10":"Description (optional)", - "nchange11":"Doing something delicious", - "nchange12":"Registering Name", - "nchange13":"The current name registration fee is", - "nchange14":"Register", - "nchange15":"Set Avatar", - "nchange16":"Need Core Update", - "nchange17":"Name Already Exists!", - "nchange18":"Name Registration Successful!", - "nchange19":"Sell Name", - "nchange20":"Cancel Sell", - "nchange21":"Buy Name", - "nchange22":"Open Market Names To Sell", - "nchange23":"Sell Price", - "nchange24":"No Names To Sell", - "nchange25":"Name To Sell", - "nchange26":"Are you sure to sell this name ?", - "nchange27":"For this price in QORT", - "nchange28":"On pressing confirm, the sell name request will be sent!", - "nchange29":"Name To Cancel", - "nchange30":"Are you sure to cancel the sell for this name ?", - "nchange31":"On pressing confirm, the cancel sell name request will be sent!", - "nchange32":"Sell Name Request Successful!", - "nchange33":"Cancel Sell Name Request Successful!", - "nchange34":"Buy Name Request Successful!", - "nchange35":"YOU HAVE A NAME!", - "nchange36":"Only accounts with no registered name can buy a name.", - "nchange37":"ATTENTION!", - "nchange38":"You not have enough qort to buy this name.", - "nchange39":"Are you sure to buy this name ?", - "nchange40":"On pressing confirm, the buy name request will be sent!" + "registernamepage": { + "nchange1": "Name Registration", + "nchange2": "Register Name", + "nchange3": "Registered Names", + "nchange4": "Avatar", + "nchange5": "Name", + "nchange6": "Owner", + "nchange7": "Action", + "nchange8": "No names registered by this account!", + "nchange9": "Register a Name!", + "nchange10": "Description (optional)", + "nchange11": "Doing something delicious", + "nchange12": "Registering Name", + "nchange13": "The current name registration fee is", + "nchange14": "Register", + "nchange15": "Set Avatar", + "nchange16": "Need Core Update", + "nchange17": "Name Already Exists!", + "nchange18": "Name Registration Successful!", + "nchange19": "Sell Name", + "nchange20": "Cancel Sell", + "nchange21": "Buy Name", + "nchange22": "Open Market Names To Sell", + "nchange23": "Sell Price", + "nchange24": "No Names To Sell", + "nchange25": "Name To Sell", + "nchange26": "Are you sure to sell this name ?", + "nchange27": "For this price in QORT", + "nchange28": "On pressing confirm, the sell name request will be sent!", + "nchange29": "Name To Cancel", + "nchange30": "Are you sure to cancel the sell for this name ?", + "nchange31": "On pressing confirm, the cancel sell name request will be sent!", + "nchange32": "Sell Name Request Successful!", + "nchange33": "Cancel Sell Name Request Successful!", + "nchange34": "Buy Name Request Successful!", + "nchange35": "YOU HAVE A NAME!", + "nchange36": "Only accounts with no registered name can buy a name.", + "nchange37": "ATTENTION!", + "nchange38": "You not have enough qort to buy this name.", + "nchange39": "Are you sure to buy this name ?", + "nchange40": "On pressing confirm, the buy name request will be sent!" }, - "websitespage":{ - "schange1":"Browse Websites", - "schange2":"Followed Websites", - "schange3":"Blocked Websites", - "schange4":"Search Websites", - "schange5":"Avatar", - "schange6":"Details", - "schange7":"Published by", - "schange8":"Actions", - "schange9":"Websites", - "schange10":"No websites available", - "schange11":"Your Followed Websites", - "schange12":"Followed Websites", - "schange13":"You aren't following any websites", - "schange14":"Your Blocked Websites", - "schange15":"Blocked Websites", - "schange16":"You have not blocked any websites", - "schange17":"Name Not Found!", - "schange18":"Relay mode is enabled. This means that your node will help to transport encrypted data around the network when a peer requests it. You can opt out by setting", - "schange19":"in", - "schange20":"Relay mode is disabled. You can enable it by setting", - "schange21":"Publish Website", - "schange22":"Error occurred when trying to follow this registered name. Please try again!", - "schange23":"Error occurred when trying to unfollow this registered name. Please try again!", - "schange24":"Error occurred when trying to block this registered name. Please try again!", - "schange25":"Error occurred when trying to unblock this registered name. Please try again!", - "schange26":"Uncategorized", - "schange27":"Size", - "schange28":"Status", - "schange29":"Follow", - "schange30":"Unfollow", - "schange31":"Block", - "schange32":"Unblock", - "schange33":"Name to search", - "schange34":"Name can not be empty!", - "schange35":"Search" + "websitespage": { + "schange1": "Browse Websites", + "schange2": "Followed Websites", + "schange3": "Blocked Websites", + "schange4": "Search Websites", + "schange5": "Avatar", + "schange6": "Details", + "schange7": "Published by", + "schange8": "Actions", + "schange9": "Websites", + "schange10": "No websites available", + "schange11": "Your Followed Websites", + "schange12": "Followed Websites", + "schange13": "You aren't following any websites", + "schange14": "Your Blocked Websites", + "schange15": "Blocked Websites", + "schange16": "You have not blocked any websites", + "schange17": "Name Not Found!", + "schange18": "Relay mode is enabled. This means that your node will help to transport encrypted data around the network when a peer requests it. You can opt out by setting", + "schange19": "in", + "schange20": "Relay mode is disabled. You can enable it by setting", + "schange21": "Publish Website", + "schange22": "Error occurred when trying to follow this registered name. Please try again!", + "schange23": "Error occurred when trying to unfollow this registered name. Please try again!", + "schange24": "Error occurred when trying to block this registered name. Please try again!", + "schange25": "Error occurred when trying to unblock this registered name. Please try again!", + "schange26": "Uncategorized", + "schange27": "Size", + "schange28": "Status", + "schange29": "Follow", + "schange30": "Unfollow", + "schange31": "Block", + "schange32": "Unblock", + "schange33": "Name to search", + "schange34": "Name can not be empty!", + "schange35": "Search" }, - "publishpage":{ - "pchange1":"Publish", - "pchange2":"Update", - "pchange3":"Note: it is recommended that you set up port forwarding before hosting data, so that it can more easily accessed by peers on the network.", - "pchange4":"Select Name", - "pchange5":"Title", - "pchange6":"Description", - "pchange7":"Select Category", - "pchange8":"Tag", - "pchange9":"Service", - "pchange10":"Identifier", - "pchange11":"Publish", - "pchange12":"Select zip file containing static content", - "pchange13":"Local path to static files", - "pchange14":"Please select a registered name to publish data for", - "pchange15":"Please select a file to host", - "pchange16":"Please select a zip file to host", - "pchange17":"Please enter the directory path containing the static content", - "pchange18":"Please enter a service name", - "pchange19":"Processing data... this can take some time...", - "pchange20":"Error:", - "pchange21":"Internal Server Error when publishing data", - "pchange22":"Computing proof of work... this can take some time...", - "pchange23":"Transaction successful!", - "pchange24":"Unable to sign and process transaction", - "pchange25":"Choose File" + "publishpage": { + "pchange1": "Publish", + "pchange2": "Update", + "pchange3": "Note: it is recommended that you set up port forwarding before hosting data, so that it can more easily accessed by peers on the network.", + "pchange4": "Select Name", + "pchange5": "Title", + "pchange6": "Description", + "pchange7": "Select Category", + "pchange8": "Tag", + "pchange9": "Service", + "pchange10": "Identifier", + "pchange11": "Publish", + "pchange12": "Select zip file containing static content", + "pchange13": "Local path to static files", + "pchange14": "Please select a registered name to publish data for", + "pchange15": "Please select a file to host", + "pchange16": "Please select a zip file to host", + "pchange17": "Please enter the directory path containing the static content", + "pchange18": "Please enter a service name", + "pchange19": "Processing data... this can take some time...", + "pchange20": "Error:", + "pchange21": "Internal Server Error when publishing data", + "pchange22": "Computing proof of work... this can take some time...", + "pchange23": "Transaction successful!", + "pchange24": "Unable to sign and process transaction", + "pchange25": "Choose File" }, - "browserpage":{ - "bchange1":"Forward", - "bchange2":"Reload", - "bchange3":"Back to list", - "bchange4":"Delete", - "bchange5":"from node", - "bchange6":"Your browser doesn't support iframes", - "bchange7":"Follow", - "bchange8":"Unfollow", - "bchange9":"Block", - "bchange10":"Unblock", - "bchange11":"Error occurred when trying to follow this registered name. Please try again!", - "bchange12":"Error occurred when trying to unfollow this registered name. Please try again!", - "bchange13":"Error occurred when trying to block this registered name. Please try again!", - "bchange14":"Error occurred when trying to unblock this registered name. Please try again!", - "bchange15":"Can't delete data from followed names. Please unfollow first.", - "bchange16":"Error occurred when trying to delete this resource. Please try again!" + "browserpage": { + "bchange1": "Forward", + "bchange2": "Reload", + "bchange3": "Back to list", + "bchange4": "Delete", + "bchange5": "from node", + "bchange6": "Your browser doesn't support iframes", + "bchange7": "Follow", + "bchange8": "Unfollow", + "bchange9": "Block", + "bchange10": "Unblock", + "bchange11": "Error occurred when trying to follow this registered name. Please try again!", + "bchange12": "Error occurred when trying to unfollow this registered name. Please try again!", + "bchange13": "Error occurred when trying to block this registered name. Please try again!", + "bchange14": "Error occurred when trying to unblock this registered name. Please try again!", + "bchange15": "Can't delete data from followed names. Please unfollow first.", + "bchange16": "Error occurred when trying to delete this resource. Please try again!" }, - "datapage":{ - "dchange1":"Data Management", - "dchange2":"Search in hosted data by this node", - "dchange3":"Data to search", - "dchange4":"Search", - "dchange5":"Registered Name", - "dchange6":"Service", - "dchange7":"Identifier", - "dchange8":"Actions", - "dchange9":"Data hosted by this node", - "dchange10":"Data name can not be empty!", - "dchange11":"Data not found!", - "dchange12":"Couldn't fetch hosted data list from node", - "dchange13":"This node isn't hosting any data", - "dchange14":"Unfollow", - "dchange15":"Delete", - "dchange16":"Block", - "dchange17":"Unblock", - "dchange18":"Error occurred when trying to block this registered name. Please try again!", - "dchange19":"Error occurred when trying to unfollow this registered name. Please try again!", - "dchange20":"Error occurred when trying to unblock this registered name. Please try again!", - "dchange21":"Error occurred when trying to delete this resource. Please try again!" + "datapage": { + "dchange1": "Data Management", + "dchange2": "Search in hosted data by this node", + "dchange3": "Data to search", + "dchange4": "Search", + "dchange5": "Registered Name", + "dchange6": "Service", + "dchange7": "Identifier", + "dchange8": "Actions", + "dchange9": "Data hosted by this node", + "dchange10": "Data name can not be empty!", + "dchange11": "Data not found!", + "dchange12": "Couldn't fetch hosted data list from node", + "dchange13": "This node isn't hosting any data", + "dchange14": "Unfollow", + "dchange15": "Delete", + "dchange16": "Block", + "dchange17": "Unblock", + "dchange18": "Error occurred when trying to block this registered name. Please try again!", + "dchange19": "Error occurred when trying to unfollow this registered name. Please try again!", + "dchange20": "Error occurred when trying to unblock this registered name. Please try again!", + "dchange21": "Error occurred when trying to delete this resource. Please try again!" }, - "chatpage":{ - "cchange1":"New Private Message", - "cchange2":"Loading...", - "cchange3":"Blocked Users", - "cchange4":"New Message", - "cchange5":"(Click to scroll down)", - "cchange6":"Type the name or address of who you want to chat with to send a private message!", - "cchange7":"Name / Address", - "cchange8":"Message...", - "cchange9":"Send", - "cchange10":"Blocked Users List", - "cchange11":"Name", - "cchange12":"Owner", - "cchange13":"Action", - "cchange14":"This account has not blocked any users.", - "cchange15":"No registered name", - "cchange16":"Successfully unblocked this user.", - "cchange17":"Error occurred when trying to unblock this user. Please try again!", - "cchange18":"unblock", - "cchange19":"Invalid Name / Address, Check the name / address and retry...", - "cchange20":"Message Sent Successfully!", - "cchange21":"Sending failed, Please retry...", - "cchange22":"Loading Messages...", - "cchange23":"Cannot Decrypt Message!", - "cchange24":"Maximum Characters per message is 255", - "cchange25":"Your Balance Is Under 4.20 QORT", - "cchange26":"Out of the need to combat spam, accounts with under 4.20 Qort balance will take a long time to SEND messages in Q-Chat. If you wish to immediately increase the send speed for Q-Chat messages, obtain over 4.20 QORT to your address. This can be done with trades in the Trade Portal, or by way of another Qortian giving you the QORT. Once you have over 4.20 QORT in your account, Q-Chat messages will be instant and this dialog will no more show. Thank you for your understanding of this necessary spam prevention method, and we hope you enjoy Qortal!" + "chatpage": { + "cchange1": "New Private Message", + "cchange2": "Loading...", + "cchange3": "Blocked Users", + "cchange4": "New Message", + "cchange5": "(Click to scroll down)", + "cchange6": "Type the name or address of who you want to chat with to send a private message! You can validate the person's name by clicking on the book icon.", + "cchange7": "Username / Address", + "cchange8": "Message...", + "cchange9": "Send", + "cchange10": "Blocked Users List", + "cchange11": "Name", + "cchange12": "Owner", + "cchange13": "Action", + "cchange14": "This account has not blocked any users.", + "cchange15": "No registered name", + "cchange16": "Successfully unblocked this user.", + "cchange17": "Error occurred when trying to unblock this user. Please try again!", + "cchange18": "unblock", + "cchange19": "Invalid Username / Address, Check the name / address and retry...", + "cchange20": "Message Sent Successfully!", + "cchange21": "Sending failed, Please retry...", + "cchange22": "Loading Messages...", + "cchange23": "Cannot Decrypt Message!", + "cchange24": "Maximum Characters per message is 255", + "cchange25": "Edit Message", + "cchange26": "File size exceeds 0.5 MB", + "cchange27": "A registered name is required to send images", + "cchange28": "This file is not an image", + "cchange29": "Maximum message size is 1000 bytes", + "cchange30": "Uploading image. This may take up to one minute.", + "cchange31": "Deleting image. This may take up to one minute.", + "cchange33": "Cancel", + "cchange34": "This chat message is using an older message version and cannot use this feature.", + "cchange35": "Error when trying to fetch the user's name. Please try again!", + "cchange36": "Search Results", + "cchange37": "No Results Found", + "cchange38": "User Verified", + "cchange39": "Cannot send an encrypted message to this user since they do not have their publickey on chain.", + "cchange40": "IMAGE (click to view)", + "cchange41": "Your Balance Is Under 4.20 QORT", + "cchange42": "Out of the need to combat spam, accounts with under 4.20 Qort balance will take a long time to SEND messages in Q-Chat. If you wish to immediately increase the send speed for Q-Chat messages, obtain over 4.20 QORT to your address. This can be done with trades in the Trade Portal, or by way of another Qortian giving you the QORT. Once you have over 4.20 QORT in your account, Q-Chat messages will be instant and this dialog will no more show. Thank you for your understanding of this necessary spam prevention method, and we hope you enjoy Qortal!", + "cchange43": "Tip QORT to", + "cchange44": "SEND MESSAGE", + "cchange45": "TIP USER", + "cchange46": "Tip Amount", + "cchange47": "Available Balance", + "cchange48": "Failed to Fetch QORT Balance. Try again!", + "cchange49": "Current static fee", + "cchange50": "Send", + "cchange51": "Insufficient Funds!", + "cchange52": "Invalid Amount!", + "cchange53": "Receiver cannot be empty!", + "cchange54": "Invalid Receiver!", + "cchange55": "Transaction Successful!", + "cchange56": "Transaction Failed!", + "cchange57": "User Info", + "cchange58": "SEND MESSAGE", + "cchange59": "TIP USER", + "cchange60": "Group Invites Pending", + "cchange61": "Error when fetching group invites. Please try again!", + "cchange62": "Wrong Username and Address Inputted! Please try again!", + "cchange63": "Enter Enabled", + "cchange64": "Enter Disabled", + "cchange65": "Please enter a recipient", + "cchange66": "Cannot fetch replied-to message. Message is too old.", + "cchange68": "edited" }, - "welcomepage":{ - "wcchange1":"Welcome to Q-Chat", - "wcchange2":"New Private Message", - "wcchange3":"Type the name or address of who you want to chat with to send a private message!", - "wcchange4":"Name / Address", - "wcchange5":"Message...", - "wcchange6":"Send", - "wcchange7":"Invalid Name / Address, Check the name / address and retry...", - "wcchange8":"Message Sent Successfully!", - "wcchange9":"Sending failed, Please retry..." + "welcomepage": { + "wcchange1": "Welcome to Q-Chat", + "wcchange2": "New Private Message", + "wcchange3": "Type the name or address of who you want to chat with to send a private message!", + "wcchange4": "Name / Address", + "wcchange5": "Message...", + "wcchange6": "Send", + "wcchange7": "Invalid Name / Address, Check the name / address and retry...", + "wcchange8": "Message Sent Successfully!", + "wcchange9": "Sending failed, Please retry..." }, - "blockpage":{ - "bcchange1":"Block User", - "bcchange2":"Successfully blocked this user!", - "bcchange3":"Error occurred when trying to block this user. Please try again!", - "bcchange4":"No registered name", - "bcchange5":"Block User Request", - "bcchange6":"Are you sure to block this user ?", - "bcchange7":"MENU", - "bcchange8":"Copy Address", - "bcchange9":"Private Message", - "bcchange10":"More" + "blockpage": { + "bcchange1": "Block User", + "bcchange2": "Successfully blocked this user!", + "bcchange3": "Error occurred when trying to block this user. Please try again!", + "bcchange4": "No registered name", + "bcchange5": "Block User Request", + "bcchange6": "Are you sure to block this user ?", + "bcchange7": "MENU", + "bcchange8": "Copy Address", + "bcchange9": "Private Message", + "bcchange10": "More", + "bcchange11": "Reply", + "bcchange12": "Edit", + "bcchange13": "Reaction", + "bcchange14": "Forward", + "bcchange15": "Message Forwarded", + "bcchange16": "Choose Recipient or Search for One Below", + "bcchange17": "FORWARDED", + "bcchange18": "Tip User" }, - "grouppage":{ - "gchange1":"Qortal Groups", - "gchange2":"Create Group", - "gchange3":"Your Joined Groups", - "gchange4":"Group Name", - "gchange5":"Description", - "gchange6":"Role", - "gchange7":"Action", - "gchange8":"Not a member of any group!", - "gchange9":"Public Groups", - "gchange10":"Owner", - "gchange11":"No Open Public Groups available!", - "gchange12":"Create a New Group", - "gchange13":"Group Type", - "gchange14":"This Field is Required", - "gchange15":"Select an option", - "gchange16":"Public", - "gchange17":"Private", - "gchange18":"Group Approval Threshold (number / percentage of Admins that must approve a transaction):", - "gchange19":"NONE", - "gchange20":"ONE", - "gchange21":"Minimum Block delay for Group Transaction Approvals:", - "gchange22":"minutes", - "gchange23":"hour", - "gchange24":"hours", - "gchange25":"day", - "gchange26":"days", - "gchange27":"Maximum Block delay for Group Transaction Approvals:", - "gchange28":"Creating Group", - "gchange29":"Create Group", - "gchange30":"Join Group Request", - "gchange31":"Date Created", - "gchange32":"Date Updated", - "gchange33":"Joining", - "gchange34":"Join Group", - "gchange35":"Leave Group Request", - "gchange36":"Leaving", - "gchange37":"Leave Group", - "gchange38":"Manage Group Owner:", - "gchange39":"Manage Group Admin:", - "gchange40":"Manage Group", - "gchange41":"Group Creation Successful!", - "gchange42":"Invalid Group Name", - "gchange43":"Invalid Group Description", - "gchange44":"Select a Group Typ", - "gchange45":"Select a Group Approval Threshold", - "gchange46":"Select a Minimum Block delay for Group Transaction Approvals", - "gchange47":"Select a Maximum Block delay for Group Transaction Approvals", - "gchange48":"Join Group Request Sent Successfully!", - "gchange49":"Leave Group Request Sent Successfully!", - "gchange50":"Leave", - "gchange51":"Join", - "gchange52":"Admin", - "gchange53":"Member", - "gchange54":"Members", - "gchange55":"Search Private Group", - "gchange56":"Group Name To Search", - "gchange57":"Private Group Name Not Found", - "gchange58":"Note that group name must exact match." + "grouppage": { + "gchange1": "Qortal Groups", + "gchange2": "Create Group", + "gchange3": "Your Joined Groups", + "gchange4": "Group Name", + "gchange5": "Description", + "gchange6": "Role", + "gchange7": "Action", + "gchange8": "Not a member of any group!", + "gchange9": "Public Groups", + "gchange10": "Owner", + "gchange11": "No Open Public Groups available!", + "gchange12": "Create a New Group", + "gchange13": "Group Type", + "gchange14": "This Field is Required", + "gchange15": "Select an option", + "gchange16": "Public", + "gchange17": "Private", + "gchange18": "Group Approval Threshold (number / percentage of Admins that must approve a transaction):", + "gchange19": "NONE", + "gchange20": "ONE", + "gchange21": "Minimum Block delay for Group Transaction Approvals:", + "gchange22": "minutes", + "gchange23": "hour", + "gchange24": "hours", + "gchange25": "day", + "gchange26": "days", + "gchange27": "Maximum Block delay for Group Transaction Approvals:", + "gchange28": "Creating Group", + "gchange29": "Create Group", + "gchange30": "Join Group Request", + "gchange31": "Date Created", + "gchange32": "Date Updated", + "gchange33": "Joining", + "gchange34": "Join Group", + "gchange35": "Leave Group Request", + "gchange36": "Leaving", + "gchange37": "Leave Group", + "gchange38": "Manage Group Owner:", + "gchange39": "Manage Group Admin:", + "gchange40": "Manage Group", + "gchange41": "Group Creation Successful!", + "gchange42": "Invalid Group Name", + "gchange43": "Invalid Group Description", + "gchange44": "Select a Group Typ", + "gchange45": "Select a Group Approval Threshold", + "gchange46": "Select a Minimum Block delay for Group Transaction Approvals", + "gchange47": "Select a Maximum Block delay for Group Transaction Approvals", + "gchange48": "Join Group Request Sent Successfully!", + "gchange49": "Leave Group Request Sent Successfully!", + "gchange50": "Leave", + "gchange51": "Join", + "gchange52": "Admin", + "gchange53": "Member", + "gchange54": "Members", + "gchange55": "Search Private Group", + "gchange56": "Group Name To Search", + "gchange57": "Private Group Name Not Found", + "gchange58": "Note that group name must exact match." }, - "puzzlepage":{ - "pchange1":"Puzzles", - "pchange2":"Reward", - "pchange3":"SOLVED by", - "pchange4":"Name", - "pchange5":"Description", - "pchange6":"Clue / Answer", - "pchange7":"Action", - "pchange8":"Guess", - "pchange9":"Enter your guess to solve this puzzle and win", - "pchange10":"Your guess needs to be 43 or 44 characters and", - "pchange11":"not", - "pchange12":"include 0 (zero), I (upper i), O (upper o) or l (lower L).", - "pchange13":"Your Guess", - "pchange14":"Checking your guess...", - "pchange15":"Submit", - "pchange16":"Guess incorrect!", - "pchange17":"Reward claim submitted - check wallet for reward!" + "puzzlepage": { + "pchange1": "Puzzles", + "pchange2": "Reward", + "pchange3": "SOLVED by", + "pchange4": "Name", + "pchange5": "Description", + "pchange6": "Clue / Answer", + "pchange7": "Action", + "pchange8": "Guess", + "pchange9": "Enter your guess to solve this puzzle and win", + "pchange10": "Your guess needs to be 43 or 44 characters and", + "pchange11": "not", + "pchange12": "include 0 (zero), I (upper i), O (upper o) or l (lower L).", + "pchange13": "Your Guess", + "pchange14": "Checking your guess...", + "pchange15": "Submit", + "pchange16": "Guess incorrect!", + "pchange17": "Reward claim submitted - check wallet for reward!" }, - "nodepage":{ - "nchange1":"Node management for:", - "nchange2":"Node has been online for:", - "nchange3":"Node's minting accounts", - "nchange4":"Add minting account", - "nchange5":"If you would like to mint with your own account you will need to create a rewardshare transaction to yourself (with rewardshare percent set to 0), and then mint with the rewardshare key it gives you.", - "nchange6":"Rewardshare key", - "nchange7":"Adding minting account", - "nchange8":"Add", - "nchange9":"Minting Account", - "nchange10":"Recipient Account", - "nchange11":"Action", - "nchange12":"Remove", - "nchange13":"No minting accounts found for this node", - "nchange14":"Peers connected to this node", - "nchange15":"Add peer", - "nchange16":"Type the peer you wish to add's address below", - "nchange17":"Peer Address", - "nchange18":"Address", - "nchange19":"Last Height", - "nchange20":"Build Version", - "nchange21":"Connected for", - "nchange22":"Action", - "nchange23":"Force Sync", - "nchange24":"Node has no connected peers", - "nchange25":"Starting Sync with Peer: ", - "nchange26":"Successfully removed Peer: ", - "nchange27":"Minting Node Added Successfully!", - "nchange28":"Failed to Add Minting Node!", - "nchange29":"Successfully Removed Minting Account!", - "nchange30":"Failed to Remove Minting Account!", - "nchange31":"Stop Node", - "nchange32":"Successfully Sent Stop Request!" + "nodepage": { + "nchange1": "Node management for:", + "nchange2": "Node has been online for:", + "nchange3": "Node's minting accounts", + "nchange4": "Add minting account", + "nchange5": "If you would like to mint with your own account you will need to create a rewardshare transaction to yourself (with rewardshare percent set to 0), and then mint with the rewardshare key it gives you.", + "nchange6": "Rewardshare key", + "nchange7": "Adding minting account", + "nchange8": "Add", + "nchange9": "Minting Account", + "nchange10": "Recipient Account", + "nchange11": "Action", + "nchange12": "Remove", + "nchange13": "No minting accounts found for this node", + "nchange14": "Peers connected to this node", + "nchange15": "Add peer", + "nchange16": "Type the peer you wish to add's address below", + "nchange17": "Peer Address", + "nchange18": "Address", + "nchange19": "Last Height", + "nchange20": "Build Version", + "nchange21": "Connected for", + "nchange22": "Action", + "nchange23": "Force Sync", + "nchange24": "Node has no connected peers", + "nchange25": "Starting Sync with Peer: ", + "nchange26": "Successfully removed Peer: ", + "nchange27": "Minting Node Added Successfully!", + "nchange28": "Failed to Add Minting Node!", + "nchange29": "Successfully Removed Minting Account!", + "nchange30": "Failed to Remove Minting Account!", + "nchange31": "Stop Node", + "nchange32": "Successfully Sent Stop Request!" }, - "transpage":{ - "tchange1":"Transaction request", - "tchange2":"Decline", - "tchange3":"Confirm", - "tchange4":"To", - "tchange5":"Amount" + "transpage": { + "tchange1": "Transaction request", + "tchange2": "Decline", + "tchange3": "Confirm", + "tchange4": "To", + "tchange5": "Amount" }, - "apipage":{ - "achange1":"Add API key", - "achange2":"API key", - "achange3":"Please enter the API key for this node. It can be found in a file called “apikey.txt“ in the directory where the core is installed. Alternatively, click Cancel to use the core with reduced functionality.", - "achange4":"Cancel", - "achange5":"Add", - "achange6":"Successfully added API Key", - "achange7":"API key wrong, no API key added" + "apipage": { + "achange1": "Add API key", + "achange2": "API key", + "achange3": "Please enter the API key for this node. It can be found in a file called “apikey.txt“ in the directory where the core is installed. Alternatively, click Cancel to use the core with reduced functionality.", + "achange4": "Cancel", + "achange5": "Add", + "achange6": "Successfully added API Key", + "achange7": "API key wrong, no API key added" }, - "transactions":{ - "amount":"Amount", - "to":"To", - "declined":"User declined transaction!", - "namedialog1":"You are registering the name below:", - "namedialog2":"On pressing confirm, the name will be registered!", - "groupdialog1":"You are requesting to join the group below:", - "groupdialog2":"On pressing confirm, the group join request will be sent!", - "groupdialog3":"You are requesting to leave the group below:", - "groupdialog4":"On pressing confirm, the leave group request will be sent!", - "groupdialog5":"You are requesting to creating the group below:", - "groupdialog6":"On pressing confirm, the group creating request will be sent!", - "rewarddialog1":"Would you like to create a reward share transaction, sharing", - "rewarddialog2":"of your minting rewards with", - "rewarddialog3":"If yes, you will need to save the key below in order to mint. It can be supplied to any node in order to allow it to mint on your behalf.", - "rewarddialog4":"On pressing confirm, the rewardshare will be created, but you will still need to supply the above key to a node in order to mint with the account.", - "rewarddialog5":"You are removing a reward share transaction associated with account:", - "rewarddialog6":"On pressing confirm, the rewardshare will be removed and the minting key will become invalid." + "transactions": { + "amount": "Amount", + "to": "To", + "declined": "User declined transaction!", + "namedialog1": "You are registering the name below:", + "namedialog2": "On pressing confirm, the name will be registered!", + "groupdialog1": "You are requesting to join the group below:", + "groupdialog2": "On pressing confirm, the group join request will be sent!", + "groupdialog3": "You are requesting to leave the group below:", + "groupdialog4": "On pressing confirm, the leave group request will be sent!", + "groupdialog5": "You are requesting to creating the group below:", + "groupdialog6": "On pressing confirm, the group creating request will be sent!", + "rewarddialog1": "Would you like to create a reward share transaction, sharing", + "rewarddialog2": "of your minting rewards with", + "rewarddialog3": "If yes, you will need to save the key below in order to mint. It can be supplied to any node in order to allow it to mint on your behalf.", + "rewarddialog4": "On pressing confirm, the rewardshare will be created, but you will still need to supply the above key to a node in order to mint with the account.", + "rewarddialog5": "You are removing a reward share transaction associated with account:", + "rewarddialog6": "On pressing confirm, the rewardshare will be removed and the minting key will become invalid." }, - "sponsorshipspage":{ - "schange1":"Active Sponsorships", - "schange2":"Account Address", - "schange3":"Total Sponsorships active", - "schange4":"Next sponsorship ending in", - "schange5":"Sponsor New Minter", - "schange6":"Finished Sponsorships", - "schange7":"Completed", - "schange8":"Addresses", - "schange9":"You currently have no active sponsorships", - "schange10":"Public Key Lookup", - "schange11":"Copy", - "schange12":"Address to Public Key Converter", - "schange13":"Enter address", - "schange14":"In progress", - "schange15":"Finishing up", - "schange16":"Copy the key below and share it with your sponsored person.", - "schange17":"Copied to clipboard", - "schange18":"Warning: do not leave this plugin or close the Qortal UI until completion!", - "schange19":"Copy Sponsorship Key", - "schange20":"Creating relationship", - "schange21":"Remove Sponsorship Key" + "sponsorshipspage": { + "schange1": "Active Sponsorships", + "schange2": "Account Address", + "schange3": "Total Sponsorships active", + "schange4": "Next sponsorship ending in", + "schange5": "Sponsor New Minter", + "schange6": "Finished Sponsorships", + "schange7": "Completed", + "schange8": "Addresses", + "schange9": "You currently have no active sponsorships", + "schange10": "Public Key Lookup", + "schange11": "Copy", + "schange12": "Address to Public Key Converter", + "schange13": "Enter address", + "schange14": "In progress", + "schange15": "Finishing up", + "schange16": "Copy the key below and share it with your sponsored person.", + "schange17": "Copied to clipboard", + "schange18": "Warning: do not leave this plugin or close the Qortal UI until completion!", + "schange19": "Copy Sponsorship Key", + "schange20": "Creating relationship", + "schange21": "Remove Sponsorship Key" }, - "explorerpage":{ - "exp1":"Address or name to search", - "exp2":"Account Balance", - "exp3":"More Info", - "exp4":"Address or Name not found !", - "exp5":"Note that registered names are case-sensitive.", - "exp6":"Founder", - "exp7":"Info", - "exp8":"Show all buy trades", - "exp9":"Show all sell trades", - "exp10":"BUY HISTORY", - "exp11":"SELL HISTORY", - "exp12":"No buy trades made yet.", - "exp13":"No sell trades made yet.", - "exp14":"Show complete info", - "exp15":"Minting Since", - "exp16":"Not Minting", - "exp17":"ALL PAYMENTS", - "exp18":"Payments", - "exp19":"Sent", - "exp20":"Received", - "exp21":"Trades" + "explorerpage": { + "exp1": "Address or name to search", + "exp2": "Account Balance", + "exp3": "More Info", + "exp4": "Address or Name not found !", + "exp5": "Note that registered names are case-sensitive.", + "exp6": "Founder", + "exp7": "Info", + "exp8": "Show all buy trades", + "exp9": "Show all sell trades", + "exp10": "BUY HISTORY", + "exp11": "SELL HISTORY", + "exp12": "No buy trades made yet.", + "exp13": "No sell trades made yet.", + "exp14": "Show complete info", + "exp15": "Minting Since", + "exp16": "Not Minting", + "exp17": "ALL PAYMENTS", + "exp18": "Payments", + "exp19": "Sent", + "exp20": "Received", + "exp21": "Trades" }, - "managegroup":{ - "mg1":"Group Members", - "mg2":"Invite To Group", - "mg3":"Group Admins", - "mg4":"Update Group", - "mg5":"Close Manage Group", - "mg6":"BAN", - "mg7":"KICK", - "mg8":"Group ID", - "mg9":"Joined", - "mg10":"Add Group Admin", - "mg11":"Are you sure to add this member to admins ?", - "mg12":"On pressing confirm, add admin request will be sent!", - "mg13":"Remove Group Admin", - "mg14":"Remove Admin Address", - "mg15":"Are you sure to remove this member from admins ?", - "mg16":"On pressing confirm, remove admin request will be sent!", - "mg17":"Ban Member From Group", - "mg18":"Member Name", - "mg19":"Member Address", - "mg20":"How Long To Ban", - "mg21":"Reason For Ban", - "mg22":"Are you sure to ban this member from the group ?", - "mg23":"On pressing confirm, the ban request will be sent!", - "mg24":"FOREVER", - "mg25":"Banned Members", - "mg26":"CANCEL BAN", - "mg27":"Ban Expiry", - "mg28":"Cancel Ban Member From Group", - "mg29":"Are you sure to cancel the ban for this member from the group ?", - "mg30":"On pressing confirm, the cancel ban request will be sent!", - "mg31":"Kick Member From Group", - "mg32":"Reason For Kick", - "mg33":"Are you sure to kick this member from the group ?", - "mg34":"On pressing confirm, the kick request will be sent!", - "mg35":"No Open Group Invites", - "mg36":"Your Open Group Invites", - "mg37":"Address or name to invite", - "mg38":"Invite Expiry Time", - "mg39":"All Fields Are Required", - "mg40":"Are you sure to invite this member to the group ?", - "mg41":"On pressing confirm, the invite request will be sent!", - "mg42":"Group Type", - "mg43":"Invite Expiry", - "mg44":"Public Group", - "mg45":"Private Group", - "mg46":"Cancel Invite", - "mg47":"Cancel Invite To Group", - "mg48":"Are you sure to cancel the invite for this member ?", - "mg49":"On pressing confirm, the cancel invite request will be sent!", - "mg50":"Coming Soon...", - "mg51":"Minimum 3 Characters / Maximum 32 Characters", - "mg52":"Maximum 128 Characters", - "mg53":"Your Open Join Requests", - "mg54":"No Open Join Requests", - "mg55":"Are you sure to accept the join request from this member ?", - "mg56":"On pressing confirm, the accept join request will be sent!", - "mg57":"Join Request Successfully Accepted", - "mg58":"SOMETHING WENT WRONG", - "mg59":"Cancel Join Request Successfully Accepted", - "mg60":"Are you sure to cancel the join request from this member ?", - "mg61":"On pressing confirm, the cancel join request will be sent!" + "managegroup": { + "mg1": "Group Members", + "mg2": "Invite To Group", + "mg3": "Group Admins", + "mg4": "Update Group", + "mg5": "Close Manage Group", + "mg6": "BAN", + "mg7": "KICK", + "mg8": "Group ID", + "mg9": "Joined", + "mg10": "Add Group Admin", + "mg11": "Are you sure to add this member to admins ?", + "mg12": "On pressing confirm, add admin request will be sent!", + "mg13": "Remove Group Admin", + "mg14": "Remove Admin Address", + "mg15": "Are you sure to remove this member from admins ?", + "mg16": "On pressing confirm, remove admin request will be sent!", + "mg17": "Ban Member From Group", + "mg18": "Member Name", + "mg19": "Member Address", + "mg20": "How Long To Ban", + "mg21": "Reason For Ban", + "mg22": "Are you sure to ban this member from the group ?", + "mg23": "On pressing confirm, the ban request will be sent!", + "mg24": "FOREVER", + "mg25": "Banned Members", + "mg26": "CANCEL BAN", + "mg27": "Ban Expiry", + "mg28": "Cancel Ban Member From Group", + "mg29": "Are you sure to cancel the ban for this member from the group ?", + "mg30": "On pressing confirm, the cancel ban request will be sent!", + "mg31": "Kick Member From Group", + "mg32": "Reason For Kick", + "mg33": "Are you sure to kick this member from the group ?", + "mg34": "On pressing confirm, the kick request will be sent!", + "mg35": "No Open Group Invites", + "mg36": "Your Open Group Invites", + "mg37": "Address or name to invite", + "mg38": "Invite Expiry Time", + "mg39": "All Fields Are Required", + "mg40": "Are you sure to invite this member to the group ?", + "mg41": "On pressing confirm, the invite request will be sent!", + "mg42": "Group Type", + "mg43": "Invite Expiry", + "mg44": "Public Group", + "mg45": "Private Group", + "mg46": "Cancel Invite", + "mg47": "Cancel Invite To Group", + "mg48": "Are you sure to cancel the invite for this member ?", + "mg49": "On pressing confirm, the cancel invite request will be sent!", + "mg50": "Coming Soon...", + "mg51": "Minimum 3 Characters / Maximum 32 Characters", + "mg52": "Maximum 128 Characters", + "mg53": "Your Open Join Requests", + "mg54": "No Open Join Requests", + "mg55": "Are you sure to accept the join request from this member ?", + "mg56": "On pressing confirm, the accept join request will be sent!", + "mg57": "Join Request Successfully Accepted", + "mg58": "SOMETHING WENT WRONG", + "mg59": "Cancel Join Request Successfully Accepted", + "mg60": "Are you sure to cancel the join request from this member ?", + "mg61": "On pressing confirm, the cancel join request will be sent!" } -} +} \ No newline at end of file diff --git a/qortal-ui-core/package.json b/qortal-ui-core/package.json index d52b6ab0..e98948c6 100644 --- a/qortal-ui-core/package.json +++ b/qortal-ui-core/package.json @@ -76,7 +76,8 @@ "rollup": "3.10.0", "rollup-plugin-node-globals": "1.4.0", "rollup-plugin-progress": "1.1.2", - "rollup-plugin-scss": "3.0.0" + "rollup-plugin-scss": "3.0.0", + "rollup-plugin-web-worker-loader": "1.6.1" }, "engines": { "node": ">=16.17.1" diff --git a/qortal-ui-core/src/components/app-info.js b/qortal-ui-core/src/components/app-info.js index 56f8303b..6d0aef89 100644 --- a/qortal-ui-core/src/components/app-info.js +++ b/qortal-ui-core/src/components/app-info.js @@ -3,6 +3,8 @@ import { connect } from 'pwa-helpers' import { store } from '../store.js' import { doPageUrl } from '../redux/app/app-actions.js' import { translate, translateUnsafeHTML } from 'lit-translate' +import WebWorker from 'web-worker:./computePowWorker.js'; +import { routes } from '../plugins/routes.js'; import '@material/mwc-icon' import '@material/mwc-button' @@ -94,6 +96,8 @@ class AppInfo extends connect(store)(LitElement) { this.nodeStatus = {} this.pageUrl = '' this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light' + this.publicKeyisOnChainConfirmation = false + this.interval } render() { @@ -108,10 +112,114 @@ class AppInfo extends connect(store)(LitElement) { ` } + async confirmPublicKeyOnChain(address) { + const _computePow2 = async (chatBytes) => { + const difficulty = 14; + const path = window.parent.location.origin + '/memory-pow/memory-pow.wasm.full' + const worker = new WebWorker(); + let nonce = null + let chatBytesArray = null + await new Promise((res, rej) => { + worker.postMessage({chatBytes, path, difficulty}); + + worker.onmessage = e => { + worker.terminate() + chatBytesArray = e.data.chatBytesArray + nonce = e.data.nonce + res() + + } + }) + + let _response = await routes.sign_chat({ + data: { + nonce: store.getState().app.selectedAddress.nonce, + chatBytesArray: chatBytesArray, + chatNonce: nonce + }, + + }); + return _response + }; + + + let stop = false + const checkPublicKey = async () => { + if (!stop) { + stop = true; + try { + if(this.publicKeyisOnChainConfirmation){ + clearInterval(this.interval) + return + } + const myNode = store.getState().app.nodeConfig.knownNodes[store.getState().app.nodeConfig.node]; + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port; + const url = `${nodeUrl}/addresses/publickey/${address}`; + const res = await fetch(url) + let data = '' + try { + data = await res.text(); + } catch (error) { + data = { + error: 'error' + } + } + if(data === 'false' && this.nodeInfo.isSynchronizing !== true){ + let _reference = new Uint8Array(64); + window.crypto.getRandomValues(_reference); + let reference = window.parent.Base58.encode(_reference); + const chatRes = await routes.chat({ + data: { + type: 19, + nonce: store.getState().app.selectedAddress.nonce, + params: { + lastReference: reference, + proofOfWorkNonce: 0, + fee: 0, + timestamp: Date.now(), + + }, + disableModal: true + }, + disableModal: true, + }); + + try { + const powRes = await _computePow2(chatRes) + if(powRes === true){ + clearInterval(this.interval) + + this.publicKeyisOnChainConfirmation = true + } + } catch (error) { + console.error(error) + } + } + + if (!data.error && data !== 'false' && data) { + clearInterval(this.interval) + + this.publicKeyisOnChainConfirmation = true + } + + } catch (error) { + } + stop = false + } + }; + this.interval = setInterval(checkPublicKey, 5000); + } + firstUpdated() { this.getNodeInfo() this.getCoreInfo() - + try { + this.confirmPublicKeyOnChain(store.getState().app.selectedAddress.address) + } catch (error) { + console.error(error) + } + + setInterval(() => { this.getNodeInfo() this.getCoreInfo() diff --git a/qortal-ui-core/src/components/app-view.js b/qortal-ui-core/src/components/app-view.js index 200c0c8f..cffc315f 100644 --- a/qortal-ui-core/src/components/app-view.js +++ b/qortal-ui-core/src/components/app-view.js @@ -142,8 +142,6 @@ class AppView extends connect(store)(LitElement) { app-drawer { box-shadow: var(--shadow-2); - background: var(--sidetopbar); - --app-drawer-scrim-background: rgba(0,0,0,0); } app-header { @@ -154,6 +152,8 @@ class AppView extends connect(store)(LitElement) { background: var(--sidetopbar); color: var(--black); border-top: var(--border); + height: 48px; + padding: 3px; } paper-progress { @@ -183,24 +183,26 @@ class AppView extends connect(store)(LitElement) { background: var(--sidetopbar); } - .sideBarMenu{ + .sideBarMenu { overflow-y: auto; flex: 1 1; } - #sideBar::-webkit-scrollbar { - width: 7px; - background-color: transparent; + .sideBarMenu::-webkit-scrollbar-track { + background-color: whitesmoke; + border-radius: 7px; } - - #sideBar::-webkit-scrollbar-track { - background-color: transparent; + + .sideBarMenu::-webkit-scrollbar { + width: 6px; + border-radius: 7px; + background-color: whitesmoke; } - - #sideBar::-webkit-scrollbar-thumb { - background-color: #333; - border-radius: 6px; - border: 3px solid #333; + + .sideBarMenu::-webkit-scrollbar-thumb { + background-color: rgb(180, 176, 176); + border-radius: 7px; + transition: all 0.3s ease-in-out; } #balanceheader { @@ -323,6 +325,11 @@ class AppView extends connect(store)(LitElement) { 0%,100% { opacity: 0; } 50% { opacity: 10; } } + + .sideBarMenu::-webkit-scrollbar-thumb:hover { + background-color: rgb(148, 146, 146); + cursor: pointer; + } ` ] } diff --git a/qortal-ui-core/src/components/computePowWorker.js b/qortal-ui-core/src/components/computePowWorker.js new file mode 100644 index 00000000..2ed60a20 --- /dev/null +++ b/qortal-ui-core/src/components/computePowWorker.js @@ -0,0 +1,82 @@ +import { Sha256 } from 'asmcrypto.js' + + +function sbrk(size, heap){ + let brk = 512 * 1024 // stack top + let old = brk + brk += size + + if (brk > heap.length) + throw new Error('heap exhausted') + + return old +} + + + +self.addEventListener('message', async e => { + const response = await computePow(e.data.chatBytes, e.data.path, e.data.difficulty) + postMessage(response) + +}) + + +const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 }) +const heap = new Uint8Array(memory.buffer) + + + +const computePow = async (chatBytes, path, difficulty) => { + + let response = null + + await new Promise((resolve, reject)=> { + + const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; }); + const chatBytesArray = new Uint8Array(_chatBytesArray); + const chatBytesHash = new Sha256().process(chatBytesArray).finish().result; + const hashPtr = sbrk(32, heap); + const hashAry = new Uint8Array(memory.buffer, hashPtr, 32); + hashAry.set(chatBytesHash); + + + const workBufferLength = 8 * 1024 * 1024; + const workBufferPtr = sbrk(workBufferLength, heap); + + + + const importObject = { + env: { + memory: memory + }, + }; + + function loadWebAssembly(filename, imports) { + // Fetch the file and compile it + return fetch(filename) + .then(response => response.arrayBuffer()) + .then(buffer => WebAssembly.compile(buffer)) + .then(module => { + + // Create the instance. + return new WebAssembly.Instance(module, importObject); + }); +} + + +loadWebAssembly(path) + .then(wasmModule => { + response = { + nonce : wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty), + chatBytesArray + } + + resolve() + + }); + + + }) + + return response +} \ No newline at end of file diff --git a/qortal-ui-core/src/styles/switch-theme.css b/qortal-ui-core/src/styles/switch-theme.css index 45fb525b..fe928e12 100644 --- a/qortal-ui-core/src/styles/switch-theme.css +++ b/qortal-ui-core/src/styles/switch-theme.css @@ -7,6 +7,17 @@ html { --border: #d0d6de; --border2: #dde2e8; --copybutton: #707584; + --chat-group: #080808; + --chat-bubble: #9f9f9f0a; + --chat-bubble-bg: #e6e6e6; + --chat-bubble-msg-color: #080808; + --reaction-bubble-outline: #6b6969; + --chat-menu-bg: #ffffff; + --chat-menu-outline: #dad9d9; + --chat-menu-icon: #3b3b3c; + --chat-menu-icon-hover: #dad9d9; + --block-user-bg-hover: #dad9d9; + --paperclip-icon: #494949; --sectxt: #576374; --vdicon: #707b8a; --tradehead: #6a6c75; @@ -17,11 +28,11 @@ html { --relaynodetxt: #646464; --menuhover: #eeeeee; --menuactive: #ebebeb; - --mainmenutext:#080808; - --mainmenutexthover:#080808; + --mainmenutext: #080808; + --mainmenutexthover: #080808; --switchbackground: #666666; --switchborder: #333333; - --sidetopbar: #ffffff; + --sidetopbar: #ffffff; --nav-selected-color: #dddddd; --nav-selected-color-text: #333333; --nav-color-active: #d1d1d1; @@ -31,6 +42,12 @@ html { --nav-border-color: #eeeeee; --nav-border-selected-color: #03a9f4; --background: url("/img/qortal_background_light_.jpg"); + --chatHeadBg: #ebebeb; + --chatHeadBgActive: #ebebeb; + --chatHeadText: #080808; + --chatHeadTextActive: #080808; + --group-header: #929292; + --group-drop-shadow: rgb(17 17 26 / 10%) 0px 1px 0px; } html[theme="dark"] { @@ -42,6 +59,17 @@ html[theme="dark"] { --border: #0b305e; --border2: #0b305e; --copybutton: #d0d6de; + --chat-group: #ffffff; + --chat-bubble: #9694941a; + --chat-bubble-bg: #2d3749; + --chat-bubble-msg-color: #ffffff; + --reaction-bubble-outline: #ffffff; + --chat-menu-bg: #32394c; + --chat-menu-outline: #32394c; + --chat-menu-icon: #ffffff; + --chat-menu-icon-hover: #a49a9a36; + --block-user-bg-hover: #121a2f; + --paperclip-icon: #d0c9c9; --sectxt: #bbc3cd; --vdicon: #d0d6de; --tradehead: #008fd5; @@ -52,11 +80,11 @@ html[theme="dark"] { --relaynodetxt: #d4d4d4; --menuhover: #008fd5; --menuactive: #008fd5; - --mainmenutext:#008fd5; - --mainmenutexthover:#0f1a2e; + --mainmenutext: #008fd5; + --mainmenutexthover: #0f1a2e; --switchbackground: #eeeeee; --switchborder: #03a9f4; - --sidetopbar: #070d19; + --sidetopbar: #070d19; --nav-selected-color: #0f1a2e; --nav-selected-color-text: #76c8f5; --nav-color-active: #d1d1d1; @@ -66,4 +94,10 @@ html[theme="dark"] { --nav-border-color: #0b305e; --nav-border-selected-color: #76c8f5; --background: url("/img/qortal_background_dark_.jpg"); -} + --chatHeadBg: #008fd5; + --chatHeadBgActive: #0f1a2e; + --chatHeadText: #ffffff; + --chatHeadTextActive: #ffffff; + --group-header: #c8c8c8; + --group-drop-shadow: rgb(191 191 191 / 32%) 0px 1px 0px +} \ No newline at end of file diff --git a/qortal-ui-core/tooling/generateBuildConfig.js b/qortal-ui-core/tooling/generateBuildConfig.js index 6459a417..1fc2b0d0 100644 --- a/qortal-ui-core/tooling/generateBuildConfig.js +++ b/qortal-ui-core/tooling/generateBuildConfig.js @@ -7,6 +7,8 @@ const commonjs = require('@rollup/plugin-commonjs') const alias = require('@rollup/plugin-alias') const terser = require('@rollup/plugin-terser'); const scss = require('rollup-plugin-scss') +const webWorkerLoader = require('rollup-plugin-web-worker-loader'); + const generateES5BuildConfig = require('./generateES5BuildConfig') @@ -61,6 +63,7 @@ const generateBuildConfig = ({ elementComponents, functionalComponents, otherOut commonjs(), globals(), progress(), + webWorkerLoader(), scss({ output: options.sassOutputDir }), diff --git a/qortal-ui-core/tooling/generateES5BuildConfig.js b/qortal-ui-core/tooling/generateES5BuildConfig.js index b434f3e6..350cf297 100644 --- a/qortal-ui-core/tooling/generateES5BuildConfig.js +++ b/qortal-ui-core/tooling/generateES5BuildConfig.js @@ -5,6 +5,7 @@ const commonjs = require('@rollup/plugin-commonjs'); const progress = require('rollup-plugin-progress'); const terser = require('@rollup/plugin-terser'); const alias = require('@rollup/plugin-alias'); +const webWorkerLoader = require('rollup-plugin-web-worker-loader'); const path = require('path'); @@ -37,6 +38,7 @@ const generateRollupConfig = (file, { outputDir, aliases }) => { }), commonjs(), progress(), + webWorkerLoader(), babel.babel({ babelHelpers: 'bundled', exclude: 'node_modules/**' diff --git a/qortal-ui-crypto/api/transactions/PublicizeTransaction.js b/qortal-ui-crypto/api/transactions/PublicizeTransaction.js index 0243901c..569f5e38 100644 --- a/qortal-ui-crypto/api/transactions/PublicizeTransaction.js +++ b/qortal-ui-crypto/api/transactions/PublicizeTransaction.js @@ -1,5 +1,6 @@ -'use strict' -import ChatBase from './chat/ChatBase.js' +"use strict"; +import ChatBase from "./chat/ChatBase.js" +import { QORT_DECIMALS } from "../constants.js" export default class PublicizeTransaction extends ChatBase { constructor() { @@ -8,16 +9,19 @@ export default class PublicizeTransaction extends ChatBase { this.fee = 0 } - set proofOfWorkNonce(proofOfWorkNonce) { - this._proofOfWorkNonce = this.constructor.utils.int32ToBytes(proofOfWorkNonce) - } - - get params() { - const params = super.params - params.push( - this._proofOfWorkNonce, - this._feeBytes - ) - return params - } + set proofOfWorkNonce(proofOfWorkNonce) { + this._proofOfWorkNonce = this.constructor.utils.int32ToBytes(proofOfWorkNonce) + } + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + get params() { + const params = super.params; + params.push( + this._proofOfWorkNonce, + this._feeBytes + ) + return params; + } } diff --git a/qortal-ui-crypto/api/transactions/groups/UpdateGroupTransaction.js b/qortal-ui-crypto/api/transactions/groups/UpdateGroupTransaction.js index e69de29b..8709f778 100644 --- a/qortal-ui-crypto/api/transactions/groups/UpdateGroupTransaction.js +++ b/qortal-ui-crypto/api/transactions/groups/UpdateGroupTransaction.js @@ -0,0 +1,72 @@ +'use strict'; +import TransactionBase from '../TransactionBase.js' +import Base58 from '../../deps/Base58.js' +import { store } from '../../../api.js' +import { QORT_DECIMALS } from "../../constants.js" + +export default class UpdateGroupTransaction extends TransactionBase { + constructor() { + super() + this.type = 23 + } + + render(html) { + const conf = store.getState().config + return html` + Are you sure to update this group ? +
+ +
+ On pressing confirm, the group details will be updated! + ` + } + + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + set newOwner(newOwner) { + this._newOwner = newOwner instanceof Uint8Array ? newOwner : this.constructor.Base58.decode(newOwner) + } + set newIsOpen(newIsOpen) { + + this._rGroupType = new Uint8Array(1) + this._rGroupType[0] = newIsOpen + } + set newDescription(newDescription) { + this._rGroupDescBytes = this.constructor.utils.stringtoUTF8Array(newDescription.toLocaleLowerCase()) + this._rGroupDescLength = this.constructor.utils.int32ToBytes(this._rGroupDescBytes.length) + } + set newApprovalThreshold(newApprovalThreshold) { + this._rGroupApprovalThreshold = new Uint8Array(1) + this._rGroupApprovalThreshold[0] = newApprovalThreshold; + } + set newMinimumBlockDelay(newMinimumBlockDelay) { + this._rGroupMinimumBlockDelayBytes = this.constructor.utils.int32ToBytes(newMinimumBlockDelay) + } + set newMaximumBlockDelay(newMaximumBlockDelay) { + + this._rGroupMaximumBlockDelayBytes = this.constructor.utils.int32ToBytes(newMaximumBlockDelay) + } + + set _groupId(_groupId){ + this._groupBytes = this.constructor.utils.int32ToBytes(_groupId) + } + get params() { + const params = super.params + params.push( + this._groupBytes, + this._newOwner, + this._rGroupDescLength, + this._rGroupDescBytes, + this._rGroupType, + this._rGroupApprovalThreshold, + this._rGroupMinimumBlockDelayBytes, + this._rGroupMaximumBlockDelayBytes, + this._feeBytes + ) + console.log('verify params', params) + return params + } +} \ No newline at end of file diff --git a/qortal-ui-crypto/api/transactions/transactions.js b/qortal-ui-crypto/api/transactions/transactions.js index b9387184..16b7b09b 100644 --- a/qortal-ui-crypto/api/transactions/transactions.js +++ b/qortal-ui-crypto/api/transactions/transactions.js @@ -16,6 +16,7 @@ import GroupKickTransaction from './groups/GroupKickTransaction.js' import GroupInviteTransaction from './groups/GroupInviteTransaction.js' import CancelGroupInviteTransaction from './groups/CancelGroupInviteTransaction.js' import JoinGroupTransaction from './groups/JoinGroupTransaction.js' +import UpdateGroupTransaction from './groups/UpdateGroupTransaction.js' import LeaveGroupTransaction from './groups/LeaveGroupTransaction.js' import RewardShareTransaction from './reward-share/RewardShareTransaction.js' import RemoveRewardShareTransaction from './reward-share/RemoveRewardShareTransaction.js' diff --git a/qortal-ui-plugins/build-config.js b/qortal-ui-plugins/build-config.js index 3bce9c67..fc0a8538 100644 --- a/qortal-ui-plugins/build-config.js +++ b/qortal-ui-plugins/build-config.js @@ -8,6 +8,8 @@ const commonjs = require('@rollup/plugin-commonjs'); const alias = require('@rollup/plugin-alias'); const terser = require('@rollup/plugin-terser'); const babel = require('@rollup/plugin-babel'); +const webWorkerLoader = require('rollup-plugin-web-worker-loader'); + const aliases = {}; @@ -40,6 +42,7 @@ const generateRollupConfig = (inputFile, outputFile) => { commonjs(), globals(), progress(), + webWorkerLoader(), babel.babel({ babelHelpers: 'bundled', exclude: 'node_modules/**', diff --git a/qortal-ui-plugins/package.json b/qortal-ui-plugins/package.json index 4a0c754e..7e6583d1 100644 --- a/qortal-ui-plugins/package.json +++ b/qortal-ui-plugins/package.json @@ -17,9 +17,31 @@ "author": "QORTAL ", "license": "GPL-3.0", "dependencies": { + "@lit-labs/motion": "1.0.3", "@material/mwc-list": "0.27.0", "@material/mwc-select": "0.27.0", - "emoji-picker-js": "https://github.com/Qortal/emoji-picker-js" + "@tiptap/core": "2.0.0-beta.209", + "@tiptap/extension-image": "2.0.0-beta.209", + "@tiptap/extension-placeholder": "2.0.0-beta.209", + "@tiptap/extension-underline": "2.0.0-beta.209", + "@tiptap/extension-highlight": "2.0.0-beta.209", + "@tiptap/html": "2.0.0-beta.209", + "@tiptap/starter-kit": "2.0.0-beta.209", + "asmcrypto.js": "2.3.2", + "compressorjs": "1.1.1", + "emoji-picker-js": "https://github.com/Qortal/emoji-picker-js", + "prosemirror-commands": "1.5.0", + "prosemirror-dropcursor": "1.6.1", + "prosemirror-gapcursor": "1.3.1", + "prosemirror-history": "1.3.0", + "prosemirror-keymap": "1.2.0", + "prosemirror-model": "1.18.3", + "prosemirror-schema-list": "1.2.2", + "prosemirror-state": "1.4.2", + "prosemirror-transform": "1.7.0", + "prosemirror-view": "1.29.1", + "localforage": "1.10.0", + "short-unique-id": "4.4.4" }, "devDependencies": { "@babel/core": "7.20.12", @@ -60,7 +82,8 @@ "lit-translate": "2.0.1", "rollup": "3.10.0", "rollup-plugin-node-globals": "1.4.0", - "rollup-plugin-progress": "1.1.2" + "rollup-plugin-progress": "1.1.2", + "rollup-plugin-web-worker-loader": "1.6.1" }, "engines": { "node": ">=16.17.1" diff --git a/qortal-ui-plugins/plugins/core/components/ChatGroupInvites.js b/qortal-ui-plugins/plugins/core/components/ChatGroupInvites.js new file mode 100644 index 00000000..27cc0a26 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/components/ChatGroupInvites.js @@ -0,0 +1,335 @@ +import { LitElement, html, css } from "lit" +import { render } from "lit/html.js" +import { get, translate } from "lit-translate" +import { Epml } from "../../../epml" +import snackbar from "./snackbar.js" +import "@material/mwc-button" +import "@material/mwc-dialog" +import "@polymer/paper-spinner/paper-spinner-lite.js" +import "@material/mwc-icon" +import "./WrapperModal" + +const parentEpml = new Epml({ type: "WINDOW", source: window.parent }) + +class ChatGroupInvites extends LitElement { + static get properties() { + return { + isLoading: { type: Boolean }, + isOpenLeaveModal: { type: Boolean }, + leaveGroupObj: { type: Object }, + error: { type: Boolean }, + message: { type: String }, + chatHeads: { type: Array }, + groupAdmin: { attribute: false }, + groupMembers: { attribute: false }, + selectedHead: { type: Object }, + } + } + + constructor() { + super() + this.isLoading = false + this.isOpenLeaveModal = false + this.leaveGroupObj = {} + this.leaveFee = 0.001 + this.error = false + this.message = "" + this.chatHeads = [] + this.groupAdmin = [] + this.groupMembers = [] + } + + static get styles() { + return css` + .top-bar-icon { + cursor: pointer; + height: 18px; + width: 18px; + transition: 0.2s all; + } + .top-bar-icon:hover { + color: var(--black); + } + .modal-button { + font-family: Roboto, sans-serif; + font-size: 16px; + color: var(--mdc-theme-primary); + background-color: transparent; + padding: 8px 10px; + border-radius: 5px; + border: none; + transition: all 0.3s ease-in-out; + } + ` + } + + firstUpdated() {} + + timeIsoString(timestamp) { + let myTimestamp = timestamp === undefined ? 1587560082346 : timestamp + let time = new Date(myTimestamp) + return time.toISOString() + } + + resetDefaultSettings() { + this.error = false + this.message = "" + this.isLoading = false + } + + renderErr9Text() { + return html`${translate("grouppage.gchange49")}` + } + + async confirmRelationship(reference) { + let interval = null + let stop = false + const getAnswer = async () => { + + + if (!stop) { + stop = true + try { + let myRef = await parentEpml.request("apiCall", { + type: "api", + url: `/transactions/reference/${reference}`, + }) + if (myRef && myRef.type) { + clearInterval(interval) + this.isLoading = false + this.isOpenLeaveModal = false + } + } catch (error) {} + stop = false + } + } + interval = setInterval(getAnswer, 5000) + } + + async getLastRef() { + let myRef = await parentEpml.request("apiCall", { + type: "api", + url: `/addresses/lastreference/${this.selectedAddress.address}`, + }) + return myRef + } + + getTxnRequestResponse(txnResponse, reference) { + if (txnResponse === true) { + this.message = this.renderErr9Text() + this.error = false + this.confirmRelationship(reference) + } else { + this.error = true + this.message = "" + throw new Error(txnResponse) + } + } + + async convertBytesForSigning(transactionBytesBase58) { + let convertedBytes = await parentEpml.request("apiCall", { + type: "api", + method: "POST", + url: `/transactions/convert`, + body: `${transactionBytesBase58}`, + }) + return convertedBytes + } + + async signTx(body){ + return await parentEpml.request("apiCall", { + type: "api", + method: "POST", + url: `/transactions/sign`, + body: body, + headers: { + 'Content-Type': 'application/json' + } + }) + } + + async process(body){ + return await parentEpml.request("apiCall", { + type: "api", + method: "POST", + url: `/transactions/process`, + body: body, + }) + } + async _addAdmin(groupId) { + // Reset Default Settings... + this.resetDefaultSettings() + const leaveFeeInput = this.leaveFee + + this.isLoading = true + + // Get Last Ref + + const validateReceiver = async () => { + let lastRef = await this.getLastRef() + let myTransaction = await makeTransactionRequest(lastRef) + this.getTxnRequestResponse(myTransaction, lastRef ) + } + + // Make Transaction Request + const makeTransactionRequest = async (lastRef) => { + const body = { + timestamp: Date.now(), + reference: lastRef, + fee: leaveFeeInput, + ownerPublicKey: window.parent.Base58.encode( + window.parent.reduxStore.getState().app.selectedAddress + .keyPair.publicKey + ), + groupId: groupId, + member: this.selectedHead.address, + } + const bodyToString = JSON.stringify(body) + let transactionBytes = await parentEpml.request("apiCall", { + type: "api", + method: "POST", + url: `/groups/addadmin`, + body: bodyToString, + headers: { + "Content-Type": "application/json", + }, + }) + const readforsign = await this.convertBytesForSigning( + transactionBytes + ) + const body2 = { + privateKey: window.parent.Base58.encode( + window.parent.reduxStore.getState().app.selectedAddress + .keyPair.privateKey + ), + transactionBytes: readforsign, + } + const bodyToString2 = JSON.stringify(body2) + let signTransaction = await this.signTx(bodyToString2) + let processTransaction = await this.process(signTransaction) + return processTransaction + } + + validateReceiver() + } + + async _removeAdmin(groupId) { + // Reset Default Settings... + this.resetDefaultSettings() + const leaveFeeInput = this.leaveFee + + this.isLoading = true + + // Get Last Ref + + const validateReceiver = async () => { + let lastRef = await this.getLastRef() + let myTransaction = await makeTransactionRequest(lastRef) + this.getTxnRequestResponse(myTransaction, lastRef) + } + + // Make Transaction Request + const makeTransactionRequest = async (lastRef) => { + const body = { + timestamp: Date.now(), + reference: lastRef, + fee: leaveFeeInput, + ownerPublicKey: window.parent.Base58.encode( + window.parent.reduxStore.getState().app.selectedAddress + .keyPair.publicKey + ), + groupId: groupId, + admin: this.selectedHead.address, + } + const bodyToString = JSON.stringify(body) + let transactionBytes = await parentEpml.request("apiCall", { + type: "api", + method: "POST", + url: `/groups/removeadmin`, + body: bodyToString, + headers: { + "Content-Type": "application/json", + }, + }) + const readforsign = await this.convertBytesForSigning( + transactionBytes + ) + const body2 = { + privateKey: window.parent.Base58.encode( + window.parent.reduxStore.getState().app.selectedAddress + .keyPair.privateKey + ), + transactionBytes: readforsign, + } + const bodyToString2 = JSON.stringify(body2) + let signTransaction = await this.signTx(bodyToString2) + let processTransaction = await this.process(signTransaction) + return processTransaction + } + + validateReceiver() + } + + render() { + console.log("leaveGroupObj", this.leaveGroupObj) + return html` + { + this.isOpenLeaveModal = true + }} class="top-bar-icon" style="margin: 0px 20px" icon="vaadin:users" slot="icon"> + + { + if (this.isLoading) return + this.isOpenLeaveModal = false + }} + style=${ + this.isOpenLeaveModal ? "display: block" : "display: none" + }> +
+

${translate("grouppage.gchange35")}

+
+
+ + + +
+ + + ${translate("grouppage.gchange36")}   + + + + + ${this.message} + +
+ + + +
+ ` + } +} + +customElements.define("chat-right-panel", ChatGroupInvites) diff --git a/qortal-ui-plugins/plugins/core/components/ChatGroupSettings.js b/qortal-ui-plugins/plugins/core/components/ChatGroupSettings.js new file mode 100644 index 00000000..ac50df20 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/components/ChatGroupSettings.js @@ -0,0 +1,283 @@ +import { LitElement, html, css } from 'lit'; +import { render } from 'lit/html.js'; +import { get, translate } from 'lit-translate'; +import { Epml } from '../../../epml'; +import snackbar from './snackbar.js' +import '@material/mwc-button'; +import '@material/mwc-dialog'; +import '@polymer/paper-spinner/paper-spinner-lite.js' +import '@material/mwc-icon'; +import './WrapperModal'; + +const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) + +class ChatGroupSettings extends LitElement { + static get properties() { + return { + isLoading: { type: Boolean }, + isOpenLeaveModal: {type: Boolean}, + leaveGroupObj: { type: Object }, + error: {type: Boolean}, + message: {type: String}, + chatHeads: {type: Array}, + setActiveChatHeadUrl: {attribute: false} + } + } + + constructor() { + super(); + this.isLoading = false; + this.isOpenLeaveModal = false + this.leaveGroupObj = {} + this.leaveFee = 0.001 + this.error = false + this.message = '' + this.chatHeads = [] + } + + static get styles() { + return css` + .top-bar-icon { + cursor: pointer; + height: 18px; + width: 18px; + transition: .2s all; + } + + .top-bar-icon:hover { + color: var(--black) + } + + .modal-button { + font-family: Roboto, sans-serif; + font-size: 16px; + color: var(--mdc-theme-primary); + background-color: transparent; + padding: 8px 10px; + border-radius: 5px; + border: none; + transition: all 0.3s ease-in-out; + } + ` + } + + firstUpdated() { + + } + + timeIsoString(timestamp) { + let myTimestamp = timestamp === undefined ? 1587560082346 : timestamp + let time = new Date(myTimestamp) + return time.toISOString() + } + + resetDefaultSettings() { + this.error = false + this.message = '' + this.isLoading = false + } + + renderErr9Text() { + return html`${translate("grouppage.gchange49")}` + } + + async confirmRelationship() { + + + let interval = null + let stop = false + const getAnswer = async () => { + const currentChats = this.chatHeads + + if (!stop) { + stop = true; + try { + const findGroup = currentChats.find((item)=> item.groupId === this.leaveGroupObj.groupId) + if (!findGroup) { + clearInterval(interval) + this.isLoading = false + this.isOpenLeaveModal= false + this.setActiveChatHeadUrl('') + } + + } catch (error) { + } + stop = false + } + }; + interval = setInterval(getAnswer, 5000); + } + + async _convertToPrivate(groupId) { + // Reset Default Settings... + this.resetDefaultSettings() + const leaveFeeInput = this.leaveFee + + this.isLoading = true + + // Get Last Ref + const getLastRef = async () => { + let myRef = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/lastreference/${this.selectedAddress.address}` + }) + return myRef + }; + + const validateReceiver = async () => { + let lastRef = await getLastRef(); + let myTransaction = await makeTransactionRequest(lastRef) + getTxnRequestResponse(myTransaction) + + } + const convertBytesForSigning = async (transactionBytesBase58) => { + let convertedBytes = await parentEpml.request("apiCall", { + type: "api", + method: "POST", + url: `/transactions/convert`, + body: `${transactionBytesBase58}`, + }) + return convertedBytes + } + + + // Make Transaction Request + const makeTransactionRequest = async (lastRef) => { + let groupdialog3 = get("transactions.groupdialog3") + let groupdialog4 = get("transactions.groupdialog4") + + const body = { + "timestamp": Date.now(), + "reference": lastRef, + "fee": leaveFeeInput, + "ownerPublicKey": window.parent.Base58.encode(window.parent.reduxStore.getState().app.selectedAddress.keyPair.publicKey), + "groupId": groupId, + "newOwner": "QdR4bQ1fJFnSZgswtW27eE8ToXwHqUQyaU", + "newIsOpen": false, + "newDescription": "my group for accounts I like", + "newApprovalThreshold": "NONE", + "newMinimumBlockDelay": 5, + "newMaximumBlockDelay": 60 + } + console.log('STRING3') + // const bodyToString = JSON.stringify(body) + // let transactionBytes = await parentEpml.request("apiCall", { + // type: "api", + // method: "POST", + // url: `/groups/update`, + // body: bodyToString, + // headers: { + // 'Content-Type': 'application/json' + // } + // }) + // console.log({transactionBytes}) + // const readforsign = await convertBytesForSigning(transactionBytes) + // // const res = await signAndProcess(transactionBytes) + // const body2 = { + // "privateKey": window.parent.Base58.encode(window.parent.reduxStore.getState().app.selectedAddress.keyPair.privateKey), + // "transactionBytes": readforsign + // } + // const bodyToString2 = JSON.stringify(body2) + // let signTransaction = await parentEpml.request("apiCall", { + // type: "api", + // method: "POST", + // url: `/transactions/sign`, + // body: bodyToString2, + // headers: { + // 'Content-Type': 'application/json' + // } + // }) + // let processTransaction = await parentEpml.request("apiCall", { + // type: "api", + // method: "POST", + // url: `/transactions/process`, + // body: signTransaction, + // }) + // return processTransaction + console.log('this.selectedAddress.nonce', this.selectedAddress.nonce) + let myTxnrequest = await parentEpml.request('transaction', { + type: 23, + nonce: this.selectedAddress.nonce, + params: { + _groupId: groupId, + lastReference: lastRef, + fee: leaveFeeInput, + "newOwner": "QdR4bQ1fJFnSZgswtW27eE8ToXwHqUQyaU", + "newIsOpen": false, + "newDescription": "my group for accounts I like", + "newApprovalThreshold": "NONE", + "newMinimumBlockDelay": 5, + "newMaximumBlockDelay": 60 + } + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + + if (txnResponse === true) { + this.message = this.renderErr9Text() + this.error = false + this.confirmRelationship() + } else { + this.error = true + this.message = "" + throw new Error(txnResponse) + } + } + validateReceiver() + } + + render() { + console.log('leaveGroupObj', this.leaveGroupObj) + return html` + { + this.isOpenLeaveModal = true + }} class="top-bar-icon" style="margin: 0px 20px" icon="vaadin:cog" slot="icon"> + + { + if(this.isLoading) return + this.isOpenLeaveModal = false + } } + style=${(this.isOpenLeaveModal) ? "display: block" : "display: none"}> +
+

${translate("grouppage.gchange35")}

+
+
+ < + + +
+ + + ${translate("grouppage.gchange36")}   + + + + + ${this.message} + +
+ + + +
+ `; + } +} + +customElements.define('chat-group-settings', ChatGroupSettings); \ No newline at end of file diff --git a/qortal-ui-plugins/plugins/core/components/ChatGroupsManagement.js b/qortal-ui-plugins/plugins/core/components/ChatGroupsManagement.js new file mode 100644 index 00000000..a6950b77 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/components/ChatGroupsManagement.js @@ -0,0 +1,296 @@ +import { LitElement, html, css } from 'lit'; +import { render } from 'lit/html.js'; +import { get, translate } from 'lit-translate'; +import { Epml } from '../../../epml'; +import snackbar from './snackbar.js' +import '@material/mwc-button'; +import '@material/mwc-dialog'; +import '@polymer/paper-spinner/paper-spinner-lite.js' +import '@material/mwc-icon'; +import './WrapperModal'; +import '@vaadin/tabs' +import '@vaadin/tabs/theme/material/vaadin-tabs.js'; +import '@vaadin/avatar'; +import '@vaadin/grid'; +import '@vaadin/grid/vaadin-grid-filter-column.js'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; + +const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) + +class ChatGroupsManagement extends LitElement { + static get properties() { + return { + isLoading: { type: Boolean }, + isOpenLeaveModal: {type: Boolean}, + leaveGroupObj: { type: Object }, + error: {type: Boolean}, + message: {type: String}, + chatHeads: {type: Array}, + setActiveChatHeadUrl: {attribute: false}, + selectedAddress: {attribute: Object}, + currentTab: {type: Number}, + groups: {type: Array} + } + } + + constructor() { + super(); + this.isLoading = false; + this.isOpenLeaveModal = false + this.leaveGroupObj = {} + this.fee = null + this.error = false + this.message = '' + this.chatHeads = [] + this.currentTab = 0 + this.groups = [] + } + + static get styles() { + return css` + .top-bar-icon { + cursor: pointer; + height: 18px; + width: 18px; + transition: .2s all; + } + .top-bar-icon:hover { + color: var(--black) + } + .modal-button { + font-family: Roboto, sans-serif; + font-size: 16px; + color: var(--mdc-theme-primary); + background-color: transparent; + padding: 8px 10px; + border-radius: 5px; + border: none; + transition: all 0.3s ease-in-out; + } + ` + } + + async getJoinedGroups(){ + let joinedG = await parentEpml.request('apiCall', { + url: `/groups/member/${this.selectedAddress.address}` + }) + return joinedG +} + + async firstUpdated() { + + try { + let _joinedGroups = await this.getJoinedGroups() + this.joinedGroups = _joinedGroups + } catch (error) { + + } + + } + + _tabChanged(e) { + this.currentTab = e.detail.value + } + + async unitFee() { + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + const url = `${nodeUrl}/transactions/unitfee?txType=LEAVE_GROUP` + let fee = null + + try { + const res = await fetch(url) + const data = await res.json() + fee = (Number(data) / 1e8).toFixed(3) + } catch (error) { + fee = null + } + + return fee + } + + timeIsoString(timestamp) { + let myTimestamp = timestamp === undefined ? 1587560082346 : timestamp + let time = new Date(myTimestamp) + return time.toISOString() + } + + resetDefaultSettings() { + this.error = false + this.message = '' + this.isLoading = false + } + + renderErr9Text() { + return html`${translate("grouppage.gchange49")}` + } + + async confirmRelationship() { + + + let interval = null + let stop = false + const getAnswer = async () => { + const currentChats = this.chatHeads + + if (!stop) { + stop = true; + try { + const findGroup = currentChats.find((item)=> item.groupId === this.leaveGroupObj.groupId) + if (!findGroup) { + clearInterval(interval) + this.isLoading = false + this.isOpenLeaveModal= false + this.setActiveChatHeadUrl('') + } + + } catch (error) { + } + stop = false + } + }; + interval = setInterval(getAnswer, 5000); + } + + async _leaveGroup(groupId, groupName) { + // Reset Default Settings... + this.resetDefaultSettings() + + const leaveFeeInput = await this.unitFee() + if(!leaveFeeInput){ + throw Error() + } + this.isLoading = true + + // Get Last Ref + const getLastRef = async () => { + let myRef = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/lastreference/${this.selectedAddress.address}` + }) + return myRef + }; + + const validateReceiver = async () => { + let lastRef = await getLastRef(); + let myTransaction = await makeTransactionRequest(lastRef) + getTxnRequestResponse(myTransaction) + + } + + // Make Transaction Request + const makeTransactionRequest = async (lastRef) => { + let groupdialog3 = get("transactions.groupdialog3") + let groupdialog4 = get("transactions.groupdialog4") + let myTxnrequest = await parentEpml.request('transaction', { + type: 32, + nonce: this.selectedAddress.nonce, + params: { + fee: leaveFeeInput, + registrantAddress: this.selectedAddress.address, + rGroupName: groupName, + rGroupId: groupId, + lastReference: lastRef, + groupdialog3: groupdialog3, + groupdialog4: groupdialog4, + } + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + + if (txnResponse.success === false && txnResponse.message) { + this.error = true + this.message = txnResponse.message + throw new Error(txnResponse) + } else if (txnResponse.success === true && !txnResponse.data.error) { + this.message = this.renderErr9Text() + this.error = false + this.confirmRelationship() + } else { + this.error = true + this.message = txnResponse.data.message + throw new Error(txnResponse) + } + } + validateReceiver() + } + + nameRenderer(person){ + console.log({person}) + return html` + + + ${person.displayName} + + `; + }; + + render() { + return html` + + + { + if(this.isLoading) return + this.isOpenLeaveModal = false + } } + customStyle=${"width: 90%; max-width: 900px; height: 90%"} + style=${(this.isOpenLeaveModal) ? "display: block" : "display: none"}> +
+
+ + + Groups + Group Join Requests + Invites + Blocked Users + +
+ +
+ + ${this.currentTab === 0 ? html` +
+ + + + +

Search groups

+ +

Current groups as owner

+

Current groups as member

+
+ ` : ''} + + +
+
+ + +
+
+
+ `; + } +} + +customElements.define('chat-groups-management', ChatGroupsManagement); \ No newline at end of file diff --git a/qortal-ui-plugins/plugins/core/components/ChatHead.js b/qortal-ui-plugins/plugins/core/components/ChatHead.js index 418f9cb1..c28a583f 100644 --- a/qortal-ui-plugins/plugins/core/components/ChatHead.js +++ b/qortal-ui-plugins/plugins/core/components/ChatHead.js @@ -13,14 +13,18 @@ class ChatHead extends LitElement { config: { type: Object }, chatInfo: { type: Object }, iconName: { type: String }, - activeChatHeadUrl: { type: String } + activeChatHeadUrl: { type: String }, + isImageLoaded: { type: Boolean }, + setActiveChatHeadUrl: {attribute: false} } } static get styles() { return css` li { - padding: 10px 2px 20px 5px; + + width: 100%; + padding: 7px 5px 7px 5px; cursor: pointer; width: 100%; } @@ -37,7 +41,7 @@ class ChatHead extends LitElement { .img-icon { float: left; font-size:40px; - color: var(--black); + color: var(--chat-group); } .about { @@ -76,14 +80,54 @@ class ChatHead extends LitElement { this.chatInfo = {} this.iconName = '' this.activeChatHeadUrl = '' + this.isImageLoaded = false + this.imageFetches = 0 } + createImage(imageUrl) { + const imageHTMLRes = new Image(); + imageHTMLRes.src = imageUrl; + imageHTMLRes.style= "width:40px; height:40px; float: left; border-radius:50%"; + imageHTMLRes.onclick= () => { + this.openDialogImage = true; + } + imageHTMLRes.onload = () => { + this.isImageLoaded = true; + } + imageHTMLRes.onerror = () => { + if (this.imageFetches < 4) { + setTimeout(() => { + this.imageFetches = this.imageFetches + 1; + imageHTMLRes.src = imageUrl; + }, 500); + } else { + + + this.isImageLoaded = false + } + }; + return imageHTMLRes; + } + render() { + let avatarImg = ''; + let backupAvatarImg = '' + if(this.chatInfo.name){ + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]; + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port; + const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.chatInfo.name}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`; + avatarImg= this.createImage(avatarUrl) + + } + return html`
  • this.getUrl(this.chatInfo.url)} class="clearfix ${this.activeChatHeadUrl === this.chatInfo.url ? 'active' : ''}"> - account_circle + ${this.isImageLoaded ? html`${avatarImg}` : html`` } + ${!this.isImageLoaded && !this.chatInfo.name && !this.chatInfo.groupName ? html`account_circle` : html`` } + ${!this.isImageLoaded && this.chatInfo.name ? html`
    ${this.chatInfo.name.charAt(0)}
    `: ''} + ${!this.isImageLoaded && this.chatInfo.groupName ? html`
    ${this.chatInfo.groupName.charAt(0)}
    `: ''}
    -
    ${this.chatInfo.groupName ? this.chatInfo.groupName : this.chatInfo.name !== undefined ? this.chatInfo.name : this.chatInfo.address.substr(0, 15)} ${this.chatInfo.groupId !== undefined ? 'lock_open' : 'lock'}
    +
    ${this.chatInfo.groupName ? this.chatInfo.groupName : this.chatInfo.name !== undefined ? this.chatInfo.name : this.chatInfo.address.substr(0, 15)} ${this.chatInfo.groupId !== undefined ? 'lock_open' : 'lock'}
  • ` @@ -108,8 +152,19 @@ class ChatHead extends LitElement { parentEpml.imReady() } + shouldUpdate(changedProperties) { + if(changedProperties.has('activeChatHeadUrl')){ + return true + } + if(changedProperties.has('chatInfo')){ + return true + } + + return false + } + getUrl(chatUrl) { - this.onPageNavigation(`/app/q-chat/${chatUrl}`) + this.setActiveChatHeadUrl(chatUrl) } onPageNavigation(pageUrl) { diff --git a/qortal-ui-plugins/plugins/core/components/ChatLeaveGroup.js b/qortal-ui-plugins/plugins/core/components/ChatLeaveGroup.js new file mode 100644 index 00000000..be249818 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/components/ChatLeaveGroup.js @@ -0,0 +1,268 @@ +import { LitElement, html, css } from 'lit'; +import { render } from 'lit/html.js'; +import { get, translate } from 'lit-translate'; +import { Epml } from '../../../epml'; +import snackbar from './snackbar.js' +import '@material/mwc-button'; +import '@material/mwc-dialog'; +import '@polymer/paper-spinner/paper-spinner-lite.js' +import '@material/mwc-icon'; +import './WrapperModal'; +const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) + +class ChatLeaveGroup extends LitElement { + static get properties() { + return { + isLoading: { type: Boolean }, + isOpenLeaveModal: {type: Boolean}, + leaveGroupObj: { type: Object }, + error: {type: Boolean}, + message: {type: String}, + chatHeads: {type: Array}, + setActiveChatHeadUrl: {attribute: false}, + selectedAddress: {attribute: Object} + } + } + + constructor() { + super(); + this.isLoading = false; + this.isOpenLeaveModal = false + this.leaveGroupObj = {} + this.fee = null + this.error = false + this.message = '' + this.chatHeads = [] + } + + static get styles() { + return css` + .top-bar-icon { + cursor: pointer; + height: 18px; + width: 18px; + transition: .2s all; + } + .top-bar-icon:hover { + color: var(--black) + } + .modal-button { + font-family: Roboto, sans-serif; + font-size: 16px; + color: var(--mdc-theme-primary); + background-color: transparent; + padding: 8px 10px; + border-radius: 5px; + border: none; + transition: all 0.3s ease-in-out; + } + ` + } + + firstUpdated() { + + } + + async unitFee() { + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + const url = `${nodeUrl}/transactions/unitfee?txType=LEAVE_GROUP` + let fee = null + + try { + const res = await fetch(url) + const data = await res.json() + fee = (Number(data) / 1e8).toFixed(3) + } catch (error) { + fee = null + } + + return fee + } + + timeIsoString(timestamp) { + let myTimestamp = timestamp === undefined ? 1587560082346 : timestamp + let time = new Date(myTimestamp) + return time.toISOString() + } + + resetDefaultSettings() { + this.error = false + this.message = '' + this.isLoading = false + } + + renderErr9Text() { + return html`${translate("grouppage.gchange49")}` + } + + async confirmRelationship() { + + + let interval = null + let stop = false + const getAnswer = async () => { + const currentChats = this.chatHeads + + if (!stop) { + stop = true; + try { + const findGroup = currentChats.find((item)=> item.groupId === this.leaveGroupObj.groupId) + if (!findGroup) { + clearInterval(interval) + this.isLoading = false + this.isOpenLeaveModal= false + this.setActiveChatHeadUrl('') + } + + } catch (error) { + } + stop = false + } + }; + interval = setInterval(getAnswer, 5000); + } + + async _leaveGroup(groupId, groupName) { + // Reset Default Settings... + this.resetDefaultSettings() + + const leaveFeeInput = await this.unitFee() + if(!leaveFeeInput){ + throw Error() + } + this.isLoading = true + + // Get Last Ref + const getLastRef = async () => { + let myRef = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/lastreference/${this.selectedAddress.address}` + }) + return myRef + }; + + const validateReceiver = async () => { + let lastRef = await getLastRef(); + let myTransaction = await makeTransactionRequest(lastRef) + getTxnRequestResponse(myTransaction) + + } + + // Make Transaction Request + const makeTransactionRequest = async (lastRef) => { + let groupdialog3 = get("transactions.groupdialog3") + let groupdialog4 = get("transactions.groupdialog4") + let myTxnrequest = await parentEpml.request('transaction', { + type: 32, + nonce: this.selectedAddress.nonce, + params: { + fee: leaveFeeInput, + registrantAddress: this.selectedAddress.address, + rGroupName: groupName, + rGroupId: groupId, + lastReference: lastRef, + groupdialog3: groupdialog3, + groupdialog4: groupdialog4, + } + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + + if (txnResponse.success === false && txnResponse.message) { + this.error = true + this.message = txnResponse.message + throw new Error(txnResponse) + } else if (txnResponse.success === true && !txnResponse.data.error) { + this.message = this.renderErr9Text() + this.error = false + this.confirmRelationship() + } else { + this.error = true + this.message = txnResponse.data.message + throw new Error(txnResponse) + } + } + validateReceiver() + } + + render() { + return html` + { + this.isOpenLeaveModal = true + }} class="top-bar-icon" style="margin: 0px 20px" icon="vaadin:exit" slot="icon"> + + { + if(this.isLoading) return + this.isOpenLeaveModal = false + } } + style=${(this.isOpenLeaveModal) ? "display: block" : "display: none"}> +
    +

    ${translate("grouppage.gchange35")}

    +
    +
    + +
    + ${translate("grouppage.gchange4")} +
    +
    ${this.leaveGroupObj.groupName}
    + + ${translate("grouppage.gchange5")} +
    +
    ${this.leaveGroupObj.description}
    + + ${translate("grouppage.gchange10")} +
    +
    ${this.leaveGroupObj.owner}
    + + ${translate("grouppage.gchange31")} +
    +
    + + ${!this.leaveGroupObj.updated ? "" : html`${translate("grouppage.gchange32")} +
    +
    `} +
    + +
    + + + ${translate("grouppage.gchange36")}   + + + + + ${this.message} + +
    + + + +
    + `; + } +} + +customElements.define('chat-leave-group', ChatLeaveGroup); \ No newline at end of file diff --git a/qortal-ui-plugins/plugins/core/components/ChatModals.js b/qortal-ui-plugins/plugins/core/components/ChatModals.js index 62e9b4e6..0778bd10 100644 --- a/qortal-ui-plugins/plugins/core/components/ChatModals.js +++ b/qortal-ui-plugins/plugins/core/components/ChatModals.js @@ -92,11 +92,11 @@ class ChatModals extends LitElement { // Send Private Message _sendMessage() { - this.isLoading = true + this.isLoading = true; - const recipient = this.shadowRoot.getElementById('sendTo').value - const messageBox = this.shadowRoot.getElementById('messageBox') - const messageText = messageBox.value + const recipient = this.shadowRoot.getElementById('sendTo').value; + const messageBox = this.shadowRoot.getElementById('messageBox'); + const messageText = messageBox.value; if (recipient.length === 0) { this.isLoading = false @@ -105,22 +105,21 @@ class ChatModals extends LitElement { } else { this.sendMessage() } - } + }; async sendMessage() { - this.isLoading = true - - const _recipient = this.shadowRoot.getElementById('sendTo').value - const messageBox = this.shadowRoot.getElementById('messageBox') - const messageText = messageBox.value - let recipient + this.isLoading = true; + const _recipient = this.shadowRoot.getElementById('sendTo').value; + const messageBox = this.shadowRoot.getElementById('messageBox'); + const messageText = messageBox.value; + let recipient; const validateName = async (receiverName) => { - let myRes + let myRes; let myNameRes = await parentEpml.request('apiCall', { type: 'api', url: `/names/${receiverName}` - }) + }); if (myNameRes.error === 401) { myRes = false @@ -128,7 +127,7 @@ class ChatModals extends LitElement { myRes = myNameRes } - return myRes + return myRes; } const myNameRes = await validateName(_recipient) @@ -139,7 +138,6 @@ class ChatModals extends LitElement { recipient = myNameRes.owner } - let _reference = new Uint8Array(64); window.crypto.getRandomValues(_reference); @@ -175,7 +173,13 @@ class ChatModals extends LitElement { }; const sendMessageRequest = async (isEncrypted, _publicKey) => { - + const messageObject = { + messageText, + images: [''], + repliedTo: '', + version: 1 + } + const stringifyMessageObject = JSON.stringify(messageObject) let chatResponse = await parentEpml.request('chat', { type: 18, nonce: this.selectedAddress.nonce, @@ -184,7 +188,7 @@ class ChatModals extends LitElement { recipient: recipient, recipientPublicKey: _publicKey, hasChatReference: 0, - message: messageText, + message: stringifyMessageObject, lastReference: reference, proofOfWorkNonce: 0, isEncrypted: isEncrypted, @@ -361,7 +365,10 @@ class ChatModals extends LitElement {

    - ${translate('welcomepage.wcchange6')} + { + this._sendMessage(); + } + }>${translate('welcomepage.wcchange6')} fetch(`/language/${lang}.json`).then(res => res.json()) -}) - -import { escape, unescape } from 'html-escaper'; -import { inputKeyCodes } from '../../utils/keyCodes.js' -import './ChatScroller.js' -import './LevelFounder.js' -import './NameMenu.js' -import './TimeAgo.js' -import { EmojiPicker } from 'emoji-picker-js' +}); +import ShortUniqueId from 'short-unique-id'; +import Compressor from 'compressorjs'; +import { escape } from 'html-escaper'; +import { inputKeyCodes } from '../../utils/keyCodes.js'; +import './ChatScroller.js'; +import './LevelFounder.js'; +import './NameMenu.js'; +import './TimeAgo.js'; +import './ChatTextEditor.js'; +import './WrapperModal.js'; +import './TipUser' +import './ChatSelect.js' +import './ChatSideNavHeads.js' +import './ChatLeaveGroup.js' +import './ChatGroupSettings.js' +import './ChatRightPanel.js' +import './ChatSeachResults.js'; +import '@polymer/paper-spinner/paper-spinner-lite.js'; +import '@material/mwc-button'; +import '@material/mwc-dialog'; +import '@material/mwc-icon'; +import { replaceMessagesEdited } from '../../utils/replace-messages-edited.js'; +import { publishData } from '../../utils/publish-image.js'; +import { EmojiPicker } from 'emoji-picker-js'; +import WebWorker from 'web-worker:./computePowWorker.js'; +import WebWorkerImage from 'web-worker:./computePowWorkerImage.js'; import '@polymer/paper-dialog/paper-dialog.js' -import '@polymer/paper-spinner/paper-spinner-lite.js' -import '@material/mwc-button' -import '@material/mwc-dialog' -import '@material/mwc-icon' +// const messagesCache = localForage.createInstance({ +// name: "messages-cache", +// }); const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) @@ -43,68 +68,429 @@ class ChatPage extends LitElement { _initialMessages: { type: Array }, isUserDown: { type: Boolean }, isPasteMenuOpen: { type: Boolean }, - showNewMesssageBar: { attribute: false }, - hideNewMesssageBar: { attribute: false }, + showNewMessageBar: { attribute: false }, + hideNewMessageBar: { attribute: false }, + setOpenPrivateMessage: { attribute: false }, chatEditorPlaceholder: { type: String }, messagesRendered: { type: Array }, - myTrimmedMeassage: { type: String } + repliedToMessageObj: { type: Object }, + editedMessageObj: { type: Object }, + iframeHeight: { type: Number }, + imageFile: { type: Object }, + isUploadingImage: { type: Boolean }, + userLanguage: { type: String }, + lastMessageRefVisible: { type: Boolean }, + isLoadingOldMessages: { type: Boolean }, + isEditMessageOpen: { type: Boolean }, + webSocket: { attribute: false }, + chatHeads: { type: Array }, + forwardActiveChatHeadUrl: { type: Object }, + openForwardOpen: {type: Boolean }, + groupAdmin: {type: Array}, + groupMembers: {type: Array}, + shifted: {type: Boolean}, + groupInfo: {type: Object}, + setActiveChatHeadUrl: {attribute: false}, + userFound: { type: Array }, + userFoundModalOpen: { type: Boolean }, + webWorker: { type: Object }, + webWorkerImage: { type: Object }, + myTrimmedMeassage: { type: String }, + editor: {type: Object}, + currentEditor: {type: String}, + isEnabledChatEnter: {type: Boolean}, + openTipUser: { type: Boolean }, + openUserInfo: { type: Boolean }, + selectedHead: { type: Object }, + userName: { type: String }, + goToRepliedMessage: {attribute: false} } } static get styles() { - return css` - html { - scroll-behavior: smooth; - } - .chat-text-area { - display: flex; - justify-content: center; - overflow: hidden; - } - .chat-text-area .typing-area { - display: flex; - flex-direction: row; - position: absolute; - bottom: 0; - width: 98%; - box-sizing: border-box; - padding: 5px; - margin-bottom: 8px; - border: 1px solid var(--black); - border-radius: 10px; - background: #f1f1f1; - color: var(--black); - } - .chat-text-area .typing-area textarea { - display: none; - } - .chat-text-area .typing-area .chat-editor { - border-color: transparent; - flex: 1; - max-height: 40px; - height: 40px; - margin: 0; - padding: 0; - border: none; - } - .chat-text-area .typing-area .emoji-button { - width: 45px; - height: 40px; - padding: 5px; - border: none; - outline: none; - background: transparent; - cursor: pointer; - max-height: 40px; - color: var(--black); - } - .float-left { - float: left; - } - img { - border-radius: 25%; - } - paper-dialog.warning { + return css` + html { + scroll-behavior: smooth; + } + + .chat-head-container { + display: flex; + justify-content: flex-start; + flex-direction: column; + height: 50vh; + overflow-y: auto; + overflow-x: hidden; + width: 100%; + } + + .repliedTo-container { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 10px 10px 8px 10px; + } + + .senderName { + margin: 0; + color: var(--mdc-theme-primary); + font-weight: bold; + user-select: none; + } + + .original-message { + color: var(--chat-bubble-msg-color); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + margin: 0; + width: 800px; + } + + .close-icon { + color: #676b71; + width: 18px; + transition: all 0.1s ease-in-out; + } + + .close-icon:hover { + cursor: pointer; + color: #494c50; + } + + .chat-text-area .typing-area .chatbar { + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: auto; + padding: 5px 5px 5px 7px; + overflow: hidden; + } + + .chat-text-area .typing-area .emoji-button { + width: 45px; + height: 40px; + padding-top: 4px; + border: none; + outline: none; + background: transparent; + cursor: pointer; + max-height: 40px; + color: var(--black); + } + + .emoji-button-caption { + width: 45px; + height: 40px; + padding-top: 4px; + border: none; + outline: none; + background: transparent; + cursor: pointer; + max-height: 40px; + color: var(--black); + } + + .caption-container { + width: 100%; + display: flex; + height: auto; + overflow: hidden; + justify-content: center; + background-color: var(--white); + padding: 5px; + border-radius: 1px; + } + + .chatbar-caption { + font-family: Roboto, sans-serif; + width: 70%; + margin-right: 10px; + outline: none; + align-items: center; + font-size: 18px; + resize: none; + border-top: 0; + border-right: 0; + border-left: 0; + border-bottom: 1px solid #cac8c8; + padding: 3px; + } + + .message-size-container { + display: flex; + justify-content: flex-end; + width: 100%; + } + + .message-size { + font-family: Roboto, sans-serif; + font-size: 12px; + color: black; + } + + .lds-grid { + width: 120px; + height: 120px; + position: absolute; + left: 50%; + top: 40%; + } + + img { + border-radius: 25%; + } + + .dialogCustom { + position: fixed; + z-index: 10000; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + top: 10px; + right: 20px; + user-select: none; + } + + .dialogCustomInner { + min-width: 300px; + height: 40px; + background-color: var(--white); + box-shadow: rgb(119 119 119 / 32%) 0px 4px 12px; + padding: 10px; + border-radius: 4px; + } + + .dialogCustomInner ul { + padding-left: 0px + } + + .dialogCustomInner li { + margin-bottom: 10px; + } + + .marginLoader { + margin-right: 8px; + } + + .last-message-ref { + position: absolute; + font-size: 18px; + top: -40px; + right: 30px; + width: 50; + height: 50; + z-index: 5; + color: black; + background-color: white; + border-radius: 50%; + transition: all 0.1s ease-in-out; + } + + .last-message-ref:hover { + cursor: pointer; + transform: scale(1.1); + } + + .arrow-down-icon { + transform: scale(1.15); + } + + .chat-container { + display: grid; + max-height: 100%; + } + + .chat-text-area { + display: flex; + position: relative; + justify-content: center; + min-height: 60px; + max-height: 100%; + } + + .chat-text-area .typing-area { + display: flex; + flex-direction: column; + width: 98%; + box-sizing: border-box; + margin-bottom: 8px; + border: 1px solid var(--chat-bubble-bg); + border-radius: 10px; + background: var(--chat-bubble-bg); + } + + .chat-text-area .typing-area textarea { + display: none; + } + + .chat-text-area .typing-area .chat-editor { + display: flex; + max-height: -webkit-fill-available; + width: 100%; + border-color: transparent; + margin: 0; + padding: 0; + border: none; + } + + .repliedTo-container { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 10px 10px 8px 10px; + } + + .repliedTo-subcontainer { + display: flex; + flex-direction: row; + align-items: center; + gap: 15px; + width: 100%; + } + + .repliedTo-message { + display: flex; + flex-direction: column; + gap: 5px; + width: 100%; + word-break: break-all; + text-overflow: ellipsis; + overflow: hidden; + max-height: 60px; + } + .repliedTo-message p { + margin: 0px; + padding: 0px; + } + + .repliedTo-message pre { + white-space: pre-wrap; + } + + .repliedTo-message p mark { + background-color: #ffe066; + border-radius: 0.25em; + box-decoration-break: clone; + padding: 0.125em 0; + } + + .reply-icon { + width: 20px; + color: var(--mdc-theme-primary); + } + + .close-icon { + color: #676b71; + width: 18px; + transition: all 0.1s ease-in-out; + } + + .close-icon:hover { + cursor: pointer; + color: #494c50; + } + + .chatbar-container { + width: 100%; + display: flex; + height: auto; + overflow: hidden; + } + + .lds-grid { + width: 120px; + height: 120px; + position: absolute; + left: 50%; + top: 40%; + } + + .lds-grid div { + position: absolute; + width: 34px; + height: 34px; + border-radius: 50%; + background: #03a9f4; + animation: lds-grid 1.2s linear infinite; + } + + .lds-grid div:nth-child(1) { + top: 4px; + left: 4px; + animation-delay: 0s; + } + + .lds-grid div:nth-child(2) { + top: 4px; + left: 48px; + animation-delay: -0.4s; + } + + .lds-grid div:nth-child(3) { + top: 4px; + left: 90px; + animation-delay: -0.8s; + } + + .lds-grid div:nth-child(4) { + top: 50px; + left: 4px; + animation-delay: -0.4s; + } + + .lds-grid div:nth-child(5) { + top: 50px; + left: 48px; + animation-delay: -0.8s; + } + + .lds-grid div:nth-child(6) { + top: 50px; + left: 90px; + animation-delay: -1.2s; + } + + .lds-grid div:nth-child(7) { + top: 95px; + left: 4px; + animation-delay: -0.8s; + } + + .lds-grid div:nth-child(8) { + top: 95px; + left: 48px; + animation-delay: -1.2s; + } + + .lds-grid div:nth-child(9) { + top: 95px; + left: 90px; + animation-delay: -1.6s; + } + + @keyframes lds-grid { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + + .float-left { + float: left; + } + + img { + border-radius: 25%; + } + + paper-dialog.warning { width: 50%; max-width: 50vw; height: 30%; @@ -120,15 +506,338 @@ class ChatPage extends LitElement { .buttons { text-align:right; } - ` + + .dialogCustom { + position: fixed; + z-index: 10000; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + top: 10px; + right: 20px; + user-select: none; + } + + .dialogCustom p { + color: var(--black) + } + + .dialogCustomInner { + min-width: 300px; + height: 40px; + background-color: var(--white); + box-shadow: rgb(119 119 119 / 32%) 0px 4px 12px; + padding: 10px; + border-radius: 4px; + } + + .dialogCustomInner ul { + padding-left: 0px + } + + .dialogCustomInner li { + margin-bottom: 10px; + } + + .marginLoader { + margin-right: 8px; + } + + .smallLoading, + .smallLoading:after { + border-radius: 50%; + width: 2px; + height: 2px; + } + + .smallLoading { + border-width: 0.8em; + border-style: solid; + border-color: rgba(3, 169, 244, 0.2) rgba(3, 169, 244, 0.2) + rgba(3, 169, 244, 0.2) rgb(3, 169, 244); + font-size: 10px; + position: relative; + text-indent: -9999em; + transform: translateZ(0px); + animation: 1.1s linear 0s infinite normal none running loadingAnimation; + } + + @-webkit-keyframes loadingAnimation { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } + + @keyframes loadingAnimation { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } + + /* Add Image Modal Dialog Styling */ + + .dialog-container { + position: relative; + display: flex; + align-items: center; + flex-direction: column; + padding: 0 10px; + gap: 10px; + height: 100%; + } + + .dialog-container-title { + font-family: Montserrat; + color: var(--black); + font-size: 20px; + margin: 15px 0 0 0; + } + + .divider { + height: 1px; + background-color: var(--chat-bubble-msg-color); + user-select: none; + width: 70%; + margin-bottom: 20px; + } + + .dialog-container-loader { + position: relative; + display: flex; + align-items: center; + padding: 0 10px; + gap: 10px; + height: 100%; + } + + .dialog-image { + width: 100%; + max-height: 300px; + border-radius: 0; + object-fit: contain; + } + + .chat-right-panel { + flex: 0; + border-left: 3px solid rgb(221, 221, 221); + height: 100%; + overflow-y: auto; + background: transparent; } + .movedin { + flex: 1 !important; + background: transparent; + } + + .main-container { + display: flex; + height: 100%; + } + + .group-nav-container { + display: flex; + height: 40px; + padding: 25px 5px 25px 20px; + margin: 0px; + background-color: var(--chat-bubble-bg); + box-sizing: border-box; + align-items: center; + justify-content: space-between; + box-shadow: var(--group-drop-shadow); + } + + .top-bar-icon { + border-radius: 50%; + color: var(--chat-bubble-msg-color); + transition: 0.3s all ease-in-out; + padding: 5px; + background-color: transparent; + } + + .top-bar-icon:hover { + background-color: #e6e6e69b; + cursor: pointer; + color: var(--black) + } + + .group-name { + font-family: Raleway, sans-serif; + font-size: 16px; + color: var(--black); + margin:0px; + padding:0px; + } + + .modal-button-row { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + } + + .modal-button { + font-family: Roboto, sans-serif; + font-size: 16px; + color: var(--mdc-theme-primary); + background-color: transparent; + padding: 8px 10px; + border-radius: 5px; + border: none; + transition: all 0.3s ease-in-out; + } + + .modal-button-red { + font-family: Roboto, sans-serif; + font-size: 16px; + color: #F44336; + background-color: transparent; + padding: 8px 10px; + border-radius: 5px; + border: none; + transition: all 0.3s ease-in-out; + } + + .modal-button-red:hover { + cursor: pointer; + background-color: #f4433663; + } + + .modal-button:hover { + cursor: pointer; + background-color: #03a8f475; + } + + .name-input { + width: 100%; + margin-bottom: 15px; + outline: 0; + border-width: 0 0 2px; + border-color: var(--mdc-theme-primary); + background-color: transparent; + padding: 10px; + font-family: Roboto, sans-serif; + font-size: 15px; + color: var(--chat-bubble-msg-color); + box-sizing: border-box; + } + + .name-input::selection { + background-color: var(--mdc-theme-primary); + color: white; + } + + .name-input::placeholder { + opacity: 0.9; + color: var(--black); + } + + .search-results-div { + position: absolute; + top: 25px; + right: 25px; + } + + .search-field { + width: 100%; + position: relative; + margin-bottom: 5px; + } + + .search-icon { + position: absolute; + right: 3px; + top: 0; + color: var(--chat-bubble-msg-color); + transition: all 0.3s ease-in-out; + background: none; + border-radius: 50%; + padding: 6px 3px; + font-size: 21px; + } + + .search-icon:hover { + cursor: pointer; + background: #d7d7d75c; + } + + .user-verified { + position: absolute; + top: 0; + right: 5px; + display: flex; + align-items: center; + gap: 10px; + color: #04aa2e; + font-size: 13px; + } + + .user-selected { + display: flex; + justify-content: space-between; + align-items: center; + margin: 0; + box-shadow: rgb(0 0 0 / 16%) 0px 3px 6px, rgb(0 0 0 / 23%) 0px 3px 6px; + padding: 18px 20px; + color: var(--chat-bubble-msg-color); + border-radius: 5px; + background-color: #ececec96; + } + + .user-selected-name { + font-family: Roboto, sans-serif; + margin: 0; + font-size: 16px; + } + + .forwarding-container { + display: flex; + gap: 15px; + } + + .user-selected-forwarding { + font-family: Livvic, sans-serif; + margin: 0; + font-size: 16px; + } + + .close-forwarding { + color: #676b71; + width: 14px; + transition: all 0.1s ease-in-out; + } + + .close-forwarding:hover { + cursor: pointer; + color: #4e5054; + } +` +} + constructor() { super() this.getOldMessage = this.getOldMessage.bind(this) this._sendMessage = this._sendMessage.bind(this) + this.insertImage = this.insertImage.bind(this) + this.toggleEnableChatEnter = this.toggleEnableChatEnter.bind(this) this._downObserverhandler = this._downObserverhandler.bind(this) + this.setOpenTipUser = this.setOpenTipUser.bind(this) + this.setOpenUserInfo = this.setOpenUserInfo.bind(this) + this.setUserName = this.setUserName.bind(this) + this.setSelectedHead = this.setSelectedHead.bind(this) this.selectedAddress = {} + this.userName = "" this.chatId = '' this.myAddress = '' this.messages = [] @@ -144,59 +853,16 @@ class ChatPage extends LitElement { this.isLoading = false this.isUserDown = false this.isPasteMenuOpen = false - this.chatEditorPlaceholder = this.renderPlaceholder() + this.chatEditorPlaceholder = "" this.messagesRendered = [] - this.myTrimmedMeassage = '' - } - - render() { - return html` - ${this.isLoadingMessages ? html`

    ${translate("chatpage.cchange22")}

    ` : this.renderChatScroller(this._initialMessages)} -
    -
    - - - -
    -
    - - -

    ${translate("chatpage.cchange25")}

    -
    -
    -

    ${translate("chatpage.cchange26")}

    -
    - this.sendMessage(this.myTrimmedMeassage)} dialog-confirm>${translate("transpage.tchange3")} -
    -
    - ` - } - - firstUpdated() { - // TODO: Load and fetch messages from localstorage (maybe save messages to localstorage...) - - // this.changeLanguage(); - this.emojiPickerHandler = this.shadowRoot.querySelector('.emoji-button'); - this.mirrorChatInput = this.shadowRoot.getElementById('messageBox'); - this.chatMessageInput = this.shadowRoot.getElementById('_chatEditorDOM'); - - document.addEventListener('keydown', (e) => { - if (!this.chatEditor.content.body.matches(':focus')) { - // WARNING: Deprecated methods from KeyBoard Event - if (e.code === "Space" || e.keyCode === 32 || e.which === 32) { - this.chatEditor.insertText(' '); - } else if (inputKeyCodes.includes(e.keyCode)) { - this.chatEditor.insertText(e.key); - return this.chatEditor.focus(); - } else { - return this.chatEditor.focus(); - } - } - }); - - // Init EmojiPicker + this.repliedToMessageObj = null + this.editedMessageObj = null + this.iframeHeight = 42 + this.imageFile = null + this.uid = new ShortUniqueId() + this.userLanguage = "" + this.lastMessageRefVisible = false + this.isEditMessageOpen = false this.emojiPicker = new EmojiPicker({ style: "twemoji", twemojiBaseUrl: '/emoji/', @@ -206,20 +872,703 @@ class ChatPage extends LitElement { position: 'top-start', boxShadow: 'rgba(4, 4, 5, 0.15) 0px 0px 0px 1px, rgba(0, 0, 0, 0.24) 0px 8px 16px 0px' }); + this.openForwardOpen = false + this.groupAdmin = [] + this.groupMembers = [] + this.shifted = false + this.groupInfo = {} + this.pageNumber = 1 + this.userFoundModalOpen = false + this.userFound = [] + this.forwardActiveChatHeadUrl = { + url: "", + name: "", + selected: false + } + this.webWorker = null; + this.webWorkerImage = null; + this.currentEditor = '_chatEditorDOM' + this.initialChat = this.initialChat.bind(this) + this.isEnabledChatEnter = true + } - this.emojiPicker.on('emoji', selection => { - const emojiHtmlString = `${selection.emoji}`; - this.chatEditor.insertEmoji(emojiHtmlString); - }); + _toggle(value) { + this.shifted = value === (false || true) ? value : !this.shifted; + this.requestUpdate() + } - // Attach Event Handler - this.emojiPickerHandler.addEventListener('click', () => this.emojiPicker.togglePicker(this.emojiPickerHandler)); + setOpenTipUser(props) { + this.openTipUser = props; + } - window.addEventListener('storage', () => { - const checkLanguage = localStorage.getItem('qortalLanguage') - use(checkLanguage) - }) + setOpenUserInfo(props) { + this.openUserInfo = props; + } + setUserName(props) { + this.userName = props.senderName ? props.senderName : props.sender; + this.setSelectedHead(props); + } + + setSelectedHead(props) { + this.selectedHead = { + ...this.selectedHead, + address: props.sender, + name: props.senderName, + }; + } + + toggleEnableChatEnter(){ + localStorage.setItem('isEnabledChatEnter', !this.isEnabledChatEnter ) + this.isEnabledChatEnter = !this.isEnabledChatEnter + } + + render() { + return html` +
    +
    + ${(!this.isReceipient && +this._chatId !== 0) ? + html` +
    +
    +

    ${this.groupInfo && this.groupInfo.groupName}

    +
    +
    + +
    +
    + ` : null} + +
    + ${this.isLoadingMessages ? + html` +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + ` : + this.renderChatScroller()} +
    +
    + +
    + { + this.shadowRoot.querySelector("chat-scroller").shadowRoot.getElementById("downObserver") + .scrollIntoView({ + behavior: 'smooth', + }); + }}> + +
    +
    + ${this.repliedToMessageObj && html` +
    +
    + +
    +

    ${this.repliedToMessageObj.senderName ? this.repliedToMessageObj.senderName : this.repliedToMessageObj.sender}

    + ${this.repliedToMessageObj.version.toString() === '1' ? html` + ${this.repliedToMessageObj.message} + ` : ''} + ${this.repliedToMessageObj.version.toString() === '2' + ? html` + ${unsafeHTML(generateHTML(this.repliedToMessageObj.message, + [ + StarterKit, + Underline, + Highlight + // other extensions … + ]))} + ` + : ''} +
    + this.closeRepliedToContainer()} + > +
    +
    + `} + ${this.editedMessageObj && html` +
    +
    + +
    +

    ${translate("chatpage.cchange25")}

    + ${unsafeHTML(generateHTML(this.editedMessageObj.message, + [ + StarterKit, + Underline, + Highlight + // other extensions … + ]))} +
    + this.closeEditMessageContainer()} + > +
    +
    + `} +
    + this.updatePlaceholder(editor, value)} + id="_chatEditorDOM" + .repliedToMessageObj=${this.repliedToMessageObj} + .toggleEnableChatEnter=${this.toggleEnableChatEnter} + ?isEnabledChatEnter=${this.isEnabledChatEnter} + > + +
    +
    +
    + + ${(this.isUploadingImage || this.isDeletingImage) ? html` +
    +
    +
    +
    +

    + ${this.isDeletingImage ? + translate("chatpage.cchange31") : translate("chatpage.cchange30")} +

    +
    +
    +
    +
    + `: ''} + { + this.removeImage(); + }} + style=${(this.imageFile && !this.isUploadingImage) ? "visibility:visible;z-index:50" : "visibility: hidden;z-index:-100"}> +
    +
    + ${this.imageFile && html` + dialog-img + `} +
    + this.updatePlaceholder(editor, value)} + > + +
    + +
    +
    +
    + +

    ${translate("chatpage.cchange41")}

    +
    +
    +

    ${translate("chatpage.cchange42")}

    +
    + this.sendMessage(this.myTrimmedMeassage)} dialog-confirm>${translate("transpage.tchange3")} +
    +
    + { + this.openForwardOpen = false; + this.forwardActiveChatHeadUrl = {}; + this.requestUpdate(); + } } + style=${this.openForwardOpen ? "display: block" : "display: none"}> +
    +
    +
    +

    ${translate("blockpage.bcchange16")}

    +
    +
    +
    +
    + { + if (this.forwardActiveChatHeadUrl.selected) { + this.forwardActiveChatHeadUrl = {}; + this.requestUpdate(); + } + } + } + /> + ${this.forwardActiveChatHeadUrl.selected ? ( + html` +
    +

    ${translate("chatpage.cchange38")}

    + +
    + ` + ) : ( + html` + + + ` + )} +
    + ${this.forwardActiveChatHeadUrl.selected ? ( + html` +
    +

    + ${this.forwardActiveChatHeadUrl.name} +

    +
    +

    + Forwarding... +

    + { + this.userFound = []; + this.forwardActiveChatHeadUrl = {}; + this.requestUpdate(); + this.shadowRoot.getElementById("sendTo").value = ""; + }}> + +
    +
    + ` + ) : ( + html` + ${this.chatHeads.map((item) => { + return html` + { + this.forwardActiveChatHeadUrl = { + ...this.forwardActiveChatHeadUrl, + url: val + }; + this.userFound = []; + }} + chatInfo=${JSON.stringify(item)}> + ` + })} + ` + )} +
    + + +
    +
    + { + this.forwardActiveChatHeadUrl = { + ...this.forwardActiveChatHeadUrl, + url: `direct/${result.owner}`, + name: result.name, + selected: true + }; + this.userFound = []; + this.userFoundModalOpen = false; + }} + .closeFunc=${() => { + this.userFoundModalOpen = false; + this.userFound = []; + }} + .searchResults=${this.userFound} + ?isOpen=${this.userFoundModalOpen} + ?loading=${this.isLoading}> + +
    +
    +
    + { + this.setOpenTipUser(false); + }} + zIndex=${55} + style=${this.openTipUser ? "display: block;" : "display: none;"}> + this.setOpenTipUser(val)}> + + + { + this.setOpenUserInfo(false); + this.setUserName(""); + this.setSelectedHead({}); + }} + style=${ + this.openUserInfo ? "display: block" : "display: none" + }> + this.setOpenUserInfo(val)} + .setOpenTipUser=${(val) => this.setOpenTipUser(val)} + .setOpenPrivateMessage=${(val) => this.setOpenPrivateMessage(val)} + .userName=${this.userName} + .selectedHead=${this.selectedHead} + > + +
    +
    + this.getMoreMembers(val)} + .toggle=${(val)=> this._toggle(val)} + .selectedAddress=${this.selectedAddress} + .groupMembers=${this.groupMembers} + .groupAdmin=${this.groupAdmin} + .leaveGroupObj=${this.groupInfo} + .setOpenPrivateMessage=${(val) => this.setOpenPrivateMessage(val)} + .setOpenTipUser=${(val) => this.setOpenTipUser(val)} + .setOpenUserInfo=${(val) => this.setOpenUserInfo(val)} + .setUserName=${(val) => this.setUserName(val)} + > + +
    + + ` + } + + async getMoreMembers(groupId){ + try { + const getMembers = await parentEpml.request("apiCall", { + type: "api", + url: `/groups/members/${groupId}?onlyAdmins=false&limit=20&offset=${this.pageNumber * 20}`, + }); + + const getMembersWithName = (getMembers.members || []).map(async (member) => { + let memberItem = member + try { + const name = await this.getName(member.member) + memberItem = { + address: member.member, + name: name ? name : undefined + } + } catch (error) { + console.log(error) + } + + return memberItem + }) + const membersWithName = await Promise.all(getMembersWithName) + this.groupMembers = membersWithName + this.pageNumber = this.pageNumber + 1 + } catch (error) { + console.error(error) + } + } + + + + async connectedCallback() { + super.connectedCallback(); + this.webWorker = new WebWorker(); + this.webWorkerImage = new WebWorkerImage(); + await this.getUpdateCompleteTextEditor(); + + const elementChatId = this.shadowRoot.getElementById('_chatEditorDOM').shadowRoot.getElementById('_chatEditorDOM') + const elementChatImageId = this.shadowRoot.getElementById('chatTextCaption').shadowRoot.getElementById('newChat') + this.editor = new Editor({ + onUpdate: ()=> { + this.shadowRoot.getElementById('_chatEditorDOM').getMessageSize(this.editor.getJSON()) + }, + + element: elementChatId, + extensions: [ + StarterKit, + Underline, + Highlight, + Placeholder.configure({ + placeholder: 'Write something …', + }), + Extension.create({ + name: 'shortcuts', + addKeyboardShortcuts:()=> { + return { + 'Enter': ()=> { + if(this.isEnabledChatEnter){ + const chatTextEditor = this.shadowRoot.getElementById('_chatEditorDOM') + chatTextEditor.sendMessageFunc({ + }) + return true + } + + }, + "Shift-Enter": () => { + if(this.isEnabledChatEnter){ + this.editor.commands.first(() => [ + this.editor.commands.newlineInCode() + ]) + } + + } + + + } + }}) + ] + }) + + this.editorImage = new Editor({ + onUpdate: ()=> { + this.shadowRoot.getElementById('chatTextCaption').getMessageSize(this.editorImage.getJSON()) + }, + element: elementChatImageId, + extensions: [ + StarterKit, + Underline, + Highlight, + Placeholder.configure({ + placeholder: 'Write something …', + }), + Extension.create({ + addKeyboardShortcuts:()=> { + return { + 'Enter':()=> { + const chatTextEditor = this.shadowRoot.getElementById('chatTextCaption') + chatTextEditor.sendMessageFunc({ + type: 'image', + imageFile: this.imageFile, + }) + return true + } + } + }}) + ] + }) + document.addEventListener('keydown', this.initialChat); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.webWorker.terminate(); + this.webWorkerImage.terminate(); + this.editor.destroy() + this.editorImage.destroy() + document.removeEventListener('keydown', this.initialChat); + } + + initialChat(e) { + if (this.editor && !this.editor.isFocused && this.currentEditor === '_chatEditorDOM' && !this.openForwardOpen && !this.openTipUser) { + // WARNING: Deprecated methods from KeyBoard Event + if (e.code === "Space" || e.keyCode === 32 || e.which === 32) { + } else if (inputKeyCodes.includes(e.keyCode)) { + this.editor.commands.insertContent(e.key) + this.editor.commands.focus('end') + } else { + this.editor.commands.focus('end') + } + } + + + } + + async goToRepliedMessage(message){ + const findMessage = this.shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById(message.reference) + if(findMessage){ + findMessage.scrollIntoView({ behavior: 'smooth', block: 'center' }) + const findElement = findMessage.shadowRoot.querySelector('.message-parent') + if(findElement){ + findElement.classList.add('blink-bg') + setTimeout(()=> { + findElement.classList.remove('blink-bg') + }, 2000) + } + + return + } + if((message.timestamp - this.messagesRendered[0].timestamp) > 86400000){ + let errorMsg = get("chatpage.cchange66") + parentEpml.request('showSnackBar', `${errorMsg}`) + return + } + + if((message.timestamp - this.messagesRendered[0].timestamp) < 86400000){ + await this.getOldMessageDynamic(0, this.messagesRendered[0].timestamp, message.timestamp - 7200000) + const findMessage = this.shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById(message.reference) + if(findMessage){ + findMessage.scrollIntoView({ behavior: 'smooth', block: 'center' }) + const findElement = findMessage.shadowRoot.querySelector('.message-parent') + if(findElement){ + findElement.classList.add('blink-bg') + setTimeout(()=> { + findElement.classList.remove('blink-bg') + }, 2000) + } + + return + } + + let errorMsg = get("chatpage.cchange66") + parentEpml.request('showSnackBar', `${errorMsg}`) + + } + + } + + async userSearch() { + const nameValue = this.shadowRoot.getElementById('sendTo').value; + if (!nameValue) { + this.userFound = []; + this.userFoundModalOpen = false; + this.loading = false; + return; + } + try { + const result = await parentEpml.request('apiCall', { + type: 'api', + url: `/names/${nameValue}` + }) + if (result.error === 401) { + this.loading = false; + this.userFound = []; + this.loading = false; + } else { + this.userFound = [ + ...this.userFound, + result, + ]; + } + this.userFoundModalOpen = true; + } catch (error) { + this.loading = false; + console.error(error); + let err4string = get("chatpage.cchange35"); + parentEpml.request('showSnackBar', `${err4string}`) + } + } + + setForwardProperties(forwardedMessage){ + this.openForwardOpen = true + this.forwardedMessage = forwardedMessage + } + + async sendForwardMessage() { + let parsedMessageObj = {}; + try { + parsedMessageObj = JSON.parse(this.forwardedMessage); + } + catch (error) { + parsedMessageObj = {}; + } + + try { + const message = { + ...parsedMessageObj, + type: 'forward' + } + delete message.reactions + const stringifyMessageObject = JSON.stringify(message); + this.sendMessage(stringifyMessageObject, undefined, '', true) + } catch (error) { + console.log({error}); + } + } + + showLastMessageRefScroller(props) { + this.lastMessageRefVisible = props; + } + + + insertImage(file) { + if (file.type.includes('image')) { + this.imageFile = file; + this.currentEditor = 'newChat' + return; + } + parentEpml.request('showSnackBar', get("chatpage.cchange28")); + } + + removeImage() { + this.imageFile = null; + this.resetChatEditor() + this.currentEditor = '_chatEditorDOM' + } + + changeMsgInput(id) { + this.chatMessageInput = this.shadowRoot.getElementById(id); + this.initChatEditor(); + } + + async initUpdate(){ + if(this.webSocket){ + this.webSocket.close() + this.webSocket= '' + } + this.pageNumber = 1 const getAddressPublicKey = () => { parentEpml.request('apiCall', { @@ -245,21 +1594,88 @@ class ChatPage extends LitElement { } }) }; + setTimeout(() => { + const isRecipient = this.chatId.includes('direct') === true ? true : false; this.chatId.includes('direct') === true ? this.isReceipient = true : this.isReceipient = false; this._chatId = this.chatId.split('/')[1]; - - const mstring = get("chatpage.cchange8") - const placeholder = this.isReceipient === true ? `Message ${this._chatId}` : `${mstring}`; + const mstring = get("chatpage.cchange8"); + const placeholder = isRecipient === true ? `Message ${this._chatId}` : `${mstring}`; this.chatEditorPlaceholder = placeholder; - this.isReceipient ? getAddressPublicKey() : this.fetchChatMessages(this._chatId); - + isRecipient ? getAddressPublicKey() : this.fetchChatMessages(this._chatId); + // Init ChatEditor - this.initChatEditor(); + // this.initChatEditor(); }, 100) + + const isRecipient = this.chatId.includes('direct') === true ? true : false; + const groupId = this.chatId.split('/')[1]; + if(!isRecipient && groupId !== 0){ + + try { + const getMembers = await parentEpml.request("apiCall", { + type: "api", + url: `/groups/members/${groupId}?onlyAdmins=false&limit=20&offset=0`, + }); + const getMembersAdmins = await parentEpml.request("apiCall", { + type: "api", + url: `/groups/members/${groupId}?onlyAdmins=true&limit=20`, + }); + const getGroupInfo = await parentEpml.request("apiCall", { + type: "api", + url: `/groups/${groupId}`, + }); + const getMembersAdminsWithName = (getMembersAdmins.members || []).map(async (member) => { + let memberItem = member + try { + const name = await this.getName(member.member) + memberItem = { + address: member.member, + name: name ? name : undefined + } + } catch (error) { + console.log(error) + } + + return memberItem + }) + const membersAdminsWithName = await Promise.all(getMembersAdminsWithName) + const getMembersWithName = (getMembers.members || []).map(async (member) => { + let memberItem = member + try { + const name = await this.getName(member.member) + memberItem = { + address: member.member, + name: name ? name : undefined + } + } catch (error) { + console.log(error) + } + + return memberItem + }) + const membersWithName = await Promise.all(getMembersWithName) + this.groupAdmin = membersAdminsWithName + this.groupMembers = membersWithName + this.groupInfo = getGroupInfo + } catch (error) { + console.error(error) + } + } + + + } + + async firstUpdated() { + window.addEventListener('storage', () => { + const checkLanguage = localStorage.getItem('qortalLanguage'); + use(checkLanguage); + this.userLanguage = checkLanguage; + }) + parentEpml.ready().then(() => { parentEpml.subscribe('selected_address', async selectedAddress => { this.selectedAddress = {} @@ -272,40 +1688,120 @@ class ChatPage extends LitElement { }).then(res => { this.balance = res }) - parentEpml.subscribe('frame_paste_menu_switch', async res => { - - res = JSON.parse(res) - if (res.isOpen === false && this.isPasteMenuOpen === true) { - - this.pasteToTextBox(textarea) - this.isPasteMenuOpen = false - } - }) + }) parentEpml.imReady(); + + const isEnabledChatEnter = localStorage.getItem('isEnabledChatEnter') + + if(isEnabledChatEnter){ + this.isEnabledChatEnter = isEnabledChatEnter === 'false' ? false : true + } + await this.initUpdate() } - changeLanguage() { - const checkLanguage = localStorage.getItem('qortalLanguage') + async updated(changedProperties) { + if (changedProperties && changedProperties.has('userLanguage')) { + const userLang = changedProperties.get('userLanguage') + if (userLang) { + await new Promise(r => setTimeout(r, 100)); + this.chatEditorPlaceholder = this.isReceipient === true ? `Message ${this._chatId}` : `${get("chatpage.cchange8")}`; + } + } - if (checkLanguage === null || checkLanguage.length === 0) { - localStorage.setItem('qortalLanguage', 'us') - use('us') - } else { - use(checkLanguage) + if (changedProperties && changedProperties.has('chatId') && changedProperties.get('chatId')) { + await this.initUpdate() + } + + + if (changedProperties && changedProperties.has('isLoading')) { + if (this.isLoading === true && this.currentEditor === '_chatEditorDOM') { + this.editor.setEditable(false) + } + if (this.isLoading === false && this.currentEditor === '_chatEditorDOM') { + this.editor.setEditable(true) + } + } + + } + + async getName (recipient) { + try { + const getNames = await parentEpml.request("apiCall", { + type: "api", + url: `/names/address/${recipient}`, + }); + + if (Array.isArray(getNames) && getNames.length > 0 ) { + return getNames[0].name + } else { + return '' + } + + } catch (error) { + return "" } } - renderPlaceholder() { - const mstring = get("chatpage.cchange8") - const placeholder = this.isReceipient === true ? `Message ${this._chatId}` : `${mstring}`; - this.chatEditorPlaceholder = placeholder; + async renderPlaceholder() { + const getName = async (recipient)=> { + try { + const getNames = await parentEpml.request("apiCall", { + type: "api", + url: `/names/address/${recipient}`, + }); + + if (Array.isArray(getNames) && getNames.length > 0 ) { + return getNames[0].name + } else { + return '' + } + + } catch (error) { + return "" + } + } + let userName = "" + if(this.isReceipient){ + userName = await getName(this._chatId); + } + const mstring = get("chatpage.cchange8"); + const placeholder = this.isReceipient === true ? `Message ${userName ? userName : this._chatId}` : `${mstring}`; + return placeholder; } - renderChatScroller(initialMessages) { - return html` ` + renderChatScroller() { + return html` + this.setRepliedToMessageObj(val)} + .setEditedMessageObj=${(val) => this.setEditedMessageObj(val)} + .sendMessage=${(val) => this._sendMessage(val)} + .sendMessageForward=${(messageText, typeMessage, chatReference, isForward, forwardParams)=> this.sendMessage(messageText, typeMessage, chatReference, isForward, forwardParams)} + .showLastMessageRefScroller=${(val) => this.showLastMessageRefScroller(val)} + .emojiPicker=${this.emojiPicker} + ?isLoadingMessages=${this.isLoadingOldMessages} + .setIsLoadingMessages=${(val) => this.setIsLoadingMessages(val)} + .setForwardProperties=${(forwardedMessage)=> this.setForwardProperties(forwardedMessage)} + .setOpenPrivateMessage=${(val) => this.setOpenPrivateMessage(val)} + .setOpenTipUser=${(val) => this.setOpenTipUser(val)} + .setOpenUserInfo=${(val) => this.setOpenUserInfo(val)} + .setUserName=${(val) => this.setUserName(val)} + .setSelectedHead=${(val) => this.setSelectedHead(val)} + ?openTipUser=${this.openTipUser} + .selectedHead=${this.selectedHead} + .goToRepliedMessage=${(val)=> this.goToRepliedMessage(val)} + .getOldMessageAfter=${(val)=> this.getOldMessageAfter(val)} + > + + ` + } + setIsLoadingMessages(val){ + this.isLoadingOldMessages = val } - async getUpdateComplete() { await super.getUpdateComplete(); const marginElements = Array.from(this.shadowRoot.querySelectorAll('chat-scroller')); @@ -313,33 +1809,245 @@ class ChatPage extends LitElement { return true; } - async getOldMessage(scrollElement) { + async getUpdateCompleteTextEditor() { + await super.getUpdateComplete(); + const marginElements = Array.from(this.shadowRoot.querySelectorAll('chat-text-editor')); + await Promise.all(marginElements.map(el => el.updateComplete)); + const marginElements2 = Array.from(this.shadowRoot.querySelectorAll('wrapper-modal')); + await Promise.all(marginElements2.map(el => el.updateComplete)); + return true; + } - if (this._messages.length <= 15 && this._messages.length >= 1) { // 15 is the default number of messages... + updatePlaceholder(editor, text){ + editor.extensionManager.extensions.forEach((extension) => { + if (extension.name === "placeholder") { + + extension.options["placeholder"] = text + editor.commands.focus('end') + } + }) + } - let __msg = [...this._messages] - this._messages = [] - this.messagesRendered = [...__msg, ...this.messagesRendered] + async getOldMessageDynamic(limit, before, after) { + + if (this.isReceipient) { + const getInitialMessages = await parentEpml.request('apiCall', { + type: 'api', + url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${this._chatId}&limit=${limit}&reverse=true&before=${before}&after=${after}&haschatreference=false`, + }); + + const decodeMsgs = getInitialMessages.map((eachMessage) => { + return this.decodeMessage(eachMessage) + }) + + + const replacedMessages = await replaceMessagesEdited({ + decodedMessages: decodeMsgs, + parentEpml, + isReceipient: this.isReceipient, + decodeMessageFunc: this.decodeMessage, + _publicKey: this._publicKey + }) + this.messagesRendered = [...replacedMessages, ...this.messagesRendered].sort(function (a, b) { + return a.timestamp + - b.timestamp + }) + this.isLoadingOldMessages = false await this.getUpdateComplete(); + const viewElement = this.shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById('viewElement'); - scrollElement.scrollIntoView({ behavior: 'auto', block: 'center' }); - return { oldMessages: __msg, scrollElement: scrollElement } - } else if (this._messages.length > 15) { - this.messagesRendered = [...this._messages.splice(this._messages.length - 15), ...this.messagesRendered] - await this.getUpdateComplete(); + if(viewElement){ + viewElement.scrollTop = 200 + } + - scrollElement.scrollIntoView({ behavior: 'auto', block: 'center' }); - return { oldMessages: this._messages.splice(this._messages.length - 15), scrollElement: scrollElement } + + + } else { + const getInitialMessages = await parentEpml.request('apiCall', { + type: 'api', + url: `/chat/messages?txGroupId=${Number(this._chatId)}&limit=${limit}&reverse=true&before=${before}&after=${after}&haschatreference=false`, + }); + + + const decodeMsgs = getInitialMessages.map((eachMessage) => { + return this.decodeMessage(eachMessage) + }) + + const replacedMessages = await replaceMessagesEdited({ + decodedMessages: decodeMsgs, + parentEpml, + isReceipient: this.isReceipient, + decodeMessageFunc: this.decodeMessage, + _publicKey: this._publicKey + }) + + this.messagesRendered = [...replacedMessages, ...this.messagesRendered].sort(function (a, b) { + return a.timestamp + - b.timestamp + }) + this.isLoadingOldMessages = false + await this.getUpdateComplete(); + const viewElement = this.shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById('viewElement'); + + if(viewElement){ + viewElement.scrollTop = 200 + } + + + + - return false } } - async processMessages(messages, isInitial) { - if (isInitial) { - this.messages = messages.map((eachMessage) => { + async getOldMessage(scrollElement) { + + if (this.isReceipient) { + const getInitialMessages = await parentEpml.request('apiCall', { + type: 'api', + url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${this._chatId}&limit=20&reverse=true&before=${scrollElement.messageObj.timestamp}&haschatreference=false`, + }); + + const decodeMsgs = getInitialMessages.map((eachMessage) => { + return this.decodeMessage(eachMessage) + }) + + + const replacedMessages = await replaceMessagesEdited({ + decodedMessages: decodeMsgs, + parentEpml, + isReceipient: this.isReceipient, + decodeMessageFunc: this.decodeMessage, + _publicKey: this._publicKey + }) + this.messagesRendered = [...replacedMessages, ...this.messagesRendered].sort(function (a, b) { + return a.timestamp + - b.timestamp + }) + this.isLoadingOldMessages = false + await this.getUpdateComplete(); + const marginElements = Array.from(this.shadowRoot.querySelector('chat-scroller').shadowRoot.querySelectorAll('message-template')); + + const findElement = marginElements.find((item)=> item.messageObj.reference === scrollElement.messageObj.reference) + + if(findElement){ + findElement.scrollIntoView({ behavior: 'auto', block: 'center' }); + } + + + } else { + const getInitialMessages = await parentEpml.request('apiCall', { + type: 'api', + url: `/chat/messages?txGroupId=${Number(this._chatId)}&limit=20&reverse=true&before=${scrollElement.messageObj.timestamp}&haschatreference=false`, + }); + + + const decodeMsgs = getInitialMessages.map((eachMessage) => { + return this.decodeMessage(eachMessage) + }) + + const replacedMessages = await replaceMessagesEdited({ + decodedMessages: decodeMsgs, + parentEpml, + isReceipient: this.isReceipient, + decodeMessageFunc: this.decodeMessage, + _publicKey: this._publicKey + }) + + this.messagesRendered = [...replacedMessages, ...this.messagesRendered].sort(function (a, b) { + return a.timestamp + - b.timestamp + }) + this.isLoadingOldMessages = false + await this.getUpdateComplete(); + const marginElements = Array.from(this.shadowRoot.querySelector('chat-scroller').shadowRoot.querySelectorAll('message-template')); + const findElement = marginElements.find((item)=> item.messageObj.reference === scrollElement.messageObj.reference) + + if(findElement){ + findElement.scrollIntoView({ behavior: 'auto', block: 'center' }); + } + + + } + } + + async getOldMessageAfter(scrollElement) { + + if (this.isReceipient) { + const getInitialMessages = await parentEpml.request('apiCall', { + type: 'api', + url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${this._chatId}&limit=20&reverse=true&afer=${scrollElement.messageObj.timestamp}&haschatreference=false`, + }); + + const decodeMsgs = getInitialMessages.map((eachMessage) => { + return this.decodeMessage(eachMessage) + }) + + + const replacedMessages = await replaceMessagesEdited({ + decodedMessages: decodeMsgs, + parentEpml, + isReceipient: this.isReceipient, + decodeMessageFunc: this.decodeMessage, + _publicKey: this._publicKey + }) + this.messagesRendered = [...this.messagesRendered, ...replacedMessages].sort(function (a, b) { + return a.timestamp + - b.timestamp + }) + this.isLoadingOldMessages = false + await this.getUpdateComplete(); + const marginElements = Array.from(this.shadowRoot.querySelector('chat-scroller').shadowRoot.querySelectorAll('message-template')); + + const findElement = marginElements.find((item)=> item.messageObj.reference === scrollElement.messageObj.reference) + + if(findElement){ + findElement.scrollIntoView({ behavior: 'auto', block: 'center' }); + } + + + } else { + const getInitialMessages = await parentEpml.request('apiCall', { + type: 'api', + url: `/chat/messages?txGroupId=${Number(this._chatId)}&limit=20&reverse=true&after=${scrollElement.messageObj.timestamp}&haschatreference=false`, + }); + + + const decodeMsgs = getInitialMessages.map((eachMessage) => { + return this.decodeMessage(eachMessage) + }) + + const replacedMessages = await replaceMessagesEdited({ + decodedMessages: decodeMsgs, + parentEpml, + isReceipient: this.isReceipient, + decodeMessageFunc: this.decodeMessage, + _publicKey: this._publicKey + }) + + this.messagesRendered = [ ...this.messagesRendered, ...replacedMessages].sort(function (a, b) { + return a.timestamp + - b.timestamp + }) + this.isLoadingOldMessages = false + await this.getUpdateComplete(); + const marginElements = Array.from(this.shadowRoot.querySelector('chat-scroller').shadowRoot.querySelectorAll('message-template')); + const findElement = marginElements.find((item)=> item.messageObj.reference === scrollElement.messageObj.reference) + + if(findElement){ + findElement.scrollIntoView({ behavior: 'auto', block: 'center' }); + } + + + } + } + + async processMessages(messages, isInitial) { + const isReceipient = this.chatId.includes('direct') + const decodedMessages = messages.map((eachMessage) => { if (eachMessage.isText === true) { this.messageSignature = eachMessage.signature @@ -351,127 +2059,119 @@ class ChatPage extends LitElement { return _eachMessage } }) + + if (isInitial) { + this.chatEditorPlaceholder = await this.renderPlaceholder(); + const replacedMessages = await replaceMessagesEdited({ + decodedMessages: decodedMessages, + parentEpml, + isReceipient: isReceipient, + decodeMessageFunc: this.decodeMessage, + _publicKey: this._publicKey + }) - this._messages = [...this.messages] - - const adjustMessages = () => { - - let __msg = [...this._messages] - this._messages = [] - this._initialMessages = __msg - } + this._messages = replacedMessages.sort(function (a, b) { + return a.timestamp + - b.timestamp + }) // TODO: Determine number of initial messages by screen height... - this._messages.length <= 15 ? adjustMessages() : this._initialMessages = this._messages.splice(this._messages.length - 15); - - this.messagesRendered = this._initialMessages - - this.isLoadingMessages = false - setTimeout(() => this.downElementObserver(), 500) + this.messagesRendered = this._messages; + this.isLoadingMessages = false; + setTimeout(() => this.downElementObserver(), 500); } else { - - let _newMessages = messages.map((eachMessage) => { - if (eachMessage.isText === true) { - let _eachMessage = this.decodeMessage(eachMessage) - - if (this.messageSignature !== eachMessage.signature) { - this.messageSignature = eachMessage.signature - // What are we waiting for, send in the message immediately... - this.renderNewMessage(_eachMessage) - } - return _eachMessage - } else { - let _eachMessage = this.decodeMessage(eachMessage) - - if (this.messageSignature !== eachMessage.signature) { - this.messageSignature = eachMessage.signature - this.renderNewMessage(_eachMessage) - } - return _eachMessage - } + const replacedMessages = await replaceMessagesEdited({ + decodedMessages: decodedMessages, + parentEpml, + isReceipient: isReceipient, + decodeMessageFunc: this.decodeMessage, + _publicKey: this._publicKey }) - this.newMessages = this.newMessages.concat(_newMessages) - + const renderEachMessage = replacedMessages.map(async(msg)=> { + await this.renderNewMessage(msg) + }) + await Promise.all(renderEachMessage) + // this.newMessages = this.newMessages.concat(_newMessages) + this.messagesRendered = [...this.messagesRendered].sort(function (a, b) { + return a.timestamp + - b.timestamp + }) } } + // set replied to message in chat editor + + setRepliedToMessageObj(messageObj) { + this.editor.commands.focus('end') + this.repliedToMessageObj = {...messageObj}; + this.editedMessageObj = null; + this.requestUpdate(); + } + + // set edited message in chat editor + + setEditedMessageObj(messageObj) { + this.editor.commands.focus('end') + this.editedMessageObj = {...messageObj}; + this.repliedToMessageObj = null; + this.requestUpdate(); + } + + closeEditMessageContainer() { + this.editedMessageObj = null; + this.isEditMessageOpen = !this.isEditMessageOpen; + this.editor.commands.setContent('') + } + + closeRepliedToContainer() { + this.repliedToMessageObj = null; + this.requestUpdate(); + } + + + /** * New Message Template implementation, takes in a message object. * @param { Object } messageObj * @property id or index * @property sender and other info.. */ - chatMessageTemplate(messageObj) { - const hidemsg = this.hideMessages - let avatarImg = '' - let nameMenu = '' - let levelFounder = '' - let hideit = hidemsg.includes(messageObj.sender) + async renderNewMessage(newMessage) { + if(newMessage.chatReference){ + const findOriginalMessageIndex = this.messagesRendered.findIndex(msg=> msg.reference === newMessage.chatReference || (msg.chatReference && msg.chatReference === newMessage.chatReference) ) + if(findOriginalMessageIndex !== -1){ + const newMessagesRendered = [...this.messagesRendered] + newMessagesRendered[findOriginalMessageIndex] = {...newMessage, timestamp: newMessagesRendered[findOriginalMessageIndex].timestamp, editedTimestamp: newMessage.timestamp } + this.messagesRendered = newMessagesRendered + await this.getUpdateComplete(); + } - levelFounder = `` - - if (messageObj.senderName) { - const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] - const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port - const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${messageObj.senderName}/qortal_avatar?async=true&apiKey=${myNode.apiKey}` - avatarImg = `` + return } - - if (messageObj.sender === this.myAddress) { - nameMenu = `${messageObj.senderName ? messageObj.senderName : messageObj.sender}` - } else { - nameMenu = `` - } - - if (hideit === true) { - return ` -
  • - ` - } else { - return ` -
  • -
    - ${nameMenu} - ${levelFounder} - -
    -
    ${avatarImg}
    -
    ${this.emojiPicker.parse(escape(messageObj.decodedMessage))}
    -
  • - ` - } - } - - async renderNewMessage(newMessage) { - const viewElement = this.shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById('viewElement'); - const downObserver = this.shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById('downObserver'); - const li = document.createElement('li'); - li.innerHTML = this.chatMessageTemplate(newMessage); - li.id = newMessage.signature; if (newMessage.sender === this.selectedAddress.address) { - this.messagesRendered = [...this.messagesRendered, newMessage] - await this.getUpdateComplete(); + this.messagesRendered = [...this.messagesRendered, newMessage] + await this.getUpdateComplete(); viewElement.scrollTop = viewElement.scrollHeight; } else if (this.isUserDown) { // Append the message and scroll to the bottom if user is down the page - this.messagesRendered = [...this.messagesRendered, newMessage] - await this.getUpdateComplete(); + this.messagesRendered = [...this.messagesRendered, newMessage] + await this.getUpdateComplete(); viewElement.scrollTop = viewElement.scrollHeight; } else { - this.messagesRendered = [...this.messagesRendered, newMessage] - await this.getUpdateComplete(); + this.messagesRendered = [...this.messagesRendered, newMessage] + await this.getUpdateComplete(); - this.showNewMesssageBar(); + this.showNewMessageBar(); } } @@ -480,41 +2180,45 @@ class ChatPage extends LitElement { * @param {Object} encodedMessageObj * */ - decodeMessage(encodedMessageObj) { - let decodedMessageObj = {} + decodeMessage(encodedMessageObj, isReceipient, _publicKey ) { + let isReceipientVar; + let _publicKeyVar; + try { + isReceipientVar = this.isReceipient === undefined ? isReceipient : this.isReceipient; + _publicKeyVar = this._publicKey === undefined ? _publicKey : this._publicKey; + } catch (error) { + isReceipientVar = isReceipient; + _publicKeyVar = _publicKey; + } + + let decodedMessageObj = {}; - if (this.isReceipient === true) { + if (isReceipientVar === true) { // direct chat - - if (encodedMessageObj.isEncrypted === true && this._publicKey.hasPubKey === true) { - - let decodedMessage = window.parent.decryptChatMessage(encodedMessageObj.data, window.parent.reduxStore.getState().app.selectedAddress.keyPair.privateKey, this._publicKey.key, encodedMessageObj.reference) - decodedMessageObj = { ...encodedMessageObj, decodedMessage } - } else if (encodedMessageObj.isEncrypted === false) { - - let bytesArray = window.parent.Base58.decode(encodedMessageObj.data) - let decodedMessage = new TextDecoder('utf-8').decode(bytesArray) - decodedMessageObj = { ...encodedMessageObj, decodedMessage } + if (encodedMessageObj.isEncrypted === true && _publicKeyVar.hasPubKey === true && encodedMessageObj.data) { + let decodedMessage = window.parent.decryptChatMessage(encodedMessageObj.data, window.parent.reduxStore.getState().app.selectedAddress.keyPair.privateKey, _publicKeyVar.key, encodedMessageObj.reference); + decodedMessageObj = { ...encodedMessageObj, decodedMessage }; + } else if (encodedMessageObj.isEncrypted === false && encodedMessageObj.data) { + let bytesArray = window.parent.Base58.decode(encodedMessageObj.data); + let decodedMessage = new TextDecoder('utf-8').decode(bytesArray); + decodedMessageObj = { ...encodedMessageObj, decodedMessage }; } else { - - decodedMessageObj = { ...encodedMessageObj, decodedMessage: "Cannot Decrypt Message!" } + decodedMessageObj = { ...encodedMessageObj, decodedMessage: "Cannot Decrypt Message!" }; } } else { // group chat - - let bytesArray = window.parent.Base58.decode(encodedMessageObj.data) - let decodedMessage = new TextDecoder('utf-8').decode(bytesArray) - decodedMessageObj = { ...encodedMessageObj, decodedMessage } + let bytesArray = window.parent.Base58.decode(encodedMessageObj.data); + let decodedMessage = new TextDecoder('utf-8').decode(bytesArray); + decodedMessageObj = { ...encodedMessageObj, decodedMessage }; } - return decodedMessageObj + return decodedMessageObj; } async fetchChatMessages(chatId) { - const initDirect = (cid) => { - + const initDirect = async (cid) => { let initial = 0 let directSocketTimeout @@ -533,40 +2237,60 @@ class ChatPage extends LitElement { directSocketLink = `ws://${nodeUrl}/websockets/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}`; } - const directSocket = new WebSocket(directSocketLink); + this.webSocket = new WebSocket(directSocketLink); // Open Connection - directSocket.onopen = () => { + this.webSocket.onopen = () => { setTimeout(pingDirectSocket, 50) } // Message Event - directSocket.onmessage = (e) => { - + this.webSocket.onmessage = async (e) => { if (initial === 0) { + + const cachedData = null + let getInitialMessages = [] + if (cachedData && cachedData.length !== 0) { + const lastMessage = cachedData[cachedData.length - 1] + const newMessages = await parentEpml.request('apiCall', { + type: 'api', + url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}&limit=20&reverse=true&after=${lastMessage.timestamp}&haschatreference=false`, + }); + getInitialMessages = [...cachedData, ...newMessages].slice(-20) + } else { + getInitialMessages = await parentEpml.request('apiCall', { + type: 'api', + url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}&limit=20&reverse=true&haschatreference=false`, + }); + + + } + + this.processMessages(getInitialMessages, true) - this.isLoadingMessages = true - this.processMessages(JSON.parse(e.data), true) initial = initial + 1 - } else { - this.processMessages(JSON.parse(e.data), false) + } else { + if(e.data){ + this.processMessages(JSON.parse(e.data), false) + } + } } // Closed Event - directSocket.onclose = () => { + this.webSocket.onclose = () => { clearTimeout(directSocketTimeout) } // Error Event - directSocket.onerror = (e) => { + this.webSocket.onerror = () => { clearTimeout(directSocketTimeout) } const pingDirectSocket = () => { - directSocket.send('ping') + this.webSocket.send('ping') directSocketTimeout = setTimeout(pingDirectSocket, 295000) } @@ -594,40 +2318,64 @@ class ChatPage extends LitElement { groupSocketLink = `ws://${nodeUrl}/websockets/chat/messages?txGroupId=${groupId}`; } - const groupSocket = new WebSocket(groupSocketLink); + this.webSocket = new WebSocket(groupSocketLink); // Open Connection - groupSocket.onopen = () => { + this.webSocket.onopen = () => { setTimeout(pingGroupSocket, 50) } // Message Event - groupSocket.onmessage = (e) => { + this.webSocket.onmessage = async (e) => { if (initial === 0) { + + const cachedData = null; + let getInitialMessages = [] + if (cachedData && cachedData.length !== 0) { + + const lastMessage = cachedData[cachedData.length - 1] + + const newMessages = await parentEpml.request('apiCall', { + type: 'api', + url: `/chat/messages?txGroupId=${groupId}&limit=20&reverse=true&after=${lastMessage.timestamp}&haschatreference=false`, + }); + + getInitialMessages = [...cachedData, ...newMessages].slice(-20) + } else { + getInitialMessages = await parentEpml.request('apiCall', { + type: 'api', + url: `/chat/messages?txGroupId=${groupId}&limit=20&reverse=true&haschatreference=false`, + }); + + + } + + + this.processMessages(getInitialMessages, true) - this.isLoadingMessages = true - this.processMessages(JSON.parse(e.data), true) initial = initial + 1 } else { - - this.processMessages(JSON.parse(e.data), false) + if(e.data){ + this.processMessages(JSON.parse(e.data), false) + } + } } // Closed Event - groupSocket.onclose = () => { + this.webSocket.onclose = () => { clearTimeout(groupSocketTimeout) } // Error Event - groupSocket.onerror = (e) => { + this.webSocket.onerror = () => { clearTimeout(groupSocketTimeout) } const pingGroupSocket = () => { - groupSocket.send('ping') + this.webSocket.send('ping') groupSocketTimeout = setTimeout(pingGroupSocket, 295000) } @@ -651,41 +2399,360 @@ class ChatPage extends LitElement { // Add to the messages... TODO: Save messages to localstorage and fetch from it to make it persistent... } - _sendMessage() { + resetChatEditor(){ + if(this.currentEditor === '_chatEditorDOM'){ + this.editor.commands.setContent('') + } + if(this.currentEditor === 'newChat'){ + this.editorImage.commands.setContent('') + } + } + + async _sendMessage(outSideMsg, msg) { + if(this.isReceipient){ + let hasPublicKey = true + if(!this._publicKey.hasPubKey){ + hasPublicKey = false + try { + const res = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/publickey/${this.selectedAddress.address}` + }) + if (res.error === 102) { + this._publicKey.key = '' + this._publicKey.hasPubKey = false + } else if (res !== false) { + this._publicKey.key = res + this._publicKey.hasPubKey = true + hasPublicKey = true + } else { + this._publicKey.key = '' + this._publicKey.hasPubKey = false + } + } catch (error) { + console.error(error); + } + + if(!hasPublicKey || !this._publicKey.hasPubKey){ + let err4string = get("chatpage.cchange39"); + parentEpml.request('showSnackBar', `${err4string}`) + return + } + + } + } + // have params to determine if it's a reply or not + // have variable to determine if it's a response, holds signature in constructor + // need original message signature + // need whole original message object, transform the data and put it in local storage + // create new var called repliedToData and use that to modify the UI + // find specific object property in local + let typeMessage = 'regular'; + let workerImage; this.isLoading = true; - this.chatEditor.disable(); - const messageText = this.mirrorChatInput.value; + const trimmedMessage = msg + + const getName = async (recipient)=> { + try { + const getNames = await parentEpml.request("apiCall", { + type: "api", + url: `/names/address/${recipient}`, + }); - // Format and Sanitize Message - const sanitizedMessage = messageText.replace(/ /gi, ' ').replace(//gi, '\n'); - const trimmedMessage = sanitizedMessage.trim(); + if (Array.isArray(getNames) && getNames.length > 0 ) { + return getNames[0].name + } else { + return '' + } - if (/^\s*$/.test(trimmedMessage)) { + } catch (error) { + return "" + } + } + + if (outSideMsg && outSideMsg.type === 'delete') { + this.isDeletingImage = true + const userName = outSideMsg.name + const identifier = outSideMsg.identifier + let compressedFile = '' + var str = "iVBORw0KGgoAAAANSUhEUgAAAsAAAAGMAQMAAADuk4YmAAAAA1BMVEX///+nxBvIAAAAAXRSTlMAQObYZgAAADlJREFUeF7twDEBAAAAwiD7p7bGDlgYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAGJrAABgPqdWQAAAABJRU5ErkJggg=="; + + if (this.webWorkerImage) { + workerImage = this.webWorkerImage; + } else { + this.webWorkerImage = new WebWorkerImage(); + } + + const b64toBlob = (b64Data, contentType='', sliceSize=512) => { + const byteCharacters = atob(b64Data); + const byteArrays = []; + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize); + + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + + const blob = new Blob(byteArrays, {type: contentType}); + return blob; + } + const blob = b64toBlob(str, 'image/png'); + await new Promise(resolve => { + new Compressor(blob, { + quality: 0.6, + maxWidth: 500, + success(result) { + const file = new File([result], "name", { + type: 'image/png' + }); + + compressedFile = file; + resolve(); + }, + error(err) { + console.log(err.message); + }, + }) + }) + try { + await publishData({ + registeredName: userName, + file : compressedFile, + service: 'QCHAT_IMAGE', + identifier: identifier, + parentEpml, + metaData: undefined, + uploadType: 'file', + selectedAddress: this.selectedAddress, + worker: workerImage + }) + this.isDeletingImage = false + } catch (error) { + this.isLoading = false; + return + } + typeMessage = 'edit'; + let chatReference = outSideMsg.editedMessageObj.reference; + + if(outSideMsg.editedMessageObj.chatReference){ + chatReference = outSideMsg.editedMessageObj.chatReference; + } + + let message = ""; + try { + const parsedMessageObj = JSON.parse(outSideMsg.editedMessageObj.decodedMessage); + message = parsedMessageObj; + + } catch (error) { + message = outSideMsg.editedMessageObj.decodedMessage; + } + const messageObject = { + ...message, + isImageDeleted: true + } + const stringifyMessageObject = JSON.stringify(messageObject); + this.sendMessage(stringifyMessageObject, typeMessage, chatReference); + + + } + else if (outSideMsg && outSideMsg.type === 'image') { + this.isUploadingImage = true; + const userName = await getName(this.selectedAddress.address); + if (!userName) { + parentEpml.request('showSnackBar', get("chatpage.cchange27")); + this.isLoading = false; + return; + } + + if (this.webWorkerImage) { + workerImage = this.webWorkerImage; + } else { + this.webWorkerImage = new WebWorkerImage(); + } + + const image = this.imageFile + const id = this.uid(); + const identifier = `qchat_${id}`; + let compressedFile = ''; + await new Promise(resolve => { + new Compressor( image, { + quality: .6, + maxWidth: 500, + success(result){ + const file = new File([result], "name", { + type: image.type + }); + compressedFile = file + resolve() + }, + error(err) { + console.log(err.message); + }, + }) + }) + const fileSize = compressedFile.size; + if (fileSize > 500000) { + parentEpml.request('showSnackBar', get("chatpage.cchange26")); + this.isLoading = false; + this.isUploadingImage = false; + return; + } + try { + await publishData({ + registeredName: userName, + file : compressedFile, + service: 'QCHAT_IMAGE', + identifier : identifier, + parentEpml, + metaData: undefined, + uploadType: 'file', + selectedAddress: this.selectedAddress, + worker: workerImage + }); + this.isUploadingImage = false; + this.removeImage() + } catch (error) { + this.isLoading = false; + this.isUploadingImage = false; + return; + } + + + + const messageObject = { + messageText: trimmedMessage, + images: [{ + service: "QCHAT_IMAGE", + name: userName, + identifier: identifier + }], + isImageDeleted: false, + repliedTo: '', + version: 2 + }; + const stringifyMessageObject = JSON.stringify(messageObject); + this.sendMessage(stringifyMessageObject, typeMessage); + } else if (outSideMsg && outSideMsg.type === 'reaction') { + typeMessage = 'edit'; + let chatReference = outSideMsg.editedMessageObj.reference; + + if (outSideMsg.editedMessageObj.chatReference) { + chatReference = outSideMsg.editedMessageObj.chatReference; + } + + let message = ""; + + try { + const parsedMessageObj = JSON.parse(outSideMsg.editedMessageObj.decodedMessage); + message = parsedMessageObj; + + } catch (error) { + message = outSideMsg.editedMessageObj.decodedMessage; + } + + let reactions = message.reactions || [] + const findEmojiIndex = reactions.findIndex((reaction)=> reaction.type === outSideMsg.reaction) + if(findEmojiIndex !== -1){ + let users = reactions[findEmojiIndex].users || [] + const findUserIndex = users.findIndex((user)=> user === this.selectedAddress.address ) + if(findUserIndex !== -1){ + users.splice(findUserIndex, 1) + } else { + users.push(this.selectedAddress.address) + } + reactions[findEmojiIndex] = { + ...reactions[findEmojiIndex], + qty: users.length, + users + } + if(users.length === 0){ + reactions.splice(findEmojiIndex, 1) + } + } else { + reactions = [...reactions, { + type: outSideMsg.reaction, + qty: 1, + users: [this.selectedAddress.address] + }] + } + const messageObject = { + ...message, + reactions + } + const stringifyMessageObject = JSON.stringify(messageObject) + this.sendMessage(stringifyMessageObject, typeMessage, chatReference); + } else if (/^\s*$/.test(trimmedMessage)) { this.isLoading = false; - this.chatEditor.enable(); - } else if (trimmedMessage.length >= 256) { - this.isLoading = false; - this.chatEditor.enable(); - let err1string = get("chatpage.cchange24"); - parentEpml.request('showSnackBar', `${err1string}`); + + } + else if (this.repliedToMessageObj) { + let chatReference = this.repliedToMessageObj.reference; + if(this.repliedToMessageObj.chatReference){ + chatReference = this.repliedToMessageObj.chatReference; + } + typeMessage = 'reply'; + const messageObject = { + messageText: trimmedMessage, + images: [''], + repliedTo: chatReference, + version: 2 + } + const stringifyMessageObject = JSON.stringify(messageObject); + this.sendMessage(stringifyMessageObject, typeMessage); + } else if (this.editedMessageObj) { + typeMessage = 'edit' + let chatReference = this.editedMessageObj.reference + + if(this.editedMessageObj.chatReference){ + chatReference = this.editedMessageObj.chatReference + } + + let message = "" + try { + const parsedMessageObj = JSON.parse(this.editedMessageObj.decodedMessage) + message = parsedMessageObj + + } catch (error) { + message = this.editedMessageObj.decodedMessage + } + const messageObject = { + ...message, + messageText: trimmedMessage, + + } + const stringifyMessageObject = JSON.stringify(messageObject) + this.sendMessage(stringifyMessageObject, typeMessage, chatReference); } else { + const messageObject = { + messageText: trimmedMessage, + images: [''], + repliedTo: '', + version: 2 + } + const stringifyMessageObject = JSON.stringify(messageObject) + if (this.balance < 4) { this.myTrimmedMeassage = '' - this.myTrimmedMeassage = trimmedMessage + this.myTrimmedMeassage = stringifyMessageObject this.shadowRoot.getElementById('confirmDialog').open() } else { - this.sendMessage(trimmedMessage) + this.sendMessage(stringifyMessageObject, typeMessage); } } } - async sendMessage(messageText) { + async sendMessage(messageText, typeMessage, chatReference, isForward) { this.isLoading = true; let _reference = new Uint8Array(64); window.crypto.getRandomValues(_reference); let reference = window.parent.Base58.encode(_reference); - const sendMessageRequest = async () => { if (this.isReceipient === true) { let chatResponse = await parentEpml.request('chat', { @@ -695,15 +2762,16 @@ class ChatPage extends LitElement { timestamp: Date.now(), recipient: this._chatId, recipientPublicKey: this._publicKey.key, - hasChatReference: 0, + hasChatReference: typeMessage === 'edit' ? 1 : 0, + chatReference: chatReference, message: messageText, lastReference: reference, proofOfWorkNonce: 0, - isEncrypted: this._publicKey.hasPubKey === false ? 0 : 1, + isEncrypted: 1, isText: 1 } }); - + _computePow(chatResponse) } else { let groupResponse = await parentEpml.request('chat', { @@ -713,7 +2781,8 @@ class ChatPage extends LitElement { timestamp: Date.now(), groupID: Number(this._chatId), hasReceipient: 0, - hasChatReference: 0, + hasChatReference: typeMessage === 'edit' ? 1 : 0, + chatReference: chatReference, message: messageText, lastReference: reference, proofOfWorkNonce: 0, @@ -726,42 +2795,229 @@ class ChatPage extends LitElement { } }; - const _computePow = async (chatBytes) => { - const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; }); - const chatBytesArray = new Uint8Array(_chatBytesArray); - const chatBytesHash = new window.parent.Sha256().process(chatBytesArray).finish().result; - const hashPtr = window.parent.sbrk(32, window.parent.heap); - const hashAry = new Uint8Array(window.parent.memory.buffer, hashPtr, 32); - hashAry.set(chatBytesHash); + const sendForwardRequest = async () => { + const userInput = this.shadowRoot.getElementById("sendTo").value.trim(); + if(!userInput && !this.forwardActiveChatHeadUrl.url) { + let err4string = get("chatpage.cchange65"); + getSendChatResponse(false, true, err4string ); + return + } + let publicKey = { + hasPubKey: false, + key: '' + }; + + if (this.forwardActiveChatHeadUrl.url) { + const activeChatHeadAddress = this.forwardActiveChatHeadUrl.url.split('/')[1]; + try { + const res = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/publickey/${activeChatHeadAddress}` + }) + + if (res.error === 102) { + publicKey.key = '' + publicKey.hasPubKey = false + } else if (res !== false) { + publicKey.key = res + publicKey.hasPubKey = true + } else { + publicKey.key = '' + publicKey.hasPubKey = false + } + } catch (error) { + console.error(error); + } + } + + if (!this.forwardActiveChatHeadUrl.selected && this.shadowRoot.getElementById("sendTo").value !== "") { + + try { + let userPubkey = ""; + const validatedAddress = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/validate/${userInput}` + }); + + const validatedUsername = await parentEpml.request('apiCall', { + type: 'api', + url: `/names/${userInput}` + }); + + if (validatedAddress && validatedUsername.name) { + userPubkey = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/publickey/${validatedUsername.owner}` + }); + this.forwardActiveChatHeadUrl = { + ...this.forwardActiveChatHeadUrl, + url: `direct/${validatedUsername.owner}`, + name: validatedUsername.name, + selected: true + }; + } else + if (!validatedAddress && (validatedUsername && !validatedUsername.error)) { + userPubkey = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/publickey/${validatedUsername.owner}` + }); + this.forwardActiveChatHeadUrl = { + ...this.forwardActiveChatHeadUrl, + url: `direct/${validatedUsername.owner}`, + name: validatedUsername.name, + selected: true + }; + } else if (validatedAddress && !validatedUsername.name) { + userPubkey = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/publickey/${userInput}` + }); + this.forwardActiveChatHeadUrl = { + ...this.forwardActiveChatHeadUrl, + url: `direct/${userInput}`, + name: "", + selected: true + }; + } else if (!validatedAddress && !validatedUsername.name) { + let err4string = get("chatpage.cchange62"); + // parentEpml.request('showSnackBar', `${err4string}`); + getSendChatResponse(false, true, err4string); + return; + } + + if (userPubkey.error === 102) { + publicKey.key = ''; + publicKey.hasPubKey = false; + } else if (userPubkey !== false) { + publicKey.key = userPubkey; + publicKey.hasPubKey = true; + } else { + publicKey.key = ''; + publicKey.hasPubKey = false; + } + } catch (error) { + console.error(error); + } + } + + const isRecipient = this.forwardActiveChatHeadUrl.url.includes('direct') === true ? true : false; + + const recipientAddress = this.forwardActiveChatHeadUrl.url.split('/')[1]; + this.openForwardOpen = false; + if (isRecipient === true) { + if(!publicKey.hasPubKey){ + let err4string = get("chatpage.cchange39"); + parentEpml.request('showSnackBar', `${err4string}`); + getSendChatResponse(false); + return; + } + let chatResponse = await parentEpml.request('chat', { + type: 18, + nonce: this.selectedAddress.nonce, + params: { + timestamp: Date.now(), + recipient: recipientAddress, + recipientPublicKey: publicKey.key, + hasChatReference: 0, + chatReference: "", + message: messageText, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 1, + isText: 1 + } + }); + + _computePow(chatResponse, true) + } else { + let groupResponse = await parentEpml.request('chat', { + type: 181, + nonce: this.selectedAddress.nonce, + params: { + timestamp: Date.now(), + groupID: Number(recipientAddress), + hasReceipient: 0, + hasChatReference: 0, + chatReference: chatReference, + message: messageText, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 0, // Set default to not encrypted for groups + isText: 1 + } + }); + + _computePow(groupResponse, true) + } + }; + + const _computePow = async (chatBytes, isForward) => { const difficulty = this.balance < 4 ? 18 : 8; - const workBufferLength = 8 * 1024 * 1024; - const workBufferPtr = window.parent.sbrk(workBufferLength, window.parent.heap); - let nonce = window.parent.computePow(hashPtr, workBufferPtr, workBufferLength, difficulty); + const path = window.parent.location.origin + '/memory-pow/memory-pow.wasm.full' + + let worker; + + if (this.webWorker) { + worker = this.webWorker; + } else { + this.webWorker = new WebWorker(); + } + + let nonce = null; + + let chatBytesArray = null; + + await new Promise((res) => { + worker.postMessage({chatBytes, path, difficulty}); + worker.onmessage = e => { + chatBytesArray = e.data.chatBytesArray; + nonce = e.data.nonce; + res(); + } + }); let _response = await parentEpml.request('sign_chat', { nonce: this.selectedAddress.nonce, chatBytesArray: chatBytesArray, chatNonce: nonce }); - getSendChatResponse(_response); + + getSendChatResponse(_response, isForward); }; - const getSendChatResponse = (response) => { + const getSendChatResponse = (response, isForward, customErrorMessage) => { if (response === true) { - this.chatEditor.resetValue(); + this.resetChatEditor() + if(isForward){ + let successString = get("blockpage.bcchange15"); + parentEpml.request('showSnackBar', `${successString}`); + } } else if (response.error) { parentEpml.request('showSnackBar', response.message); } else { let err2string = get("chatpage.cchange21"); - parentEpml.request('showSnackBar', `${err2string}`); + parentEpml.request('showSnackBar', `${customErrorMessage || err2string}`); + } + if(isForward && response !== true){ + this.isLoading = false; + return } - this.isLoading = false; - this.chatEditor.enable(); + this.closeEditMessageContainer() + this.closeRepliedToContainer() + this.openForwardOpen = false + this.forwardActiveChatHeadUrl = { + url: "", + name: "", + selected: false + } }; - // Exec.. + if (isForward) { + sendForwardRequest(); + return; + } sendMessageRequest(); } @@ -778,7 +3034,7 @@ class ChatPage extends LitElement { if (entries[0].isIntersecting) { this.setIsUserDown(true) - this.hideNewMesssageBar() + this.hideNewMessageBar() } else { this.setIsUserDown(false) @@ -819,283 +3075,6 @@ class ChatPage extends LitElement { if (!arr) { return true } return arr.length === 0 } - - initChatEditor() { - - const ChatEditor = function (editorConfig) { - - const ChatEditor = function () { - const editor = this; - editor.init(); - }; - - ChatEditor.prototype.getValue = function () { - const editor = this; - - if (editor.content) { - return editor.content.body.innerHTML; - } - }; - - ChatEditor.prototype.setValue = function (value) { - const editor = this; - - if (value) { - editor.content.body.innerHTML = value; - editor.updateMirror(); - } - - editor.focus(); - }; - - ChatEditor.prototype.resetValue = function () { - const editor = this; - editor.content.body.innerHTML = ''; - editor.updateMirror(); - editor.focus(); - }; - - ChatEditor.prototype.styles = function () { - const editor = this; - - editor.styles = document.createElement('style'); - editor.styles.setAttribute('type', 'text/css'); - editor.styles.innerText = ` - html { - cursor: text; - } - body { - font-size: 1rem; - line-height: 1.38rem; - font-weight: 400; - font-family: "Open Sans", helvetica, sans-serif; - padding-right: 3px; - text-align: left; - white-space: break-spaces; - word-break: break-word; - outline: none; - } - body[contentEditable=true]:empty:before { - content: attr(data-placeholder); - display: block; - color: rgb(103, 107, 113); - text-overflow: ellipsis; - overflow: hidden; - user-select: none; - white-space: nowrap; - } - body[contentEditable=false]{ - background: rgba(0,0,0,0.1); - } - img.emoji { - width: 1.7em; - height: 1.5em; - margin-bottom: -2px; - vertical-align: bottom; - } - `; - editor.content.head.appendChild(editor.styles); - }; - - ChatEditor.prototype.enable = function () { - const editor = this; - - editor.content.body.setAttribute('contenteditable', 'true'); - editor.focus(); - }; - - ChatEditor.prototype.disable = function () { - const editor = this; - - editor.content.body.setAttribute('contenteditable', 'false'); - }; - - ChatEditor.prototype.state = function () { - const editor = this; - - return editor.content.body.getAttribute('contenteditable'); - }; - - ChatEditor.prototype.focus = function () { - const editor = this; - - editor.content.body.focus(); - }; - - ChatEditor.prototype.clearSelection = function () { - const editor = this; - - let selection = editor.content.getSelection().toString(); - if (!/^\s*$/.test(selection)) editor.content.getSelection().removeAllRanges(); - }; - - ChatEditor.prototype.insertEmoji = function (emojiImg) { - const editor = this; - - const doInsert = () => { - - if (editor.content.queryCommandSupported("InsertHTML")) { - editor.content.execCommand("insertHTML", false, emojiImg); - editor.updateMirror(); - } - }; - - editor.focus(); - return doInsert(); - }; - - ChatEditor.prototype.insertText = function (text) { - const editor = this; - - const parsedText = editorConfig.emojiPicker.parse(text); - const doPaste = () => { - - if (editor.content.queryCommandSupported("InsertHTML")) { - editor.content.execCommand("insertHTML", false, parsedText); - editor.updateMirror(); - } - }; - - editor.focus(); - return doPaste(); - }; - - ChatEditor.prototype.updateMirror = function () { - const editor = this; - - const chatInputValue = editor.getValue(); - const filteredValue = chatInputValue.replace(//g, ''); - - let unescapedValue = editorConfig.unescape(filteredValue); - editor.mirror.value = unescapedValue; - }; - - ChatEditor.prototype.listenChanges = function () { - const editor = this; - - ['drop', 'contextmenu', 'mouseup', 'click', 'touchend', 'keydown', 'blur', 'paste'].map(function (event) { - editor.content.body.addEventListener(event, function (e) { - - if (e.type === 'click') { - - e.preventDefault(); - e.stopPropagation(); - } - - if (e.type === 'paste') { - e.preventDefault(); - - navigator.clipboard.readText().then(clipboardText => { - - let escapedText = editorConfig.escape(clipboardText); - - editor.insertText(escapedText); - }).catch(err => { - - // Fallback if everything fails... - let textData = (e.originalEvent || e).clipboardData.getData('text/plain'); - editor.insertText(textData); - }) - return false; - } - - if (e.type === 'contextmenu') { - e.preventDefault(); - e.stopPropagation(); - return false; - } - - if (e.type === 'keydown') { - - // Handle Enter - if (e.keyCode === 13 && !e.shiftKey) { - - // Update Mirror - editor.updateMirror(); - - if (editor.state() === 'false') return false; - - editorConfig.sendFunc(); - e.preventDefault(); - return false; - } - - // Handle Commands with CTR or CMD - if (e.ctrlKey || e.metaKey) { - switch (e.keyCode) { - case 66: - case 98: e.preventDefault(); - return false; - case 73: - case 105: e.preventDefault(); - return false; - case 85: - case 117: e.preventDefault(); - return false; - } - - return false; - } - } - - if (e.type === 'blur') { - editor.clearSelection(); - } - - if (e.type === 'drop') { - e.preventDefault(); - - let droppedText = e.dataTransfer.getData('text/plain') - let escapedText = editorConfig.escape(droppedText) - - editor.insertText(escapedText); - return false; - } - - editor.updateMirror(); - }); - }); - - editor.content.addEventListener('click', function (event) { - - event.preventDefault(); - editor.focus(); - }); - }; - - ChatEditor.prototype.init = function () { - const editor = this; - - editor.frame = editorConfig.editableElement; - editor.mirror = editorConfig.mirrorElement; - - editor.content = (editor.frame.contentDocument || editor.frame.document); - editor.content.body.setAttribute('contenteditable', 'true'); - editor.content.body.setAttribute('data-placeholder', editorConfig.placeholder); - editor.content.body.setAttribute('spellcheck', 'false'); - - editor.styles(); - editor.listenChanges(); - }; - - - function doInit() { - return new ChatEditor(); - } - return doInit(); - }; - - const editorConfig = { - mirrorElement: this.mirrorChatInput, - editableElement: this.chatMessageInput, - sendFunc: this._sendMessage, - emojiPicker: this.emojiPicker, - escape: escape, - unescape: unescape, - placeholder: this.chatEditorPlaceholder - }; - this.chatEditor = new ChatEditor(editorConfig); - } } window.customElements.define('chat-page', ChatPage) diff --git a/qortal-ui-plugins/plugins/core/components/ChatRightPanel.js b/qortal-ui-plugins/plugins/core/components/ChatRightPanel.js new file mode 100644 index 00000000..a63bc8c5 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/components/ChatRightPanel.js @@ -0,0 +1,306 @@ +import { LitElement, html, css } from "lit"; +import { render } from "lit/html.js"; +import { get, translate } from "lit-translate"; +import { Epml } from "../../../epml"; +import { getUserNameFromAddress } from "../../utils/getUserNameFromAddress"; +import snackbar from "./snackbar.js"; +import "@material/mwc-button"; +import "@material/mwc-dialog"; +import "@polymer/paper-spinner/paper-spinner-lite.js"; +import '@polymer/paper-progress/paper-progress.js'; +import "@material/mwc-icon"; +import '@vaadin/button'; +import "./WrapperModal"; +import "./TipUser" +import "./UserInfo/UserInfo"; + +class ChatRightPanel extends LitElement { + static get properties() { + return { + leaveGroupObj: { type: Object }, + error: { type: Boolean }, + chatHeads: { type: Array }, + groupAdmin: { attribute: false }, + groupMembers: { attribute: false }, + selectedHead: { type: Object }, + toggle: { attribute: false }, + getMoreMembers:{ attribute: false }, + setOpenPrivateMessage: { attribute: false }, + userName: { type: String }, + walletBalance: { type: Number }, + sendMoneyLoading: { type: Boolean }, + btnDisable: { type: Boolean }, + errorMessage: { type: String }, + successMessage: { type: String }, + setOpenTipUser: { attribute: false }, + setOpenUserInfo: { attribute: false }, + setUserName: { attribute: false }, + } + } + + constructor() { + super() + this.leaveGroupObj = {} + this.leaveFee = 0.001 + this.error = false + this.chatHeads = [] + this.groupAdmin = [] + this.groupMembers = [] + this.observerHandler = this.observerHandler.bind(this) + this.viewElement = '' + this.downObserverElement = '' + this.myAddress = window.parent.reduxStore.getState().app.selectedAddress.address + this.sendMoneyLoading = false + this.btnDisable = false + this.errorMessage = "" + this.successMessage = "" + } + + static get styles() { + return css` + .top-bar-icon { + cursor: pointer; + height: 18px; + width: 18px; + transition: 0.2s all; + } + + .top-bar-icon:hover { + color: var(--black); + } + + .modal-button { + font-family: Roboto, sans-serif; + font-size: 16px; + color: var(--mdc-theme-primary); + background-color: transparent; + padding: 8px 10px; + border-radius: 5px; + border: none; + transition: all 0.3s ease-in-out; + } + + .close-row { + width: 100%; + display: flex; + justify-content: flex-end; + height: 50px; + flex:0 + + } + + .container-body { + width: 100%; + display: flex; + flex-direction: column; + flex-grow: 1; + overflow:auto; + margin-top: 5px; + padding: 0px 6px; + box-sizing: border-box; + } + + .container-body::-webkit-scrollbar-track { + background-color: whitesmoke; + border-radius: 7px; + } + + .container-body::-webkit-scrollbar { + width: 6px; + border-radius: 7px; + background-color: whitesmoke; + } + + .container-body::-webkit-scrollbar-thumb { + background-color: rgb(180, 176, 176); + border-radius: 7px; + transition: all 0.3s ease-in-out; + } + + .container-body::-webkit-scrollbar-thumb:hover { + background-color: rgb(148, 146, 146); + cursor: pointer; + } + + p { + color: var(--black); + margin: 0px; + padding: 0px; + word-break: break-all; + } + + .container { + display: flex; + width: 100%; + flex-direction: column; + height: 100%; + } + + .chat-right-panel-label { + font-family: Montserrat, sans-serif; + color: var(--group-header); + padding: 5px; + font-size: 13px; + user-select: none; + } + + .group-info { + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 10px; + } + + .group-name { + font-family: Raleway, sans-serif; + font-size: 20px; + color: var(--chat-bubble-msg-color); + text-align: center; + user-select: none; + } + + .group-description { + font-family: Roboto, sans-serif; + color: var(--chat-bubble-msg-color); + letter-spacing: 0.3px; + font-weight: 300; + font-size: 14px; + margin-top: 15px; + word-break: break-word; + user-select: none; + } + + .group-subheader { + font-family: Montserrat, sans-serif; + font-size: 14px; + color: var(--chat-bubble-msg-color); + } + + .group-data { + font-family: Roboto, sans-serif; + letter-spacing: 0.3px; + font-weight: 300; + font-size: 14px; + color: var(--chat-bubble-msg-color); + } + ` + } + + firstUpdated() { + this.viewElement = this.shadowRoot.getElementById('viewElement'); + this.downObserverElement = this.shadowRoot.getElementById('downObserver'); + this.elementObserver(); + } + + async updated(changedProperties) { + if (changedProperties && changedProperties.has('selectedHead')) { + if (this.selectedHead !== {}) { + const userName = await getUserNameFromAddress(this.selectedHead.address); + this.userName = userName; + } + } + } + + elementObserver() { + const options = { + root: this.viewElement, + rootMargin: '0px', + threshold: 1 + } + // identify an element to observe + const elementToObserve = this.downObserverElement; + // passing it a callback function + const observer = new IntersectionObserver(this.observerHandler, options); + // call `observe()` on that MutationObserver instance, + // passing it the element to observe, and the options object + observer.observe(elementToObserve); + } + + observerHandler(entries) { + if (!entries[0].isIntersecting) { + return + } else { + if(this.groupMembers.length < 20){ + return + } + console.log('this.leaveGroupObjp', this.leaveGroupObj) + this.getMoreMembers(this.leaveGroupObj.groupId) + } + } + + render() { + const owner = this.groupAdmin.filter((admin)=> admin.address === this.leaveGroupObj.owner) + return html` +
    +
    + this.toggle(false)} style="margin: 0px 10px" icon="vaadin:close" slot="icon"> +
    +
    +

    ${this.leaveGroupObj && this.leaveGroupObj.groupName}

    +
    +

    ${this.leaveGroupObj && this.leaveGroupObj.description}

    +

    Members: ${this.leaveGroupObj && this.leaveGroupObj.memberCount}

    + +

    Date created : ${new Date(this.leaveGroupObj.created).toLocaleDateString("en-US")}

    +
    +
    +

    GROUP OWNER

    + ${owner.map((item) => { + return html` { + if (val.address === this.myAddress) return; + console.log({ val }); + this.selectedHead = val; + this.setOpenUserInfo(true); + this.setUserName({ + sender: val.address, + senderName: val.name ? val.name : "" + }); + }} + chatInfo=${JSON.stringify(item)} + >` + })} +

    ADMINS

    + ${this.groupAdmin.map((item) => { + return html` { + if (val.address === this.myAddress) return; + console.log({ val }); + this.selectedHead = val; + this.setOpenUserInfo(true); + this.setUserName({ + sender: val.address, + senderName: val.name ? val.name : "" + }); + }} + chatInfo=${JSON.stringify(item)} + >` + })} +

    MEMBERS

    + ${this.groupMembers.map((item) => { + return html` { + if (val.address === this.myAddress) return; + console.log({ val }); + this.selectedHead = val; + this.setOpenUserInfo(true); + this.setUserName({ + sender: val.address, + senderName: val.name ? val.name : "" + }); + }} + chatInfo=${JSON.stringify(item)} + >` + })} +
    +
    +
    + + ` + } +} + +customElements.define("chat-right-panel", ChatRightPanel) diff --git a/qortal-ui-plugins/plugins/core/components/ChatScroller-css.js b/qortal-ui-plugins/plugins/core/components/ChatScroller-css.js index c18a6bcc..b88600c8 100644 --- a/qortal-ui-plugins/plugins/core/components/ChatScroller-css.js +++ b/qortal-ui-plugins/plugins/core/components/ChatScroller-css.js @@ -15,6 +15,12 @@ export const chatStyles = css` scrollbar-color: var(--thumbBG) var(--scrollbarBG); --mdc-theme-primary: rgb(3, 169, 244); --mdc-theme-secondary: var(--mdc-theme-primary); + --mdc-dialog-max-width: 85vw; + --mdc-dialog-max-height: 95vh; + } + + * :focus-visible { + outline: none; } *::-webkit-scrollbar-track { @@ -35,110 +41,214 @@ export const chatStyles = css` ul { list-style: none; margin: 0; - padding: 20px; - } - - .last-message-ref { - position: fixed; - font-size: 20px; - right: 40px; - bottom: 100px; - width: 50; - height: 50; - z-index: 5; - opacity: 0; - color: black; - background-color: white; - border-radius: 50%; - transition: all 0.1s ease-in-out; - } - - .last-message-ref:hover { - cursor: pointer; - transform: scale(1.1); + padding: 20px 17px; } .chat-list { overflow-y: auto; overflow-x: hidden; - height: 92vh; + height: 100%; box-sizing: border-box; } .message-data { width: 92%; margin-bottom: 15px; - margin-left: 50px; + margin-left: 55px; } .message-data-name { - color: var(--black); + user-select: none; + color: #03a9f4; + margin-bottom: 5px; + } + + .forwarded-text { + user-select: none; + color: #03a9f4; + margin-bottom: 5px; + } + + .message-data-forward { + user-select: none; + color: var(--mainmenutext); + margin-bottom: 5px; + font-size: 12px; + } + + .message-data-my-name { + color: #cf21e8; + text-shadow: 0 0 3px #cf21e8; } .message-data-time { - color: #a8aab1; + color: #888888; font-size: 13px; - padding-left: 6px; - padding-bottom: 4px; + user-select: none; + display: flex; + width: 100%; + padding-top: 2px; } - .message-data-level { - color: #03a9f4; + .message-data-time-hidden { + visibility: hidden; + transition: all 0.1s ease-in-out; + color: #888888; font-size: 13px; - padding-left: 8px; - padding-bottom: 4px; + user-select: none; + display: flex; + width: 100%; + padding-top: 2px; + } + + .message-user-info { + display: flex; + justify-content: space-between; + width: 100%; + gap: 10px; + } + + .chat-bubble-container { + display:flex; + gap: 7px; } .message-container { position: relative; } + .message-subcontainer1 { + position: relative; + display: flex; + align-items: flex-end; + } + + .message-subcontainer2 { + position: relative; + display: flex; + background-color: var(--chat-bubble-bg); + flex-grow: 0; + flex-direction: column; + align-items: flex-start; + justify-content: center; + border-radius: 5px; + padding: 12px 15px 4px 15px; + width: fit-content; + min-width: 150px; + } + + .message-triangle { + position: relative; + } + + .message-triangle:after { + content: ""; + position: absolute; + bottom: 0px; + left: -9px; + width: 0; + height: 0; + border-style: solid; + border-width: 0px 0px 7px 9px; + border-color: transparent transparent var(--chat-bubble-bg) transparent; + } + + .message-reactions { + background-color: transparent; + width: calc(100% - 54px); + margin-left: 54px; + } + + .original-message { + position: relative; + display: flex; + flex-direction: column; + color: var(--chat-bubble-msg-color); + line-height: 19px; + user-select: text; + font-size: 15px; + width: 90%; + border-radius: 5px; + padding: 8px 5px 8px 25px; + margin-bottom: 10px; + cursor: pointer; + } + + .original-message:before { + content: ""; + position: absolute; + top: 5px; + left: 10px; + height: 75%; + width: 2.6px; + background-color: var(--mdc-theme-primary); + } + + .original-message-sender { + margin: 0 0 5px 0; + color: var(--mdc-theme-primary); + cursor: pointer; + } + + .replied-message { + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 300px; + max-height: 40px; + } + .replied-message p { + margin: 0px; + padding: 0px; + } + .message { - color: black; - padding: 12px 10px; + display: flex; + flex-direction: column; + color: var(--chat-bubble-msg-color); line-height: 19px; - white-space: pre-line; - word-wrap: break-word; + overflow-wrap: anywhere; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; user-select: text; font-size: 16px; - border-radius: 7px; - margin-bottom: 20px; - width: 90%; + width: 100%; position: relative; } - .message:after { - bottom: 100%; - left: 93%; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - white-space: pre-line; - word-wrap: break-word; - pointer-events: none; - border-bottom-color: #ddd; - border-width: 10px; - margin-left: -10px; + .message-data-avatar { + margin: 0px 10px 0px 3px; + width: 42px; + height: 42px; + float: left; + } + + .message-parent { + padding: 3px; + background: rgba(245, 245, 245, 0); + transition: all 0.1s ease-in-out; + } + + .message-parent:hover { + background: var(--chat-bubble); + border-radius: 8px; } .message-parent:hover .chat-hover { display: block; } - .message-parent:hover .message{ - filter:brightness(0.90); + .message-parent:hover .message-data-time-hidden { + visibility: visible; } .chat-hover { display: none; position: absolute; - top: -38px; - left: 88.2%; + top: -25px; + right: 5px; } .emoji { @@ -148,27 +258,7 @@ export const chatStyles = css` vertical-align: bottom; object-fit: contain; } - - .my-message { - background: #d1d1d1; - border: 2px solid #eeeeee; - } - - .my-message:after { - border-bottom-color: #d1d1d1; - left: 7%; - } - - .other-message { - background: #f1f1f1; - border: 2px solid #dedede; - } - - .other-message:after { - border-bottom-color: #f1f1f1; - left: 7%; - } - + .align-left { text-align: left; } @@ -202,25 +292,29 @@ export const chatStyles = css` display: flex; flex-direction: row; align-items: center; - gap: 5px; - background-color: white; - border: 1px solid #dad9d9; + background-color: var(--chat-menu-bg); + border: 1px solid var(--chat-menu-outline); border-radius: 5px; height:100%; - width: 100px; position: relative; } + .container:focus-visible { + outline: none; + } + .menu-icon { width: 100%; - padding: 5px; + padding: 5px 7px; display: flex; align-items: center; font-size: 13px; + color: var(--chat-menu-icon); } .menu-icon:hover { - background-color: #dad9d9; + border-radius: 5px; + background-color: var(--chat-menu-icon-hover); transition: all 0.1s ease-in-out; cursor: pointer; } @@ -231,11 +325,12 @@ export const chatStyles = css` .tooltip:before { content: attr(data-text); + display: none; position: absolute; top: -47px; left: 50%; transform: translateX(-50%); - width: 90px; + width: auto; padding: 10px; border-radius: 10px; background:#fff; @@ -244,7 +339,8 @@ export const chatStyles = css` box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; font-size: 12px; z-index: 5; - display: none; + white-space: nowrap; + overflow: hidden; } .tooltip:hover:before { @@ -269,17 +365,299 @@ export const chatStyles = css` .block-user-container { display: block; position: absolute; - left: -48px; + left: -5px; } .block-user { - justify-content: space-between; + width: 100%; + padding: 5px 7px; + display: flex; + align-items: center; + font-size: 13px; + color: var(--chat-menu-icon); + justify-content: space-evenly; border: 1px solid rgb(218, 217, 217); border-radius: 5px; - background-color: white; - width: 100%; + background-color: var(--chat-menu-bg); + width: 150px; height: 32px; padding: 3px 8px; box-shadow: rgba(77, 77, 82, 0.2) 0px 7px 29px 0px; } + + .block-user:hover { + cursor:pointer; + background-color: var(--block-user-bg-hover); + transition: all 0.1s ease-in-out 0s; + } + + .reactions-bg { + background-color: #d5d5d5; + border-radius: 10px; + padding: 5px; + color: black; + margin-right: 10px; + transition: all 0.1s ease-in-out; + border: 0.5px solid transparent; + cursor: pointer; + } + + .reactions-bg:hover { + border: 0.5px solid var(--reaction-bubble-outline); + } + + .image-container { + display: flex; + } + + .message-data-level { + height: 21px; + width: 21px; + overflow: hidden; + } + + .defaultSize { + width: 45vh; + height: 40vh; + } + + .image-deleted-msg { + font-family: Roboto, sans-serif; + font-size: 14px; + font-style: italic; + color: var(--chat-bubble-msg-color); + margin: 0; + padding-top: 10px; + } + + .image-delete-icon { + margin-left: 5px; + height: 20px; + cursor: pointer; + visibility: hidden; + transition: .2s all; + opacity: 0.8; + color: rgb(228, 222, 222); + padding-left: 7px; + } + + .image-delete-icon:hover { + opacity: 1; + } + + .message-parent:hover .image-delete-icon { + visibility: visible; + } + + .imageContainer { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + } + .spinnerContainer { + display: flex; + width: 100%; + justify-content: center + } + + .delete-image-msg { + font-family: Livvic, sans-serif; + font-size: 20px; + color: var(--chat-bubble-msg-color); + letter-spacing: 0.3px; + font-weight: 300; + text-align: center; + } + + .modal-button-row { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + } + + .modal-button { + font-family: Roboto, sans-serif; + font-size: 16px; + color: var(--mdc-theme-primary); + background-color: transparent; + padding: 8px 10px; + border-radius: 5px; + border: none; + transition: all 0.3s ease-in-out; + } + + .modal-button-red { + font-family: Roboto, sans-serif; + font-size: 16px; + color: #F44336; + background-color: transparent; + padding: 8px 10px; + border-radius: 5px; + border: none; + transition: all 0.3s ease-in-out; + } + + .modal-button-red:hover { + cursor: pointer; + background-color: #f4433663; + } + + .modal-button:hover { + cursor: pointer; + background-color: #03a8f475; + } + + #messageContent p { + margin: 0px; + padding: 0px; + } + + #messageContent p mark { + background-color: #ffe066; + border-radius: 0.25em; + box-decoration-break: clone; + padding: 0.125em 0; + } + + #messageContent > * + * { + outline: none; + } + + #messageContent ul, + ol { + padding: 0 1rem; + } + + #messageContent h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.1; + } + + #messageContent code { + background-color: rgba(#616161, 0.1); + color: #616161; + } + + #messageContent pre { + background: #0D0D0D; + color: #FFF; + font-family: 'JetBrainsMono', monospace; + padding: 0.75rem 1rem; + border-radius: 0.5rem; + white-space: pre-wrap; + } + #messageContent pre code { + color: inherit; + padding: 0; + background: none; + font-size: 0.8rem; + } + + + #messageContent img { + width: 1.7em; + height: 1.5em; + margin: 0px; + + } + + #messageContent blockquote { + padding-left: 1rem; + border-left: 2px solid rgba(#0D0D0D, 0.1); + } + + #messageContent hr { + border: none; + border-top: 2px solid rgba(#0D0D0D, 0.1); + margin: 2rem 0; + } + + .replied-message p { + margin: 0px; + padding: 0px; + } + + .replied-message > * + * { + margin-top: 0.75em; + outline: none; + } + + .replied-message ul, + ol { + padding: 0 1rem; + } + + .replied-message h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.1; + } + + .replied-message code { + background-color: rgba(#616161, 0.1); + color: #616161; + } + + .replied-message pre { + background: #0D0D0D; + color: #FFF; + font-family: 'JetBrainsMono', monospace; + padding: 0.75rem 1rem; + border-radius: 0.5rem; + white-space: pre-wrap; + margin: 0px; + } + .replied-message pre code { + color: inherit; + padding: 0; + background: none; + font-size: 0.8rem; + } + + + .replied-message img { + width: 1.7em; + height: 1.5em; + margin: 0px; + + } + + .replied-message blockquote { + padding-left: 1rem; + border-left: 2px solid rgba(#0D0D0D, 0.1); + } + + .replied-message hr { + border: none; + border-top: 2px solid rgba(#0D0D0D, 0.1); + margin: 2rem 0; + } + + .edited-message-style { + font-family: "Work Sans", sans-serif; + font-style: italic; + font-size: 13px; + visibility: visible; + } + + .blink-bg{ + border-radius: 8px; + animation: blinkingBackground 3s; + } + @keyframes blinkingBackground{ + 0% { background-color: rgba(var(--menuactivergb), 1)} + + 100% { background-color:rgba(var(--menuactivergb), 0)} + } + + ` diff --git a/qortal-ui-plugins/plugins/core/components/ChatScroller.js b/qortal-ui-plugins/plugins/core/components/ChatScroller.js index 92c95673..fc80c6c8 100644 --- a/qortal-ui-plugins/plugins/core/components/ChatScroller.js +++ b/qortal-ui-plugins/plugins/core/components/ChatScroller.js @@ -5,26 +5,54 @@ import { translate, get } from 'lit-translate'; import {unsafeHTML} from 'lit/directives/unsafe-html.js'; import { chatStyles } from './ChatScroller-css.js' import { Epml } from "../../../epml"; +import { cropAddress } from "../../utils/cropAddress"; import './LevelFounder.js'; import './NameMenu.js'; import './ChatModals.js'; +import './WrapperModal'; +import "./UserInfo/UserInfo"; import '@vaadin/icons'; import '@vaadin/icon'; import '@material/mwc-button'; import '@material/mwc-dialog'; import '@material/mwc-icon'; +import { EmojiPicker } from 'emoji-picker-js'; +import { generateHTML } from '@tiptap/core' +import StarterKit from '@tiptap/starter-kit' +import Underline from '@tiptap/extension-underline'; +import Highlight from '@tiptap/extension-highlight' const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) +let toggledMessage = {} class ChatScroller extends LitElement { static get properties() { return { getNewMessage: { attribute: false }, getOldMessage: { attribute: false }, - emojiPicker: { attribute: false }, escapeHTML: { attribute: false }, - initialMessages: { type: Array }, // First set of messages to load.. 15 messages max ( props ) messages: { type: Array }, - hideMessages: { type: Array } + hideMessages: { type: Array }, + setRepliedToMessageObj: { attribute: false }, + setEditedMessageObj: { attribute: false }, + sendMessage: { attribute: false }, + sendMessageForward: { attribute: false }, + showLastMessageRefScroller: { attribute: false }, + emojiPicker: { attribute: false }, + isLoadingMessages: { type: Boolean}, + setIsLoadingMessages: { attribute: false }, + chatId: { type: String }, + setForwardProperties: { attribute: false }, + setOpenPrivateMessage: { attribute: false }, + setOpenUserInfo: { attribute: false }, + setOpenTipUser: { attribute: false }, + setUserName: { attribute: false }, + setSelectedHead: { attribute: false }, + openTipUser: { type: Boolean }, + openUserInfo: { type: Boolean }, + userName: { type: String }, + selectedHead: { type: Object }, + goToRepliedMessage: { attribute: false }, + getOldMessageAfter: {attribute: false} } } @@ -36,61 +64,165 @@ class ChatScroller extends LitElement { this._upObserverhandler = this._upObserverhandler.bind(this) this._downObserverHandler = this._downObserverHandler.bind(this) this.myAddress = window.parent.reduxStore.getState().app.selectedAddress.address - this.hideMessages = JSON.parse(localStorage.getItem("MessageBlockedAddresses") || "[]") + this.hideMessages = JSON.parse(localStorage.getItem("MessageBlockedAddresses") || "[]") + this.openTipUser = false; + this.openUserInfo = false; } - render() { + let formattedMessages = this.messages.reduce((messageArray, message, index) => { + const lastGroupedMessage = messageArray[messageArray.length - 1]; + let timestamp; + let sender; + let repliedToData; + + let firstMessageInChat; + + if (index === 0) { + firstMessageInChat = true; + } else { + firstMessageInChat = false; + } + + message = {...message, firstMessageInChat} + + if (lastGroupedMessage) { + timestamp = lastGroupedMessage.timestamp; + sender = lastGroupedMessage.sender; + repliedToData = lastGroupedMessage.repliedToData; + } + const isSameGroup = Math.abs(timestamp - message.timestamp) < 600000 && sender === message.sender && !repliedToData; + + if (isSameGroup) { + messageArray[messageArray.length - 1].messages = [...(messageArray[messageArray.length - 1]?.messages || []), message]; + } else { + messageArray.push({ + messages: [message], + ...message + }); + } + return messageArray; + }, []) + + return html` + ${this.isLoadingMessages ? html` +
    + +
    + ` : ''}
      - ${repeat( - this.messages, - (message) => message.reference, - (message) => html`` - )} + ${formattedMessages.map((formattedMessage) => { + return repeat( + formattedMessage.messages, + (message) => message.reference, + (message, indexMessage) => html` + 1} + ?isLastMessageInGroup=${indexMessage === formattedMessage.messages.length - 1} + .setToggledMessage=${this.setToggledMessage} + .setForwardProperties=${this.setForwardProperties} + .setOpenPrivateMessage=${(val) => this.setOpenPrivateMessage(val)} + .setOpenTipUser=${(val) => this.setOpenTipUser(val)} + .setOpenUserInfo=${(val) => this.setOpenUserInfo(val)} + .setUserName=${(val) => this.setUserName(val)} + id=${message.reference} + .goToRepliedMessage=${this.goToRepliedMessage} + > + ` + ) + })}
      -
      - { - this.shadowRoot.getElementById('downObserver').scrollIntoView({ - behavior: 'smooth', - }) - }}> - -
    ` } + shouldUpdate(changedProperties) { + if(changedProperties.has('isLoadingMessages')){ + return true + } + if(changedProperties.has('chatId') && changedProperties.get('chatId')){ + return true + } + if(changedProperties.has('openTipUser')){ + return true + } + if(changedProperties.has('openUserInfo')){ + return true + } + if(changedProperties.has('userName')){ + return true + } + // Only update element if prop1 changed. + return changedProperties.has('messages'); + } + + async getUpdateComplete() { + await super.getUpdateComplete(); + const marginElements = Array.from(this.shadowRoot.querySelectorAll('message-template')); + await Promise.all(marginElements.map(el => el.updateComplete)); + return true; + } + + setToggledMessage(message) { + toggledMessage = message; + } + async firstUpdated() { - this.viewElement = this.shadowRoot.getElementById('viewElement') - this.upObserverElement = this.shadowRoot.getElementById('upObserver') - this.downObserverElement = this.shadowRoot.getElementById('downObserver') - + this.emojiPicker.on('emoji', selection => { + this.sendMessage({ + type: 'reaction', + editedMessageObj: toggledMessage, + reaction: selection.emoji, + }) + }); + this.viewElement = this.shadowRoot.getElementById('viewElement'); + this.upObserverElement = this.shadowRoot.getElementById('upObserver'); + this.downObserverElement = this.shadowRoot.getElementById('downObserver'); // Intialize Observers - this.upElementObserver() - this.downElementObserver() - await this.updateComplete - this.viewElement.scrollTop = this.viewElement.scrollHeight + 50 + this.upElementObserver(); + this.downElementObserver(); + await this.getUpdateComplete(); + this.viewElement.scrollTop = this.viewElement.scrollHeight + 50; } _getOldMessage(_scrollElement) { this.getOldMessage(_scrollElement) } + _getOldMessageAfter(_scrollElement) { + this.getOldMessageAfter(_scrollElement) + } + _upObserverhandler(entries) { if (entries[0].isIntersecting) { - let _scrollElement = entries[0].target.nextElementSibling - this._getOldMessage(_scrollElement) + if(this.messages.length < 20){ + return + } + this.setIsLoadingMessages(true); + let _scrollElement = entries[0].target.nextElementSibling; + this._getOldMessage(_scrollElement); } } _downObserverHandler(entries) { if (!entries[0].isIntersecting) { - this.shadowRoot.querySelector(".last-message-ref").style.opacity = '1' + let _scrollElement = entries[0].target.previousElementSibling; + // this._getOldMessageAfter(_scrollElement); + this.showLastMessageRefScroller(true); } else { - this.shadowRoot.querySelector(".last-message-ref").style.opacity = '0' + this.showLastMessageRefScroller(false); } } @@ -100,9 +232,8 @@ class ChatScroller extends LitElement { rootMargin: '0px', threshold: 1 }; - - const observer = new IntersectionObserver(this._upObserverhandler, options) - observer.observe(this.upObserverElement) + const observer = new IntersectionObserver(this._upObserverhandler, options); + observer.observe(this.upObserverElement); } downElementObserver() { @@ -111,17 +242,13 @@ class ChatScroller extends LitElement { rootMargin: '0px', threshold: 1 } - // identify an element to observe - const elementToObserve = this.downObserverElement - + const elementToObserve = this.downObserverElement; // passing it a callback function - const observer = new IntersectionObserver(this._downObserverHandler, options) - + const observer = new IntersectionObserver(this._downObserverHandler, options); // call `observe()` on that MutationObserver instance, // passing it the element to observe, and the options object - observer.observe(elementToObserve) - + observer.observe(elementToObserve); } } @@ -135,9 +262,28 @@ class MessageTemplate extends LitElement { emojiPicker: { attribute: false }, escapeHTML: { attribute: false }, hideMessages: { type: Array }, - openDialogPrivateMessage: {type: Boolean}, - openDialogBlockUser: {type: Boolean}, - showBlockAddressIcon: { type: Boolean } + openDialogPrivateMessage: { type: Boolean }, + openDialogBlockUser: { type: Boolean }, + showBlockAddressIcon: { type: Boolean }, + setRepliedToMessageObj: { attribute: false }, + setEditedMessageObj: { attribute: false }, + sendMessage: { attribute: false }, + sendMessageForward: { attribute: false }, + openDialogImage: { attribute: false }, + openDeleteImage: { type: Boolean }, + isImageLoaded: { type: Boolean }, + isFirstMessage: { type: Boolean }, + isSingleMessageInGroup: { type: Boolean }, + isLastMessageInGroup: { type: Boolean }, + setToggledMessage: { attribute: false }, + setForwardProperties: { attribute: false }, + viewImage: { type: Boolean }, + setOpenPrivateMessage : { attribute: false }, + setOpenTipUser: { attribute: false }, + setOpenUserInfo: { attribute: false }, + setUserName: { attribute: false }, + openTipUser:{ type: Boolean }, + goToRepliedMessage: { attribute: false }, } } @@ -148,6 +294,13 @@ class MessageTemplate extends LitElement { this.openDialogBlockUser = false this.showBlockAddressIcon = false this.myAddress = window.parent.reduxStore.getState().app.selectedAddress.address + this.imageFetches = 0 + this.openDialogImage = false + this.isImageLoaded = false + this.isFirstMessage = false + this.isSingleMessageInGroup = false + this.isLastMessageInGroup = false + this.viewImage = false } static styles = [chatStyles] @@ -171,8 +324,7 @@ class MessageTemplate extends LitElement { } showBlockIconFunc(bool) { - this.shadowRoot.querySelector(".chat-hover").focus({ preventScroll: true }) - if(bool) { + if (bool) { this.showBlockAddressIcon = true; } else { this.showBlockAddressIcon = false; @@ -180,51 +332,343 @@ class MessageTemplate extends LitElement { } render() { - const hidemsg = this.hideMessages + const hidemsg = this.hideMessages; + let message = ""; + let messageVersion2 = "" + let reactions = []; + let repliedToData = null; + let image = null; + let isImageDeleted = false; + let version = 0; + let isForwarded = false + let isEdited = false + try { + const parsedMessageObj = JSON.parse(this.messageObj.decodedMessage); + if(parsedMessageObj.version.toString() === '2'){ - let avatarImg = '' - let nameMenu = '' - let levelFounder = '' - let hideit = hidemsg.includes(this.messageObj.sender) - - levelFounder = html`` + messageVersion2 = generateHTML(parsedMessageObj.messageText, [ + StarterKit, + Underline, + Highlight + // other extensions … + ]) + } + message = parsedMessageObj.messageText; + repliedToData = this.messageObj.repliedToData; + isImageDeleted = parsedMessageObj.isImageDeleted; + reactions = parsedMessageObj.reactions || []; + version = parsedMessageObj.version + isForwarded = parsedMessageObj.type === 'forward' + isEdited = this.messageObj.editedTimestamp && true + if (parsedMessageObj.images && Array.isArray(parsedMessageObj.images) && parsedMessageObj.images.length > 0) { + image = parsedMessageObj.images[0]; + } + } catch (error) { + message = this.messageObj.decodedMessage; + } + let avatarImg = ''; + let imageHTML = ''; + let imageHTMLDialog = ''; + let imageUrl = ''; + let nameMenu = ''; + let levelFounder = ''; + let hideit = hidemsg.includes(this.messageObj.sender); + let forwarded = '' + let edited = '' + levelFounder = html``; if (this.messageObj.senderName) { - const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] - const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port - const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.messageObj.senderName}/qortal_avatar?async=true&apiKey=${myNode.apiKey}` - avatarImg = html`` + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]; + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port; + const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.messageObj.senderName}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`; + avatarImg = html``; + } else { + avatarImg = html`` + } + + const createImage = (imageUrl) => { + const imageHTMLRes = new Image(); + imageHTMLRes.src = imageUrl; + imageHTMLRes.style= "max-width:45vh; max-height:40vh; border-radius: 5px; cursor: pointer"; + imageHTMLRes.onclick= () => { + this.openDialogImage = true; + } + imageHTMLRes.onload = () => { + this.isImageLoaded = true; + } + imageHTMLRes.onerror = () => { + if (this.imageFetches < 4) { + setTimeout(() => { + this.imageFetches = this.imageFetches + 1; + imageHTMLRes.src = imageUrl; + }, 500); + } else { + imageHTMLRes.src = '/img/chain.png'; + imageHTMLRes.style= "max-width:45vh; max-height:20vh; border-radius: 5px; filter: opacity(0.5)"; + imageHTMLRes.onclick= () => { + + } + + this.isImageLoaded = true + } + }; + return imageHTMLRes; + } + if (image) { + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]; + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port; + imageUrl = `${nodeUrl}/arbitrary/${image.service}/${image.name}/${image.identifier}?async=true&apiKey=${myNode.apiKey}`; + + if(this.viewImage || this.myAddress === this.messageObj.sender){ + imageHTML = createImage(imageUrl); + imageHTMLDialog = createImage(imageUrl) + imageHTMLDialog.style= "height: auto; max-height: 80vh; width: auto; max-width: 80vw; object-fit: contain; border-radius: 5px"; + } + } - if (this.messageObj.sender === this.myAddress) { - nameMenu = html`${this.messageObj.senderName ? this.messageObj.senderName : this.messageObj.sender}` - } else { - nameMenu = html`${this.messageObj.senderName ? this.messageObj.senderName : this.messageObj.sender}` + nameMenu = html` + + ${this.messageObj.senderName ? this.messageObj.senderName : cropAddress(this.messageObj.sender)} + + `; + forwarded = html` + + ${translate("blockpage.bcchange17")} + + `; + + edited = html` + + ${translate("chatpage.cchange68")} + + `; + + if (repliedToData) { + try { + const parsedMsg = JSON.parse(repliedToData.decodedMessage); + repliedToData.decodedMessage = parsedMsg; + } catch (error) { + console.error(error); + } + } + const escapedMessage = this.escapeHTML(message) + const replacedMessage = escapedMessage.replace(new RegExp('\r?\n','g'), '
    '); return hideit ? html`
  • ` : html` -
  • -
    - ${nameMenu} - ${levelFounder} - -
    -
    ${avatarImg}
    -
    -
    ${unsafeHTML(this.emojiPicker.parse(this.escapeHTML(this.messageObj.decodedMessage)))}
    - this.showPrivateMessageModal()} - .showBlockUserModal=${() => this.showBlockUserModal()} - .showBlockIconFunc=${(props) => this.showBlockIconFunc(props)} - .showBlockAddressIcon=${this.showBlockAddressIcon} - @blur=${() => this.showBlockIconFunc(false)} - > - +
  • +
    +
    +
    + ${(this.isSingleMessageInGroup === false || + (this.isSingleMessageInGroup === true && this.isLastMessageInGroup === true)) + ? ( + html` +
    { + if (this.myAddress === this.messageObj.sender) return; + this.setOpenUserInfo(true); + this.setUserName(this.messageObj); + }} class="message-data-avatar"> + ${avatarImg} +
    + ` + ) : + html` +
    + `} +
    + + ${repliedToData && html` +
    { + this.goToRepliedMessage(repliedToData) + }}> +

    + ${repliedToData.senderName ?? cropAddress(repliedToData.sender)} +

    +

    + + + ${version.toString() === '1' ? html` + ${repliedToData.decodedMessage.messageText} + ` : ''} + ${version.toString() === '2' ? html` + ${unsafeHTML(generateHTML(repliedToData.decodedMessage.messageText, [ + StarterKit, + Underline, + Highlight + // other extensions … + ]))} + ` : ''} + + +

    +
    + `} + ${image && !isImageDeleted && !this.viewImage && this.myAddress !== this.messageObj.sender ? html` +
    { + this.viewImage = true + }} + class=${[`image-container`, !this.isImageLoaded ? 'defaultSize' : ''].join(' ')} + style=${this.isFirstMessage && "margin-top: 10px;"}> +
    + ${translate("chatpage.cchange40")} +
    + +
    + ` : html``} + ${image && !isImageDeleted && (this.viewImage || this.myAddress === this.messageObj.sender) ? html` +
    + ${imageHTML} { + this.openDeleteImage = true; + this.chatE + }} + class="image-delete-icon" icon="vaadin:close" slot="icon"> +
    + ` : image && isImageDeleted ? html` +

    This image has been deleted

    + ` : html``} +
    + ${version.toString() === '2' ? html` + ${unsafeHTML(messageVersion2)} + ` : ''} + ${version.toString() === '1' ? html` + ${unsafeHTML(this.emojiPicker.parse(replacedMessage))} + ` : ''} +
    + ${isEdited ? + html` + + ${edited} + + ` + : null + } + +
    +
    +
    + this.showPrivateMessageModal()} + .showBlockUserModal=${() => this.showBlockUserModal()} + .showBlockIconFunc=${(props) => this.showBlockIconFunc(props)} + .showBlockAddressIcon=${this.showBlockAddressIcon} + .originalMessage=${{...this.messageObj, message}} + .setRepliedToMessageObj=${this.setRepliedToMessageObj} + .setEditedMessageObj=${this.setEditedMessageObj} + .myAddress=${this.myAddress} + @blur=${() => this.showBlockIconFunc(false)} + .sendMessage=${this.sendMessage} + .sendMessageForward=${this.sendMessageForward} + version=${version} + .emojiPicker=${this.emojiPicker} + .setToggledMessage=${this.setToggledMessage} + .setForwardProperties=${this.setForwardProperties} + ?firstMessageInChat=${this.messageObj.firstMessageInChat} + .setOpenPrivateMessage=${(val) => this.setOpenPrivateMessage(val)} + .setOpenTipUser=${(val) => this.setOpenTipUser(val)} + .setUserName=${(val) => this.setUserName(val)} + > + +
    +
    + ${reactions.map((reaction)=> { + return html` + this.sendMessage({ + type: 'reaction', + editedMessageObj: this.messageObj, + reaction: reaction.type, + })} + class="reactions-bg"> + ${reaction.type} ${reaction.qty} + ` + })} +
    +
    +
  • + { + this.openDialogImage = false + }}> +
    +
    + ${imageHTMLDialog} +
    + { + + this.openDialogImage = false + }} + > + ${translate("general.close")} + +
    + { + this.openDeleteImage = false; + }}> +
    +

    Are you sure you want to delete this image?

    +
    + +
    ` } } @@ -245,18 +736,30 @@ class ChatMenu extends LitElement { static get properties() { return { menuItems: { type: Array }, - selectedAddress: { type: Object }, - showPrivateMessageModal: {type: Function}, - showBlockUserModal: {type: Function}, + showPrivateMessageModal: {attribute: false}, + showBlockUserModal: {attribute: false}, toblockaddress: { type: String, attribute: true }, - showBlockIconFunc: {type: Function}, - showBlockAddressIcon: {type: Boolean} + showBlockIconFunc: {attribute: false}, + showBlockAddressIcon: { type: Boolean }, + originalMessage: { type: Object }, + setRepliedToMessageObj: {attribute: false}, + setEditedMessageObj: {attribute: false}, + myAddress: { type: Object }, + emojiPicker: { attribute: false }, + sendMessage: { attribute: false }, + version: { type: String }, + setToggledMessage: { attribute: false }, + sendMessageForward: { attribute: false }, + setForwardProperties: { attribute: false }, + firstMessageInChat: { type: Boolean }, + setOpenPrivateMessage: { attribute: false }, + setOpenTipUser: { attribute: false }, + setUserName: { attribute: false }, } } constructor() { super(); - this.selectedAddress = window.parent.reduxStore.getState().app.selectedAddress.address; this.showPrivateMessageModal = () => {}; this.showBlockUserModal = () => {}; } @@ -275,23 +778,149 @@ class ChatMenu extends LitElement { console.error('Copy to clipboard error:', err) } } - + + versionErrorSnack(){ + let errorMsg = get("chatpage.cchange34") + parentEpml.request('showSnackBar', `${errorMsg}`) + } + + async messageForwardFunc(){ + let parsedMessageObj = {} + let publicKey = { + hasPubKey: false, + key: '' + } + try { + parsedMessageObj = JSON.parse(this.originalMessage.decodedMessage); + + } catch (error) { + parsedMessageObj = {} + } + + try { + const res = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/publickey/${this._chatId}` + }) + if (res.error === 102) { + publicKey.key = '' + publicKey.hasPubKey = false + } else if (res !== false) { + publicKey.key = res + publicKey.hasPubKey = true + } else { + publicKey.key = '' + publicKey.hasPubKey = false + } + } catch (error) { + + } + + try { + const message = { + ...parsedMessageObj, + type: 'forward' + } + const stringifyMessageObject = JSON.stringify(message) + this.setForwardProperties(stringifyMessageObject) + + } catch (error) { + console.log({error}) + } + } + render() { return html` -
    -