Getting ready for first public announcement:
FORUM CHANGES: - added encryption to Minters room (this was not easy) - cross-UI encrypted communciation now possible in Minters room. - NOTE - Minters room attachments and pictures are not yet able to be VIEWED from others, you can PUBLISH THEM but you CANNOT DOWNLOAD/VIEW THEM YET. - fixed NEW MESSAGE INDICATOR - added focusing on NEW messages when they are indicated in any given room. - added backups to base64 functions for failover - added encrypted publishing for all things in the ADMIN ROOM. - added new temporary group to be utilized for representation of admins on forum and Minter board 'Mintership-Forum-Admins' and added all invites to existing minter admins. - many little improvements and bugfixes. MINTER BOARD CHANGES: - Minter Board should now show downvotes from admins if they are in the Mintership-Forum-Admins group. - 3 downvotes from admins should make the card no longer show up (this needs testing) - Cards are supposed to be displaying in order, but are currently not doing so, will fix this in next update. MINTER ADMIN TOOLS PAGE - Minter Admin Tools page is set to be OBVIOUSLY UNDER CONSTRUCTION - Functionality here will be added once the overall mintership changes are completed. OTHER - Many additional improvements and bug fixes - visual changes - New functions in QortalApi.js for many use cases now and in the future. - updated to version 0.4beta Many additional changes.
This commit is contained in:
parent
f2887c7a6a
commit
eaecae79c7
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/.vscode
|
||||
/.sync*
|
@ -1,260 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html >
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<link rel="shortcut icon" href="assets/images/modded-circle-2-new-128x128.png" type="image/x-icon">
|
||||
<meta name="description" content="Welcome to the Mintership Forum (alpha version)">
|
||||
|
||||
|
||||
<title>Home</title>
|
||||
<link rel="stylesheet" href="assets/web/assets/mobirise-icons2/mobirise2.css">
|
||||
<link rel="stylesheet" href="assets/web/assets/mobirise-icons/mobirise-icons.css">
|
||||
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap-grid.min.css">
|
||||
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap-reboot.min.css">
|
||||
<link rel="stylesheet" href="assets/parallax/jarallax.css">
|
||||
<link rel="stylesheet" href="assets/animatecss/animate.css">
|
||||
<link rel="stylesheet" href="assets/dropdown/css/style.css">
|
||||
<link rel="stylesheet" href="assets/socicon/css/styles.css">
|
||||
<link rel="stylesheet" href="assets/theme/css/style.css">
|
||||
<!-- <link rel="preload" href="https://fonts.googleapis.com/css?family=DM+Sans:100,200,300,400,500,600,700,800,900,100i,200i,300i,400i,500i,600i,700i,800i,900i&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=DM+Sans:100,200,300,400,500,600,700,800,900,100i,200i,300i,400i,500i,600i,700i,800i,900i&display=swap"></noscript>
|
||||
<link rel="preload" href="https://fonts.googleapis.com/css?family=Space+Grotesk:300,400,500,600,700&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Space+Grotesk:300,400,500,600,700&display=swap"></noscript> -->
|
||||
<link rel="preload" as="style" href="assets/mobirise/css/mbr-additional.css?v=U9lZDZ"><link rel="stylesheet" href="assets/mobirise/css/mbr-additional.css?v=U9lZDZ" type="text/css">
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="assets/css/forum-styles.css">
|
||||
<link rel="preload" href="assets/css/css.css?family=DM+Sans:100,200,300,400,500,600,700,800,900,100i,200i,300i,400i,500i,600i,700i,800i,900i&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="assets/css/css.css?family=DM+Sans:100,200,300,400,500,600,700,800,900,100i,200i,300i,400i,500i,600i,700i,800i,900i&display=swap"></noscript>
|
||||
<link rel="preload" href="assets/css/space-grotesk.css?family=Space+Grotesk:300,400,500,600,700&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="assets/css/space-grotesk.css?family=Space+Grotesk:300,400,500,600,700&display=swap"></noscript>
|
||||
<link href="assets/quill/quill.snow.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section data-bs-version="5.1" class="menu menu1 boldm5 cid-ttRnktJ11Q" once="menu" id="menu1-0">
|
||||
|
||||
<nav class="navbar navbar-dropdown navbar-expand-lg">
|
||||
<div class="menu_box container-fluid">
|
||||
<div class="navbar-brand d-flex d-lg-none">
|
||||
<span class="navbar-logo">
|
||||
<a href="index.html">
|
||||
<img src="assets/images/logo.png" alt="">
|
||||
</a>
|
||||
</span>
|
||||
<span class="navbar-caption-wrap">
|
||||
<a class="navbar-caption display-4" href="index.html">Q-Mintership Alpha
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-bs-toggle="collapse" data-target="#navbarSupportedContent" data-bs-target="#navbarSupportedContent" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<div class="hamburger">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<div class="navbar-brand d-none d-lg-flex">
|
||||
<span class="navbar-logo">
|
||||
<a href="index.html">
|
||||
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
||||
</a>
|
||||
</span>
|
||||
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership Alpha v0.1<br></a></span>
|
||||
</div>
|
||||
<ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM">MINTERSHIP-FORUM</a></li></ul>
|
||||
|
||||
|
||||
<div class="mbr-section-btn-main" role="tablist"><a class="btn btn-danger display-4" href="MINTERSHIP-FORUM">FORUM<br></a> <a class="btn btn-secondary display-4" href="TOOLS">MA TOOLS</a><a class="btn btn-secondary display-4" href="MINTERS">MINTERS</a></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="header1 boldm5 cid-ttRnlSkg2R mbr-fullscreen mbr-parallax-background" id="header1-1">
|
||||
|
||||
<div class="mbr-overlay" style="opacity: 0.4; background-color: rgb(0, 0, 0);">
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="title-wrapper">
|
||||
<h1 class="mbr-section-title mbr-fonts-style display-1">Q-Mintership Alpha</h1>
|
||||
<p class="mbr-text mbr-fonts-style display-7">This is the initial 'alpha' of the Mintership Forum / Mintership tools that will be built into the final Q-Mintership app. This is a simplistic version built by crowetic that will offer a very simple communciations location, and the tools for the minter admins to accomplish the necessary GROUP_APPROVAL transactions. Scroll down for the currently available tools... </p>
|
||||
<div class="mbr-section-btn"><a class="btn btn-primary display-4" href="index.html#features7-6"><span class="mobi-mbri mobi-mbri-arrow-down mbr-iconfont mbr-iconfont-btn"></span>
|
||||
|
||||
See more
|
||||
</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="features7 boldm5 cid-ttRnAijqXt" id="features7-6">
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6 col-md-6 item features-image active">
|
||||
<a class="item-link" href="TOOLS">
|
||||
<div class="item-wrapper">
|
||||
<img src="assets/images/mbr-1623x1082.jpg" alt="MA Tools" data-slide-to="1" data-bs-slide-to="1">
|
||||
<div class="item-content">
|
||||
<h2 class="card-title mbr-fonts-style display-2">
|
||||
Minter Admin Tools</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div><div class="col-12 col-lg-6 col-md-6 item features-image">
|
||||
<a class="item-link" href="MINTERS">
|
||||
<div class="item-wrapper">
|
||||
<img src="assets/images/mbr-1623x1112.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0">
|
||||
<div class="item-content">
|
||||
<h2 class="card-title mbr-fonts-style display-2">New Minters - Start Here</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="features1 boldm5 cid-utzh0dnVQB" id="features1-2">
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card-wrapper" style="justify-content:center; align: center; align-text: center;">
|
||||
<div class="icon-wrapper">
|
||||
<span class="mbr-iconfont mbr-iconfont-btn mbri-file"></span>
|
||||
</div>
|
||||
<h3 class="mbr-section-title mbr-fonts-style display-2">
|
||||
Mintership Details</h3>
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
Learn more about the Mintership concept, and why it was needed. The days of 'sponsorship' are a thing of the past on the Qortal Network. No more will there be the ability to self-sponsor. A new era of Qortal begins! Join the conversation with the other minters and admins here!</p>
|
||||
<div class="icon-wrapper"><a class="btn btn-secondary display-4" style="-ms-flex-align: center;" href="TOOLS">MINTER BOARD</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card-wrapper">
|
||||
<div class="icon-wrapper">
|
||||
<span class="mbr-iconfont mbr-iconfont-btn mbri-key"></span>
|
||||
</div>
|
||||
<h3 class="mbr-section-title mbr-fonts-style display-2">Become A Minter</h3>
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
Not already minting? You've come to the right place to get started. The '<a href="MINTERS">MINTERS</a>.' links will take you to the 'Minter Board'. The Minter Board is a place to publish your intent to become a minter, and get support from the existing minters and Minter Admins.</p>
|
||||
<div class="icon-wrapper"><a class="btn btn-secondary display-4" style="-ms-flex-align: center;" href="TOOLS">MINTER BOARD</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card-wrapper">
|
||||
<div class="icon-wrapper">
|
||||
<span class="mbr-iconfont mbr-iconfont-btn mbri-extension"></span>
|
||||
</div>
|
||||
<h3 class="mbr-section-title mbr-fonts-style display-2">Minter Admin Tools</h3>
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
Are you one of the initially selected Minter Admins? We have the tools here you need to create and approve GROUP_APPROVAL transactions, and communicate securely with your fellow admins. There is a private forum, and Minter Admin Tools section available for you!</p>
|
||||
<div class="icon-wrapper"><a class="btn btn-secondary display-4" style="-ms-flex-align: center;" href="TOOLS">MINTER BOARD</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="content3 boldm5 cid-uu3bTy9Zr1" id="content3-4">
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 card">
|
||||
<div class="title-wrapper">
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-1">
|
||||
More information...</h2>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6">
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-7 card">
|
||||
<div class="title-wrapper">
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-2">
|
||||
This is the beginning...</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-5 card">
|
||||
<div class="text-wrapper">
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
This is the very start of the Q-Mintership app. It will be dramatically changing upon the beta release, and modification to the Q-Mintership Q-App. This initial version is a version that could be launched more quickly, and does not have nearly as much functionality as what will exist once the main app goes live. </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="footer1 visualm5 cid-uhGd4SVHZK" once="footers" id="footer1-1">
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="title-wrapper">
|
||||
<div class="title-wrap">
|
||||
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-5">Q-Mintership Alpha</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="link-wrap" href="#">
|
||||
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.1 Alpha - Release Details</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
|
||||
</div>
|
||||
<div class="col-12">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<script src="assets/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/parallax/jarallax.js"></script>
|
||||
<script src="assets/smoothscroll/smooth-scroll.js"></script>
|
||||
<script src="assets/ytplayer/index.js"></script>
|
||||
<script src="assets/dropdown/js/navbar-dropdown.js"></script>
|
||||
<script src="assets/theme/js/script.js"></script>
|
||||
|
||||
|
||||
<script src="./assets/quill/quill.min.js"></script>
|
||||
<script src="./assets/js/QortalApi.js"></script>
|
||||
<script src="./assets/js/Q-Mintership.js"></script>
|
||||
<script src="./assets/js/AdminTools.js"></script>
|
||||
<input name="animation" type="hidden">
|
||||
</body>
|
||||
</html>
|
@ -1,300 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html >
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<link rel="shortcut icon" href="assets/images/modded-circle-2-new-128x128.png" type="image/x-icon">
|
||||
<meta name="description" content="Welcome to the Mintership Forum (alpha version)">
|
||||
|
||||
|
||||
<title>Home</title>
|
||||
<link rel="stylesheet" href="assets/web/assets/mobirise-icons2/mobirise2.css">
|
||||
<link rel="stylesheet" href="assets/web/assets/mobirise-icons/mobirise-icons.css">
|
||||
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap-grid.min.css">
|
||||
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap-reboot.min.css">
|
||||
<link rel="stylesheet" href="assets/parallax/jarallax.css">
|
||||
<link rel="stylesheet" href="assets/animatecss/animate.css">
|
||||
<link rel="stylesheet" href="assets/dropdown/css/style.css">
|
||||
<link rel="stylesheet" href="assets/socicon/css/styles.css">
|
||||
<link rel="stylesheet" href="assets/theme/css/style.css">
|
||||
<!-- <link rel="preload" href="https://fonts.googleapis.com/css?family=DM+Sans:100,200,300,400,500,600,700,800,900,100i,200i,300i,400i,500i,600i,700i,800i,900i&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=DM+Sans:100,200,300,400,500,600,700,800,900,100i,200i,300i,400i,500i,600i,700i,800i,900i&display=swap"></noscript>
|
||||
<link rel="preload" href="https://fonts.googleapis.com/css?family=Space+Grotesk:300,400,500,600,700&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Space+Grotesk:300,400,500,600,700&display=swap"></noscript> -->
|
||||
<link rel="preload" as="style" href="assets/mobirise/css/mbr-additional.css?v=U9lZDZ"><link rel="stylesheet" href="assets/mobirise/css/mbr-additional.css?v=U9lZDZ" type="text/css">
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="assets/css/forum-styles.css">
|
||||
<link rel="preload" href="assets/css/css.css?family=DM+Sans:100,200,300,400,500,600,700,800,900,100i,200i,300i,400i,500i,600i,700i,800i,900i&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="assets/css/css.css?family=DM+Sans:100,200,300,400,500,600,700,800,900,100i,200i,300i,400i,500i,600i,700i,800i,900i&display=swap"></noscript>
|
||||
<link rel="preload" href="assets/css/space-grotesk.css?family=Space+Grotesk:300,400,500,600,700&display=swap" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="assets/css/space-grotesk.css?family=Space+Grotesk:300,400,500,600,700&display=swap"></noscript>
|
||||
<link href="assets/quill/quill.snow.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section data-bs-version="5.1" class="menu menu1 boldm5 cid-ttRnktJ11Q" once="menu" id="menu1-0">
|
||||
|
||||
<nav class="navbar navbar-dropdown navbar-expand-lg">
|
||||
<div class="menu_box container-fluid">
|
||||
<div class="navbar-brand d-flex d-lg-none">
|
||||
<span class="navbar-logo">
|
||||
<a href="index.html">
|
||||
<img src="assets/images/logo.png" alt="">
|
||||
</a>
|
||||
</span>
|
||||
<span class="navbar-caption-wrap">
|
||||
<a class="navbar-caption display-4" href="index.html">Q-Mintership Alpha
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-bs-toggle="collapse" data-target="#navbarSupportedContent" data-bs-target="#navbarSupportedContent" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<div class="hamburger">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<div class="navbar-brand d-none d-lg-flex">
|
||||
<span class="navbar-logo">
|
||||
<a href="index.html">
|
||||
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
||||
</a>
|
||||
</span>
|
||||
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership Alpha v0.1<br></a></span>
|
||||
</div>
|
||||
<ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM">MINTERSHIP-FORUM</a></li></ul>
|
||||
|
||||
|
||||
<div class="mbr-section-btn-main" role="tablist"><a class="btn btn-danger display-4" href="MINTERSHIP-FORUM">FORUM<br></a> <a class="btn btn-secondary display-4" href="TOOLS">MA TOOLS</a><a class="btn btn-secondary display-4" href="MINTERS">MINTERS</a></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="header1 boldm5 cid-ttRnlSkg2R mbr-fullscreen mbr-parallax-background" id="header1-1">
|
||||
|
||||
<div class="mbr-overlay" style="opacity: 0.4; background-color: rgb(0, 0, 0);">
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="title-wrapper">
|
||||
<h1 class="mbr-section-title mbr-fonts-style display-1">Q-Mintership Alpha</h1>
|
||||
<p class="mbr-text mbr-fonts-style display-7">This is the initial 'alpha' of the Mintership Forum / Mintership tools that will be built into the final Q-Mintership app. This is a simplistic version built by crowetic that will offer a very simple communciations location, and the tools for the minter admins to accomplish the necessary GROUP_APPROVAL transactions. Scroll down for the currently available tools... </p>
|
||||
<div class="mbr-section-btn"><a class="btn btn-primary display-4" href="index.html#features7-6"><span class="mobi-mbri mobi-mbri-arrow-down mbr-iconfont mbr-iconfont-btn"></span>
|
||||
|
||||
See more
|
||||
</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="features7 boldm5 cid-ttRnAijqXt" id="features7-6">
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6 col-md-6 item features-image active">
|
||||
<a class="item-link" href="TOOLS">
|
||||
<div class="item-wrapper">
|
||||
<img src="assets/images/mbr-1623x1082.jpg" alt="MA Tools" data-slide-to="1" data-bs-slide-to="1">
|
||||
<div class="item-content">
|
||||
<h2 class="card-title mbr-fonts-style display-2">
|
||||
Minter Admin Tools</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div><div class="col-12 col-lg-6 col-md-6 item features-image">
|
||||
<a class="item-link" href="MINTERS">
|
||||
<div class="item-wrapper">
|
||||
<img src="assets/images/mbr-1623x1112.jpg" alt="Mintership Forum" data-slide-to="0" data-bs-slide-to="0">
|
||||
<div class="item-content">
|
||||
<h2 class="card-title mbr-fonts-style display-2">New Minters - Start Here</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="features1 boldm5 cid-utzh0dnVQB" id="features1-2">
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card-wrapper" style="justify-content:center; align: center; align-text: center;">
|
||||
<div class="icon-wrapper">
|
||||
<span class="mbr-iconfont mbr-iconfont-btn mbri-file"></span>
|
||||
</div>
|
||||
<h3 class="mbr-section-title mbr-fonts-style display-2">
|
||||
Mintership Details</h3>
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
Learn more about the Mintership concept, and why it was needed. The days of 'sponsorship' are a thing of the past on the Qortal Network. No more will there be the ability to self-sponsor. A new era of Qortal begins!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card-wrapper">
|
||||
<div class="icon-wrapper">
|
||||
<span class="mbr-iconfont mbr-iconfont-btn mbri-key"></span>
|
||||
</div>
|
||||
<h3 class="mbr-section-title mbr-fonts-style display-2">Become A Minter</h3>
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
Not already minting? You've come to the right place to get started. The '<a href="MINTERS">MINTERS</a>.' links will take you to the 'Minter Board'. The Minter Board is a place to publish your intent to become a minter, and get support from the existing minters and Minter Admins.</p>
|
||||
<div class="icon-wrapper"><a class="btn btn-secondary display-4" style="-ms-flex-align: center;" href="TOOLS">MINTER BOARD</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card-wrapper">
|
||||
<div class="icon-wrapper">
|
||||
<span class="mbr-iconfont mbr-iconfont-btn mbri-extension"></span>
|
||||
</div>
|
||||
<h3 class="mbr-section-title mbr-fonts-style display-2">Minter Admin Tools</h3>
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
Are you one of the initially selected Minter Admins? We have the tools here you need to create and approve GROUP_APPROVAL transactions, and communicate securely with your fellow admins.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="content3 boldm5 cid-uu3bTy9Zr1" id="content3-4">
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 card">
|
||||
<div class="title-wrapper">
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-1">
|
||||
More information...</h2>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="features1 boldm5 cid-uufI05uMCB" id="features1-5">
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card-wrapper">
|
||||
<div class="icon-wrapper">
|
||||
<span class="mbr-iconfont mbr-iconfont-btn mbri-info"></span>
|
||||
</div>
|
||||
<h3 class="mbr-section-title mbr-fonts-style display-5">
|
||||
Get Information</h3>
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
Would you like to become a minter? You've come to the right place! Obtain details about the new Mintership-based minting process on Qortal.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card-wrapper">
|
||||
<div class="icon-wrapper">
|
||||
<span class="mbr-iconfont mbr-iconfont-btn mbri-setting"></span>
|
||||
</div>
|
||||
<h3 class="mbr-section-title mbr-fonts-style display-5">
|
||||
Minter Admin Tools</h3>
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
If you are a Minter Admin, you will need to know how to create and sign GROUP_APPROVAL transactions. You may do so here!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card-wrapper">
|
||||
<div class="icon-wrapper">
|
||||
<span class="mbr-iconfont mbr-iconfont-btn mbri-rocket"></span>
|
||||
</div>
|
||||
<h3 class="mbr-section-title mbr-fonts-style display-5">
|
||||
Start your Mission</h3>
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
Every mission has a beginning. If your mission is to become a minter, or a Minter Admin, then you've landed at the correct launchpad!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6">
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-7 card">
|
||||
<div class="title-wrapper">
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-2">
|
||||
This is the beginning...</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-5 card">
|
||||
<div class="text-wrapper">
|
||||
<p class="mbr-text mbr-fonts-style display-7">
|
||||
This is the very start of the Q-Mintership app. It will be dramatically changing upon the beta release, and modification to the Q-Mintership Q-App. This initial version is a version that could be launched more quickly, and does not have nearly as much functionality as what will exist once the main app goes live. </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section data-bs-version="5.1" class="footer1 visualm5 cid-uhGd4SVHZK" once="footers" id="footer1-1">
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="title-wrapper">
|
||||
<div class="title-wrap">
|
||||
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
||||
<h2 class="mbr-section-title mbr-fonts-style display-5">Q-Mintership Alpha</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="link-wrap" href="#">
|
||||
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.1 Alpha - Release Details</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
|
||||
</div>
|
||||
<div class="col-12">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<script src="assets/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/parallax/jarallax.js"></script>
|
||||
<script src="assets/smoothscroll/smooth-scroll.js"></script>
|
||||
<script src="assets/ytplayer/index.js"></script>
|
||||
<script src="assets/dropdown/js/navbar-dropdown.js"></script>
|
||||
<script src="assets/theme/js/script.js"></script>
|
||||
|
||||
|
||||
<script src="./assets/quill/quill.min.js"></script>
|
||||
<script src="./assets/js/QortalApi.js"></script>
|
||||
<script src="./assets/js/Q-Mintership.js"></script>
|
||||
<script src="./assets/js/AdminTools.js"></script>
|
||||
<input name="animation" type="hidden">
|
||||
</body>
|
||||
</html>
|
@ -107,6 +107,14 @@
|
||||
color: white
|
||||
}
|
||||
|
||||
.new-indicator {
|
||||
margin-left: 1.25rem;
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem; /* Adjust size as needed */
|
||||
}
|
||||
|
||||
|
||||
.message-header.username {
|
||||
color: #1b8fc4;
|
||||
}
|
||||
|
@ -73,12 +73,17 @@ async function loadMinterAdminToolsPage() {
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="tools-main mbr-parallax-background cid-ttRnlSkg2R">
|
||||
<div class="tools-header" style="color: lightblue; display: flex; justify-content: center; align-items: center; padding: 10px;">
|
||||
<div> <h1 style="font-size: 50px; margin: 0;">MINTER ADMIN TOOLS for Admin = </h1></div>
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="tools-header" style="color: white; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 10px;">
|
||||
<div> <h1 style="font-size: 50px; margin: 0;">MINTER ADMIN TOOLS </h1><a style="color: red;">Under Construction...</a></div>
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; display: flex; align-items: center; justify-content: center; ">
|
||||
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
|
||||
<span>${userState.accountName || 'Guest'}</span>
|
||||
</div>
|
||||
<div><h2>No Functionality Here Yet</h2></div>
|
||||
<div>
|
||||
<p>This page is still under development. Until the final Mintership proposal modifications are made, and the MINTER group is transferred to null, there is no need for this page's functionality. The page will be updated when the final modifications are made.</p>
|
||||
<p> This page until then is simply a placeholder.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tools-submenu" class="tools-submenu">
|
||||
|
@ -1,157 +0,0 @@
|
||||
let currentMinterToolPage = 'overview'; // Track the current page
|
||||
|
||||
// Load latest state for admin verification
|
||||
async function verifyMinterAdminState() {
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
return minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin)
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const isAdmin = await verifyUserIsAdmin();
|
||||
|
||||
if (isAdmin) {
|
||||
console.log(`User is an Admin, buttons for MA Tools not removed. userState.isAdmin = ${userState.isMinterAdmin}`);
|
||||
} else {
|
||||
// Remove all "TOOLS" links and their related elements
|
||||
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
|
||||
toolsLinks.forEach(link => {
|
||||
// If the link is within a button, remove the button
|
||||
const buttonParent = link.closest('button');
|
||||
if (buttonParent) {
|
||||
buttonParent.remove();
|
||||
}
|
||||
|
||||
// If the link is within an image card or any other element, remove that element
|
||||
const cardParent = link.closest('.item.features-image');
|
||||
if (cardParent) {
|
||||
cardParent.remove();
|
||||
}
|
||||
|
||||
// Finally, remove the link itself if it's not covered by the above removals
|
||||
link.remove();
|
||||
});
|
||||
|
||||
console.log(`User is NOT a Minter Admin, buttons for MA Tools removed. userState.isMinterAdmin = ${userState.isMinterAdmin}`);
|
||||
|
||||
// Center the remaining card if it exists
|
||||
const remainingCard = document.querySelector('.features7 .row .item.features-image');
|
||||
if (remainingCard) {
|
||||
remainingCard.classList.remove('col-lg-6', 'col-md-6');
|
||||
remainingCard.classList.add('col-12', 'text-center');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Add event listener for admin tools link if the user is an admin
|
||||
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
|
||||
toolsLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await loadMinterAdminToolsPage();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
async function loadMinterAdminToolsPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Set the background image directly from a file
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="tools-main tools-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg');">
|
||||
<div class="tools-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<span>MINTER ADMIN TOOLS (Alpha)</span>
|
||||
</div>
|
||||
<div id="tools-content" class="tools-content">
|
||||
<div class="tools-buttons">
|
||||
<button id="display-pending" class="tools-button">Display Pending Approval Transactions</button>
|
||||
<button id="create-group-invite" class="tools-button">Create Pending Group Invite</button>
|
||||
<button id="create-promotion" class="tools-button">Create Pending Promotion</button>
|
||||
</div>
|
||||
<div id="tools-window" class="tools-window"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
addToolsPageEventListeners();
|
||||
}
|
||||
|
||||
|
||||
function addToolsPageEventListeners() {
|
||||
document.getElementById("display-pending").addEventListener("click", async () => {
|
||||
await displayPendingApprovals();
|
||||
});
|
||||
|
||||
document.getElementById("create-group-invite").addEventListener("click", async () => {
|
||||
createPendingGroupInvite();
|
||||
});
|
||||
|
||||
document.getElementById("create-promotion").addEventListener("click", async () => {
|
||||
createPendingPromotion();
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch and display pending approvals
|
||||
async function displayPendingApprovals() {
|
||||
console.log("Fetching pending approval transactions...");
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_TRANSACTIONS",
|
||||
txGroupId: 694,
|
||||
txType: [
|
||||
"ADD_GROUP_ADMIN",
|
||||
"GROUP_INVITE"
|
||||
],
|
||||
confirmationStatus: "UNCONFIRMED",
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
reverse: false
|
||||
});
|
||||
|
||||
console.log("Fetched pending approvals: ", response);
|
||||
|
||||
const toolsWindow = document.getElementById('tools-window');
|
||||
if (response && response.length > 0) {
|
||||
toolsWindow.innerHTML = response.map(tx => `
|
||||
<div class="pending-approval-item" style="border: 1px solid lightblue; padding: 10px; margin-bottom: 10px;">
|
||||
<p><strong>Transaction Type:</strong> ${tx.type}</p>
|
||||
<p><strong>Amount:</strong> ${tx.amount}</p>
|
||||
<p><strong>Creator Address:</strong> ${tx.creatorAddress}</p>
|
||||
<p><strong>Recipient:</strong> ${tx.recipient}</p>
|
||||
<p><strong>Timestamp:</strong> ${new Date(tx.timestamp).toLocaleString()}</p>
|
||||
<button onclick="approveTransaction('${tx.signature}')">Approve</button>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
toolsWindow.innerHTML = '<p>No pending approvals found.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder function to create a pending group invite
|
||||
async function createPendingGroupInvite() {
|
||||
console.log("Creating a pending group invite...");
|
||||
// Placeholder code for creating a pending group invite
|
||||
alert('Pending group invite created (placeholder).');
|
||||
}
|
||||
|
||||
// Placeholder function to create a pending promotion
|
||||
async function createPendingPromotion() {
|
||||
console.log("Creating a pending promotion...");
|
||||
// Placeholder code for creating a pending promotion
|
||||
alert('Pending promotion created (placeholder).');
|
||||
}
|
||||
|
||||
// Placeholder function for approving a transaction
|
||||
function approveTransaction(signature) {
|
||||
console.log("Approving transaction with signature: ", signature);
|
||||
// Placeholder code for approving transaction
|
||||
alert(`Transaction with signature ${signature} approved (placeholder).`);
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
let currentMinterToolPage = 'overview'; // Track the current page
|
||||
|
||||
// Load latest state for admin verification
|
||||
async function verifyMinterAdminState() {
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
return minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin)
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const isAdmin = await verifyUserIsAdmin();
|
||||
|
||||
if (isAdmin) {
|
||||
console.log(`User is an Admin, buttons for MA Tools not removed. userState.isAdmin = ${userState.isMinterAdmin}`);
|
||||
} else {
|
||||
// Remove all "TOOLS" links and their related elements
|
||||
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
|
||||
toolsLinks.forEach(link => {
|
||||
// If the link is within a button, remove the button
|
||||
const buttonParent = link.closest('button');
|
||||
if (buttonParent) {
|
||||
buttonParent.remove();
|
||||
}
|
||||
|
||||
// If the link is within an image card or any other element, remove that element
|
||||
const cardParent = link.closest('.item.features-image');
|
||||
if (cardParent) {
|
||||
cardParent.remove();
|
||||
}
|
||||
|
||||
// Finally, remove the link itself if it's not covered by the above removals
|
||||
link.remove();
|
||||
});
|
||||
|
||||
console.log(`User is NOT a Minter Admin, buttons for MA Tools removed. userState.isMinterAdmin = ${userState.isMinterAdmin}`);
|
||||
|
||||
// Center the remaining card if it exists
|
||||
const remainingCard = document.querySelector('.features7 .row .item.features-image');
|
||||
if (remainingCard) {
|
||||
remainingCard.classList.remove('col-lg-6', 'col-md-6');
|
||||
remainingCard.classList.add('col-12', 'text-center');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Add event listener for admin tools link if the user is an admin
|
||||
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
|
||||
toolsLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await loadMinterAdminToolsPage();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
async function loadMinterAdminToolsPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Set the background image directly from a file
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="tools-main tools-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg');">
|
||||
<div class="tools-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<span>MINTER ADMIN TOOLS (Alpha)</span>
|
||||
</div>
|
||||
<div id="tools-content" class="tools-content">
|
||||
<div class="tools-buttons">
|
||||
<button id="display-pending" class="tools-button">Display Pending Approval Transactions</button>
|
||||
<button id="create-group-invite" class="tools-button">Create Pending Group Invite</button>
|
||||
<button id="create-promotion" class="tools-button">Create Pending Promotion</button>
|
||||
</div>
|
||||
<div id="tools-window" class="tools-window"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
addToolsPageEventListeners();
|
||||
}
|
||||
|
||||
|
||||
function addToolsPageEventListeners() {
|
||||
document.getElementById("display-pending").addEventListener("click", async () => {
|
||||
await displayPendingApprovals();
|
||||
});
|
||||
|
||||
document.getElementById("create-group-invite").addEventListener("click", async () => {
|
||||
createPendingGroupInvite();
|
||||
});
|
||||
|
||||
document.getElementById("create-promotion").addEventListener("click", async () => {
|
||||
createPendingPromotion();
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch and display pending approvals
|
||||
async function displayPendingApprovals() {
|
||||
console.log("Fetching pending approval transactions...");
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_TRANSACTIONS",
|
||||
txGroupId: 694,
|
||||
txType: [
|
||||
"ADD_GROUP_ADMIN",
|
||||
"GROUP_INVITE"
|
||||
],
|
||||
confirmationStatus: "UNCONFIRMED",
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
reverse: false
|
||||
});
|
||||
|
||||
console.log("Fetched pending approvals: ", response);
|
||||
|
||||
const toolsWindow = document.getElementById('tools-window');
|
||||
if (response && response.length > 0) {
|
||||
toolsWindow.innerHTML = response.map(tx => `
|
||||
<div class="message-item" style="border: 1px solid lightblue; padding: 10px; margin-bottom: 10px;">
|
||||
<p><strong>Transaction Type:</strong> ${tx.type}</p>
|
||||
<p><strong>Amount:</strong> ${tx.amount}</p>
|
||||
<p><strong>Creator Address:</strong> ${tx.creatorAddress}</p>
|
||||
<p><strong>Recipient:</strong> ${tx.recipient}</p>
|
||||
<p><strong>Timestamp:</strong> ${new Date(tx.timestamp).toLocaleString()}</p>
|
||||
<button onclick="approveTransaction('${tx.signature}')">Approve</button>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
toolsWindow.innerHTML = '<p>No pending approvals found.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder function to create a pending group invite
|
||||
async function createPendingGroupInvite() {
|
||||
console.log("Creating a pending group invite...");
|
||||
// Placeholder code for creating a pending group invite
|
||||
alert('Pending group invite created (placeholder).');
|
||||
}
|
||||
|
||||
// Placeholder function to create a pending promotion
|
||||
async function createPendingPromotion() {
|
||||
console.log("Creating a pending promotion...");
|
||||
// Placeholder code for creating a pending promotion
|
||||
alert('Pending promotion created (placeholder).');
|
||||
}
|
||||
|
||||
// Placeholder function for approving a transaction
|
||||
function approveTransaction(signature) {
|
||||
console.log("Approving transaction with signature: ", signature);
|
||||
// Placeholder code for approving transaction
|
||||
alert(`Transaction with signature ${signature} approved (placeholder).`);
|
||||
}
|
@ -1,317 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
|
||||
// Load the latest message identifiers from local storage
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
// const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
|
||||
const backgroundImage = "url('/assets/images/background.jpg')"
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "") {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
|
||||
// Create message object with unique identifier and HTML content
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: false,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
replyToMessageIdentifier = null; // Clear reply reference after sending
|
||||
// Clear reply reference after sending if it exists.
|
||||
if (replyToMessageIdentifier) {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
}
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
// Update the latest message identifier
|
||||
latestMessageIdentifiers[room] = messageIdentifier;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
// Reload messages
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for the load more button
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load messages for any given room with pagination
|
||||
async function loadMessagesFromQDN(room, page) {
|
||||
try {
|
||||
const offset = page * 10;
|
||||
const limit = 10;
|
||||
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, offset, limit);
|
||||
|
||||
const qdnMessages = response;
|
||||
console.log("Messages fetched successfully:", qdnMessages);
|
||||
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (messagesContainer) {
|
||||
if (!qdnMessages || !qdnMessages.length) {
|
||||
if (page === 0) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear messages only when loading the first page
|
||||
if (page === 0) {
|
||||
messagesContainer.innerHTML = "";
|
||||
}
|
||||
|
||||
// Fetch all messages
|
||||
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render messages without duplication
|
||||
const existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
fetchMessages.forEach((message) => {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
|
||||
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
}
|
||||
});
|
||||
|
||||
// setTimeout(() => {
|
||||
// messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
// }, 1000);
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Polling function to check for new messages
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
@ -1,317 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
|
||||
// Load the latest message identifiers from local storage
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
// const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
|
||||
const backgroundImage = "url('/assets/images/background.jpg')"
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "") {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
|
||||
// Create message object with unique identifier and HTML content
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: false,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
replyToMessageIdentifier = null; // Clear reply reference after sending
|
||||
// Clear reply reference after sending if it exists.
|
||||
if (replyToMessageIdentifier) {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
}
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
// Update the latest message identifier
|
||||
latestMessageIdentifiers[room] = messageIdentifier;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
// Reload messages
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for the load more button
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load messages for any given room with pagination
|
||||
async function loadMessagesFromQDN(room, page) {
|
||||
try {
|
||||
const offset = page * 10;
|
||||
const limit = 10;
|
||||
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, offset, limit);
|
||||
|
||||
const qdnMessages = response;
|
||||
console.log("Messages fetched successfully:", qdnMessages);
|
||||
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (messagesContainer) {
|
||||
if (!qdnMessages || !qdnMessages.length) {
|
||||
if (page === 0) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear messages only when loading the first page
|
||||
if (page === 0) {
|
||||
messagesContainer.innerHTML = "";
|
||||
}
|
||||
|
||||
// Fetch all messages
|
||||
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render messages without duplication
|
||||
const existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
fetchMessages.forEach((message) => {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
|
||||
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
}
|
||||
});
|
||||
|
||||
// setTimeout(() => {
|
||||
// messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
// }, 1000);
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Polling function to check for new messages
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
@ -1,423 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once.
|
||||
|
||||
// If there is a previous latest message identifiers, use them. Otherwise, use an empty.
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
// const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
// const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${userState.isAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (userState.isAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div id="load-more-container"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<div class="attachment-section">
|
||||
<input type="file" id="file-input" class="file-input" multiple>
|
||||
<button id="attach-button" class="attach-button">Attach Files</button>
|
||||
</div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
let selectedFiles = [];
|
||||
|
||||
// Add event listener to handle file selection
|
||||
document.getElementById('file-input').addEventListener('change', (event) => {
|
||||
selectedFiles = Array.from(event.target.files);
|
||||
});
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "" || selectedFiles.length > 0) {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
let attachmentIdentifiers = [];
|
||||
|
||||
// Handle attachments
|
||||
for (const file of selectedFiles) {
|
||||
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
|
||||
try {
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
file: file,
|
||||
filename: file.name,
|
||||
filetype: file.type,
|
||||
});
|
||||
attachmentIdentifiers.push({
|
||||
identifier: attachmentID,
|
||||
filename: file.name,
|
||||
mimeType: file.type
|
||||
});
|
||||
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
|
||||
} catch (error) {
|
||||
console.error(`Error publishing attachment ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create message object with unique identifier, HTML content, and attachments
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: attachmentIdentifiers.length > 0,
|
||||
attachments: attachmentIdentifiers,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`);
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
|
||||
console.log("Message published successfully");
|
||||
|
||||
// Clear the editor after sending the message, including any potential attached files and replies.
|
||||
quill.root.innerHTML = "";
|
||||
document.getElementById('file-input').value = "";
|
||||
selectedFiles = [];
|
||||
replyToMessageIdentifier = null;
|
||||
const replyContainer = document.querySelector(".reply-container");
|
||||
if (replyContainer) {
|
||||
replyContainer.remove()
|
||||
}
|
||||
|
||||
// Show success notification
|
||||
const notification = document.createElement('div');
|
||||
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
|
||||
notification.style.color = "green";
|
||||
notification.style.marginTop = "10px";
|
||||
document.querySelector(".message-input-section").appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for the load more button
|
||||
const loadMoreContainer = document.getElementById("load-more-container");
|
||||
if (loadMoreContainer) {
|
||||
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load messages for any given room with pagination
|
||||
async function loadMessagesFromQDN(room, page, isPolling = false) {
|
||||
try {
|
||||
// const offset = page * 10;
|
||||
const offset = page * 10;
|
||||
const limit = 20;
|
||||
|
||||
// Get the set of existing identifiers from the messages container
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch only messages that are not already present in the messages container
|
||||
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
|
||||
|
||||
if (messagesContainer) {
|
||||
// If there are no messages and we're not polling, display "no messages" message
|
||||
if (!response || !response.length) {
|
||||
if (page === 0 && !isPolling) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Define `mostRecentMessage` to track the latest message during this fetch
|
||||
let mostRecentMessage = null;
|
||||
|
||||
// Fetch all messages that haven't been fetched before
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return {
|
||||
name: resource.name,
|
||||
content: messageObject.messageHtml,
|
||||
date: formattedTimestamp,
|
||||
identifier: resource.identifier,
|
||||
replyTo: messageObject.replyTo,
|
||||
timestamp,
|
||||
attachments: messageObject.attachments || [] // Include attachments if they exist
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render new messages without duplication
|
||||
for (const message of fetchMessages) {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]?.latestTimestamp);
|
||||
|
||||
let attachmentHtml = "";
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
if (attachment.mimeType.startsWith('image/')) {
|
||||
try {
|
||||
// Fetch the base64 string for the image
|
||||
const image = await fetchFileBase64(attachment.service, attachment.name, attachment.identifier);
|
||||
|
||||
// Create a data URL for the Base64 string
|
||||
const dataUrl = `data:${attachment.mimeType};base64,${image}`;
|
||||
|
||||
// Add the image HTML with the data URL
|
||||
attachmentHtml += `<div class="attachment"><img src="${dataUrl}" alt="${attachment.filename}" class="inline-image"></div>`;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
|
||||
}
|
||||
} else {
|
||||
// Display a button to download other attachments
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<button onclick="fetchAttachment('${attachment.service}', '${message.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
${attachmentHtml}
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Append new message to the end of the container
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
|
||||
// Track the most recent message
|
||||
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.timestamp || 0)) {
|
||||
mostRecentMessage = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update latestMessageIdentifiers for the room
|
||||
if (mostRecentMessage) {
|
||||
latestMessageIdentifiers[room] = {
|
||||
latestIdentifier: mostRecentMessage.identifier,
|
||||
latestTimestamp: mostRecentMessage.timestamp
|
||||
};
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
}
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
const editor = document.querySelector(".ql-editor");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
if (response.length >= limit) {
|
||||
document.getElementById("load-more-container").style.display = 'block';
|
||||
} else {
|
||||
document.getElementById("load-more-container").style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Polling function to check for new messages without clearing existing ones
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage, true);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
||||
|
@ -1,418 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once.
|
||||
|
||||
// If there is a previous latest message identifiers, use them. Otherwise, use an empty.
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
// const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
// const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${userState.isAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (userState.isAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
${(existingIdentifiers.size > 10)? '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>' : ''}
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<div class="attachment-section">
|
||||
<input type="file" id="file-input" class="file-input" multiple>
|
||||
<button id="attach-button" class="attach-button">Attach Files</button>
|
||||
</div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
let selectedFiles = [];
|
||||
|
||||
// Add event listener to handle file selection
|
||||
document.getElementById('file-input').addEventListener('change', (event) => {
|
||||
selectedFiles = Array.from(event.target.files);
|
||||
});
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "" || selectedFiles.length > 0) {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
let attachmentIdentifiers = [];
|
||||
|
||||
// Handle attachments
|
||||
for (const file of selectedFiles) {
|
||||
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
|
||||
try {
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
file: file,
|
||||
filename: file.name,
|
||||
filetype: file.type,
|
||||
});
|
||||
attachmentIdentifiers.push({
|
||||
identifier: attachmentID,
|
||||
filename: file.name,
|
||||
mimeType: file.type
|
||||
});
|
||||
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
|
||||
} catch (error) {
|
||||
console.error(`Error publishing attachment ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create message object with unique identifier, HTML content, and attachments
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: attachmentIdentifiers.length > 0,
|
||||
attachments: attachmentIdentifiers,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`);
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
|
||||
console.log("Message published successfully");
|
||||
|
||||
// Clear the editor after sending the message, including any potential attached files and replies.
|
||||
quill.root.innerHTML = "";
|
||||
document.getElementById('file-input').value = "";
|
||||
selectedFiles = [];
|
||||
replyToMessageIdentifier = null;
|
||||
const replyContainer = document.querySelector(".reply-container");
|
||||
if (replyContainer) {
|
||||
replyContainer.remove()
|
||||
}
|
||||
// Update the latest message identifier - DO NOT DO THIS ON PUBLISH, OR MESSAGE WILL NOT BE LOADED CORRECTLY.
|
||||
// latestMessageIdentifiers[room] = messageIdentifier;
|
||||
// localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
|
||||
// Show success notification
|
||||
const notification = document.createElement('div');
|
||||
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
|
||||
notification.style.color = "green";
|
||||
notification.style.marginTop = "10px";
|
||||
document.querySelector(".message-input-section").appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for the load more button
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load messages for any given room with pagination
|
||||
async function loadMessagesFromQDN(room, page, isPolling = false) {
|
||||
try {
|
||||
// const offset = page * 10;
|
||||
const offset = 0;
|
||||
const limit = 0;
|
||||
|
||||
// Get the set of existing identifiers from the messages container
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch only messages that are not already present in the messages container
|
||||
const response = await searchAllWithoutDuplicates(`${messageIdentifierPrefix}-${room}`, limit, offset, existingIdentifiers);
|
||||
|
||||
if (messagesContainer) {
|
||||
// If there are no messages and we're not polling, display "no messages" message
|
||||
if (!response || !response.length) {
|
||||
if (page === 0 && !isPolling) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Define `mostRecentMessage` to track the latest message during this fetch
|
||||
let mostRecentMessage = null;
|
||||
|
||||
// Fetch all messages that haven't been fetched before
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return {
|
||||
name: resource.name,
|
||||
content: messageObject.messageHtml,
|
||||
date: formattedTimestamp,
|
||||
identifier: resource.identifier,
|
||||
replyTo: messageObject.replyTo,
|
||||
timestamp,
|
||||
attachments: messageObject.attachments || [] // Include attachments if they exist
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render new messages without duplication
|
||||
for (const message of fetchMessages) {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]?.latestTimestamp);
|
||||
|
||||
let attachmentHtml = "";
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
if (attachment.mimeType.startsWith('image/')) {
|
||||
try {
|
||||
// Fetch the base64 string for the image
|
||||
const image = await fetchFileBase64(attachment.service, attachment.name, attachment.identifier);
|
||||
|
||||
// Create a data URL for the Base64 string
|
||||
const dataUrl = `data:${attachment.mimeType};base64,${image}`;
|
||||
|
||||
// Add the image HTML with the data URL
|
||||
attachmentHtml += `<div class="attachment"><img src="${dataUrl}" alt="${attachment.filename}" class="inline-image"></div>`;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
|
||||
}
|
||||
} else {
|
||||
// Display a button to download other attachments
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<button onclick="fetchAttachment('${attachment.service}', '${message.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
${attachmentHtml}
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Append new message to the end of the container
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
|
||||
// Track the most recent message
|
||||
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.timestamp || 0)) {
|
||||
mostRecentMessage = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update latestMessageIdentifiers for the room
|
||||
if (mostRecentMessage) {
|
||||
latestMessageIdentifiers[room] = {
|
||||
latestIdentifier: mostRecentMessage.identifier,
|
||||
latestTimestamp: mostRecentMessage.timestamp
|
||||
};
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
}
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
const editor = document.querySelector(".ql-editor");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Polling function to check for new messages without clearing existing ones
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage, true);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
||||
|
@ -1,505 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
|
||||
|
||||
// NOTE - SET adminGroups in QortalApi.js to enable admin access to forum for specific groups. Minter Admins will be fetched automatically.
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once.
|
||||
|
||||
// If there is a previous latest message identifiers, use them. Otherwise, use an empty.
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
//login if not already logged in.
|
||||
if (!userState.isLoggedIn) {
|
||||
await login();
|
||||
}
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// // Remove all sections except the menu
|
||||
// const allSections = document.querySelectorAll('body > section');
|
||||
// allSections.forEach(section => {
|
||||
// if (!section.classList.contains('menu')) {
|
||||
// section.remove();
|
||||
// }
|
||||
// });
|
||||
const bodyChildren = document.body.children;
|
||||
for (let i = bodyChildren.length - 1; i >= 0; i--) {
|
||||
const child = bodyChildren[i];
|
||||
if (!child.classList.contains('menu')) {
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`;
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background mbr-fullscreen cid-ttRnlSkg2R" style="background-image: url('./assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: center; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; display: flex; align-items: center; justify-content: center;">
|
||||
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
|
||||
<span>${userState.accountName || 'Guest'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${userState.isAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (userState.isAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
async function renderPaginationControls(room, totalMessages, limit) {
|
||||
const paginationContainer = document.getElementById("pagination-container");
|
||||
if (!paginationContainer) return;
|
||||
|
||||
paginationContainer.innerHTML = ""; // Clear existing buttons
|
||||
|
||||
const totalPages = Math.ceil(totalMessages / limit);
|
||||
|
||||
// Add "Previous" button
|
||||
if (currentPage > 0) {
|
||||
const prevButton = document.createElement("button");
|
||||
prevButton.innerText = "Previous";
|
||||
prevButton.addEventListener("click", () => {
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
loadMessagesFromQDN(room, currentPage, false);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(prevButton);
|
||||
}
|
||||
|
||||
// Add numbered page buttons
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const pageButton = document.createElement("button");
|
||||
pageButton.innerText = i + 1;
|
||||
pageButton.className = i === currentPage ? "active-page" : "";
|
||||
pageButton.addEventListener("click", () => {
|
||||
if (i !== currentPage) {
|
||||
currentPage = i;
|
||||
loadMessagesFromQDN(room, currentPage, false);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(pageButton);
|
||||
}
|
||||
|
||||
// Add "Next" button
|
||||
if (currentPage < totalPages - 1) {
|
||||
const nextButton = document.createElement("button");
|
||||
nextButton.innerText = "Next";
|
||||
nextButton.addEventListener("click", () => {
|
||||
if (currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage, false);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(nextButton);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div id="pagination-container" class="pagination-container" style="margin-top: 20px; text-align: center;"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<div class="attachment-section">
|
||||
<input type="file" id="file-input" class="file-input" multiple>
|
||||
<button id="attach-button" class="attach-button">Attach Files</button>
|
||||
</div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
await loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
let selectedFiles = [];
|
||||
|
||||
// Add event listener to handle file selection
|
||||
document.getElementById('file-input').addEventListener('change', (event) => {
|
||||
selectedFiles = Array.from(event.target.files);
|
||||
});
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "" || selectedFiles.length > 0) {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
let attachmentIdentifiers = [];
|
||||
|
||||
// Handle attachments
|
||||
for (const file of selectedFiles) {
|
||||
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
|
||||
try {
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
file: file,
|
||||
});
|
||||
attachmentIdentifiers.push({
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
filename: file.name,
|
||||
mimeType: file.type
|
||||
});
|
||||
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
|
||||
} catch (error) {
|
||||
console.error(`Error publishing attachment ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create message object with unique identifier, HTML content, and attachments
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: attachmentIdentifiers.length > 0,
|
||||
attachments: attachmentIdentifiers,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`);
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
|
||||
console.log("Message published successfully");
|
||||
|
||||
// Clear the editor after sending the message, including any potential attached files and replies.
|
||||
quill.root.innerHTML = "";
|
||||
document.getElementById('file-input').value = "";
|
||||
selectedFiles = [];
|
||||
replyToMessageIdentifier = null;
|
||||
const replyContainer = document.querySelector(".reply-container");
|
||||
if (replyContainer) {
|
||||
replyContainer.remove()
|
||||
}
|
||||
|
||||
// Show success notification
|
||||
const notification = document.createElement('div');
|
||||
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
|
||||
notification.style.color = "green";
|
||||
notification.style.marginTop = "10px";
|
||||
document.querySelector(".message-input-section").appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Add event listener for the load more button
|
||||
const loadMoreContainer = document.getElementById("load-more-container");
|
||||
if (loadMoreContainer) {
|
||||
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMessagesFromQDN(room, page, isPolling = false) {
|
||||
try {
|
||||
const limit = 10;
|
||||
const offset = page * limit;
|
||||
console.log(`Loading messages for room: ${room}, page: ${page}, offset: ${offset}, limit: ${limit}`);
|
||||
|
||||
// Get the messages container
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (!messagesContainer) return;
|
||||
|
||||
// If not polling, clear the message container and the existing identifiers for a fresh load
|
||||
if (!isPolling) {
|
||||
messagesContainer.innerHTML = ""; // Clear the messages container before loading new page
|
||||
existingIdentifiers.clear(); // Clear the existing identifiers set for fresh page load
|
||||
}
|
||||
|
||||
// Get the set of existing identifiers from the messages container
|
||||
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch messages for the current room and page
|
||||
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
|
||||
console.log(`Fetched messages count: ${response.length} for page: ${page}`);
|
||||
|
||||
if (response.length === 0) {
|
||||
// If no messages are fetched and it's not polling, display "no messages" for the initial load
|
||||
if (page === 0 && !isPolling) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Define `mostRecentMessage` to track the latest message during this fetch
|
||||
let mostRecentMessage = latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
|
||||
|
||||
// Fetch all messages that haven't been fetched before
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
if (existingIdentifiers.has(resource.identifier)) {
|
||||
return null; // Skip messages that are already displayed
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return {
|
||||
name: resource.name,
|
||||
content: messageObject.messageHtml,
|
||||
date: formattedTimestamp,
|
||||
identifier: resource.identifier,
|
||||
replyTo: messageObject.replyTo,
|
||||
timestamp,
|
||||
attachments: messageObject.attachments || [] // Include attachments if they exist
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render new messages without duplication
|
||||
for (const message of fetchMessages) {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
|
||||
|
||||
let attachmentHtml = "";
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
if (attachment.mimeType.startsWith('image/')) {
|
||||
try {
|
||||
// OTHER METHOD NOT BEING USED HERE. WE CAN LOAD THE IMAGE DIRECTLY SINCE IT WILL BE PUBLISHED UNENCRYPTED/UNENCODED.
|
||||
// const imageHtml = await loadImageHtml(attachment.service, attachment.name, attachment.identifier, attachment.filename, attachment.mimeType);
|
||||
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
|
||||
|
||||
// Add the image HTML with the direct URL
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image" style="max-width: 30%; height: auto;"/>
|
||||
</div>`;
|
||||
// FOR OTHER METHOD NO LONGER USED
|
||||
// attachmentHtml += imageHtml;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
|
||||
}
|
||||
} else {
|
||||
// Display a button to download other attachments
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
<div class="message-header" style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<img src="${avatarUrl}" alt="Avatar" class="user-avatar" style="width: 30px; height: 30px; border-radius: 50%; margin-right: 10px;">
|
||||
<span class="username">${message.name}</span>
|
||||
</div>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
</div>
|
||||
${replyHtml}
|
||||
${attachmentHtml}
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Append new message to the end of the container
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
|
||||
// Update mostRecentMessage if this message is newer
|
||||
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp || 0)) {
|
||||
mostRecentMessage = {
|
||||
latestIdentifier: message.identifier,
|
||||
latestTimestamp: message.timestamp
|
||||
};
|
||||
}
|
||||
|
||||
// Add the identifier to the existingIdentifiers set
|
||||
existingIdentifiers.add(message.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// Update latestMessageIdentifiers for the room
|
||||
if (mostRecentMessage) {
|
||||
latestMessageIdentifiers[room] = mostRecentMessage;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
}
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
const editor = document.querySelector(".ql-editor");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Render pagination controls
|
||||
const totalMessages = await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`);
|
||||
renderPaginationControls(room, totalMessages, limit);
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Polling function to check for new messages without clearing existing ones
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage, true);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
||||
|
@ -1,485 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
|
||||
|
||||
// NOTE - SET adminGroups in QortalApi.js to enable admin access to forum for specific groups. Minter Admins will be fetched automatically.
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once.
|
||||
|
||||
// If there is a previous latest message identifiers, use them. Otherwise, use an empty.
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
// const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
// const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${userState.isAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (userState.isAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
async function renderPaginationControls(room, totalMessages, limit) {
|
||||
const paginationContainer = document.getElementById("pagination-container");
|
||||
if (!paginationContainer) return;
|
||||
|
||||
paginationContainer.innerHTML = ""; // Clear existing buttons
|
||||
|
||||
const totalPages = Math.ceil(totalMessages / limit);
|
||||
|
||||
// Add "Previous" button
|
||||
if (currentPage > 0) {
|
||||
const prevButton = document.createElement("button");
|
||||
prevButton.innerText = "Previous";
|
||||
prevButton.addEventListener("click", () => {
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(prevButton);
|
||||
}
|
||||
|
||||
// Add numbered page buttons
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const pageButton = document.createElement("button");
|
||||
pageButton.innerText = i + 1;
|
||||
pageButton.className = i === currentPage ? "active-page" : "";
|
||||
pageButton.addEventListener("click", () => {
|
||||
if (i !== currentPage) {
|
||||
currentPage = i;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(pageButton);
|
||||
}
|
||||
|
||||
// Add "Next" button
|
||||
if (currentPage < totalPages - 1) {
|
||||
const nextButton = document.createElement("button");
|
||||
nextButton.innerText = "Next";
|
||||
nextButton.addEventListener("click", () => {
|
||||
if (currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(nextButton);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div id="pagination-container" class="pagination-container" style="margin-top: 20px; text-align: center;"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<div class="attachment-section">
|
||||
<input type="file" id="file-input" class="file-input" multiple>
|
||||
<button id="attach-button" class="attach-button">Attach Files</button>
|
||||
</div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
let selectedFiles = [];
|
||||
|
||||
// Add event listener to handle file selection
|
||||
document.getElementById('file-input').addEventListener('change', (event) => {
|
||||
selectedFiles = Array.from(event.target.files);
|
||||
});
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "" || selectedFiles.length > 0) {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
let attachmentIdentifiers = [];
|
||||
|
||||
// Handle attachments
|
||||
for (const file of selectedFiles) {
|
||||
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
|
||||
try {
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
file: file,
|
||||
filename: file.name,
|
||||
filetype: file.type,
|
||||
});
|
||||
attachmentIdentifiers.push({
|
||||
identifier: attachmentID,
|
||||
filename: file.name,
|
||||
mimeType: file.type
|
||||
});
|
||||
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
|
||||
} catch (error) {
|
||||
console.error(`Error publishing attachment ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create message object with unique identifier, HTML content, and attachments
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: attachmentIdentifiers.length > 0,
|
||||
attachments: attachmentIdentifiers,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`);
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
|
||||
console.log("Message published successfully");
|
||||
|
||||
// Clear the editor after sending the message, including any potential attached files and replies.
|
||||
quill.root.innerHTML = "";
|
||||
document.getElementById('file-input').value = "";
|
||||
selectedFiles = [];
|
||||
replyToMessageIdentifier = null;
|
||||
const replyContainer = document.querySelector(".reply-container");
|
||||
if (replyContainer) {
|
||||
replyContainer.remove()
|
||||
}
|
||||
|
||||
// Show success notification
|
||||
const notification = document.createElement('div');
|
||||
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
|
||||
notification.style.color = "green";
|
||||
notification.style.marginTop = "10px";
|
||||
document.querySelector(".message-input-section").appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for the load more button
|
||||
const loadMoreContainer = document.getElementById("load-more-container");
|
||||
if (loadMoreContainer) {
|
||||
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load messages for any given room with pagination
|
||||
async function loadMessagesFromQDN(room, page, isPolling = false) {
|
||||
try {
|
||||
// const offset = page * 10;
|
||||
|
||||
const limit = 10;
|
||||
const offset = page * limit;
|
||||
console.log(`Loading messages for room: ${room}, page: ${page}, offset: ${offset}, limit: ${limit}`);
|
||||
|
||||
// Get the set of existing identifiers from the messages container
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch only messages that are not already present in the messages container
|
||||
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
|
||||
console.log(`Fetched messages count: ${response.length} for page: ${page}`);
|
||||
|
||||
if (messagesContainer) {
|
||||
// If there are no messages and we're not polling, display "no messages" message
|
||||
if (!response || !response.length) {
|
||||
if (page === 0 && !isPolling) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPolling) {
|
||||
messagesContainer.innerHTML = "";
|
||||
}
|
||||
|
||||
// Define `mostRecentMessage` to track the latest message during this fetch
|
||||
let mostRecentMessage = null;
|
||||
|
||||
// Fetch all messages that haven't been fetched before
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return {
|
||||
name: resource.name,
|
||||
content: messageObject.messageHtml,
|
||||
date: formattedTimestamp,
|
||||
identifier: resource.identifier,
|
||||
replyTo: messageObject.replyTo,
|
||||
timestamp,
|
||||
attachments: messageObject.attachments || [] // Include attachments if they exist
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render new messages without duplication
|
||||
for (const message of fetchMessages) {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]?.latestTimestamp);
|
||||
|
||||
let attachmentHtml = "";
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
if (attachment.mimeType.startsWith('image/')) {
|
||||
try {
|
||||
// Fetch the base64 string for the image
|
||||
const imageHtml = await loadImageHtml(attachment.service, attachment.name, attachment.identifier);
|
||||
|
||||
// Create a data URL for the Base64 string - THIS CAN BE USED IF IMAGES ARE POSTED IN BASE64, ALONG WITH obtain
|
||||
//const dataUrl = `data:${attachment.mimeType};base64,${image}`;
|
||||
|
||||
// Add the image HTML with the data URL
|
||||
attachmentHtml += imageHtml;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
|
||||
}
|
||||
} else {
|
||||
// Display a button to download other attachments
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${message.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
${attachmentHtml}
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Append new message to the end of the container
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
|
||||
// Track the most recent message
|
||||
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.timestamp || 0)) {
|
||||
mostRecentMessage = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update latestMessageIdentifiers for the room
|
||||
if (mostRecentMessage) {
|
||||
latestMessageIdentifiers[room] = {
|
||||
latestIdentifier: mostRecentMessage.identifier,
|
||||
latestTimestamp: mostRecentMessage.timestamp
|
||||
};
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
}
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
const editor = document.querySelector(".ql-editor");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// if (response.length >= limit) {
|
||||
// document.getElementById("load-more-container").style.display = 'block';
|
||||
// } else {
|
||||
// document.getElementById("load-more-container").style.display = 'none';
|
||||
// }
|
||||
|
||||
const totalMessages = await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`);
|
||||
renderPaginationControls(room, totalMessages, limit);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Polling function to check for new messages without clearing existing ones
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage, true);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
||||
|
@ -1,494 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
|
||||
|
||||
// NOTE - SET adminGroups in QortalApi.js to enable admin access to forum for specific groups. Minter Admins will be fetched automatically.
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once.
|
||||
|
||||
// If there is a previous latest message identifiers, use them. Otherwise, use an empty.
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`;
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; display: flex; align-items: center; justify-content: center;">
|
||||
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
|
||||
<span>User: ${userState.accountName || 'Guest'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${userState.isAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (userState.isAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
async function renderPaginationControls(room, totalMessages, limit) {
|
||||
const paginationContainer = document.getElementById("pagination-container");
|
||||
if (!paginationContainer) return;
|
||||
|
||||
paginationContainer.innerHTML = ""; // Clear existing buttons
|
||||
|
||||
const totalPages = Math.ceil(totalMessages / limit);
|
||||
|
||||
// Add "Previous" button
|
||||
if (currentPage > 0) {
|
||||
const prevButton = document.createElement("button");
|
||||
prevButton.innerText = "Previous";
|
||||
prevButton.addEventListener("click", () => {
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
loadMessagesFromQDN(room, currentPage, false);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(prevButton);
|
||||
}
|
||||
|
||||
// Add numbered page buttons
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const pageButton = document.createElement("button");
|
||||
pageButton.innerText = i + 1;
|
||||
pageButton.className = i === currentPage ? "active-page" : "";
|
||||
pageButton.addEventListener("click", () => {
|
||||
if (i !== currentPage) {
|
||||
currentPage = i;
|
||||
loadMessagesFromQDN(room, currentPage, false);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(pageButton);
|
||||
}
|
||||
|
||||
// Add "Next" button
|
||||
if (currentPage < totalPages - 1) {
|
||||
const nextButton = document.createElement("button");
|
||||
nextButton.innerText = "Next";
|
||||
nextButton.addEventListener("click", () => {
|
||||
if (currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage, false);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(nextButton);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div id="pagination-container" class="pagination-container" style="margin-top: 20px; text-align: center;"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<div class="attachment-section">
|
||||
<input type="file" id="file-input" class="file-input" multiple>
|
||||
<button id="attach-button" class="attach-button">Attach Files</button>
|
||||
</div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
let selectedFiles = [];
|
||||
|
||||
// Add event listener to handle file selection
|
||||
document.getElementById('file-input').addEventListener('change', (event) => {
|
||||
selectedFiles = Array.from(event.target.files);
|
||||
});
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "" || selectedFiles.length > 0) {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
let attachmentIdentifiers = [];
|
||||
|
||||
// Handle attachments
|
||||
for (const file of selectedFiles) {
|
||||
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
|
||||
try {
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
file: file,
|
||||
});
|
||||
attachmentIdentifiers.push({
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
filename: file.name,
|
||||
mimeType: file.type
|
||||
});
|
||||
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
|
||||
} catch (error) {
|
||||
console.error(`Error publishing attachment ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create message object with unique identifier, HTML content, and attachments
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: attachmentIdentifiers.length > 0,
|
||||
attachments: attachmentIdentifiers,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`);
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
|
||||
console.log("Message published successfully");
|
||||
|
||||
// Clear the editor after sending the message, including any potential attached files and replies.
|
||||
quill.root.innerHTML = "";
|
||||
document.getElementById('file-input').value = "";
|
||||
selectedFiles = [];
|
||||
replyToMessageIdentifier = null;
|
||||
const replyContainer = document.querySelector(".reply-container");
|
||||
if (replyContainer) {
|
||||
replyContainer.remove()
|
||||
}
|
||||
|
||||
// Show success notification
|
||||
const notification = document.createElement('div');
|
||||
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
|
||||
notification.style.color = "green";
|
||||
notification.style.marginTop = "10px";
|
||||
document.querySelector(".message-input-section").appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Add event listener for the load more button
|
||||
const loadMoreContainer = document.getElementById("load-more-container");
|
||||
if (loadMoreContainer) {
|
||||
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMessagesFromQDN(room, page, isPolling = false) {
|
||||
try {
|
||||
const limit = 10;
|
||||
const offset = page * limit;
|
||||
console.log(`Loading messages for room: ${room}, page: ${page}, offset: ${offset}, limit: ${limit}`);
|
||||
|
||||
// Get the messages container
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (!messagesContainer) return;
|
||||
|
||||
// If not polling, clear the message container and the existing identifiers for a fresh load
|
||||
if (!isPolling) {
|
||||
messagesContainer.innerHTML = ""; // Clear the messages container before loading new page
|
||||
existingIdentifiers.clear(); // Clear the existing identifiers set for fresh page load
|
||||
}
|
||||
|
||||
// Get the set of existing identifiers from the messages container
|
||||
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch messages for the current room and page
|
||||
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
|
||||
console.log(`Fetched messages count: ${response.length} for page: ${page}`);
|
||||
|
||||
if (response.length === 0) {
|
||||
// If no messages are fetched and it's not polling, display "no messages" for the initial load
|
||||
if (page === 0 && !isPolling) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Define `mostRecentMessage` to track the latest message during this fetch
|
||||
let mostRecentMessage = latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
|
||||
|
||||
// Fetch all messages that haven't been fetched before
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
if (existingIdentifiers.has(resource.identifier)) {
|
||||
return null; // Skip messages that are already displayed
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return {
|
||||
name: resource.name,
|
||||
content: messageObject.messageHtml,
|
||||
date: formattedTimestamp,
|
||||
identifier: resource.identifier,
|
||||
replyTo: messageObject.replyTo,
|
||||
timestamp,
|
||||
attachments: messageObject.attachments || [] // Include attachments if they exist
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render new messages without duplication
|
||||
for (const message of fetchMessages) {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
|
||||
|
||||
let attachmentHtml = "";
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
if (attachment.mimeType.startsWith('image/')) {
|
||||
try {
|
||||
// OTHER METHOD NOT BEING USED HERE. WE CAN LOAD THE IMAGE DIRECTLY SINCE IT WILL BE PUBLISHED UNENCRYPTED/UNENCODED.
|
||||
// const imageHtml = await loadImageHtml(attachment.service, attachment.name, attachment.identifier, attachment.filename, attachment.mimeType);
|
||||
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
|
||||
|
||||
// Add the image HTML with the direct URL
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image" style="max-width: 100%; height: auto;"/>
|
||||
</div>`;
|
||||
// FOR OTHER METHOD NO LONGER USED
|
||||
// attachmentHtml += imageHtml;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
|
||||
}
|
||||
} else {
|
||||
// Display a button to download other attachments
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header" style="display: flex; align-items: center;">
|
||||
<img src="${avatarUrl}" alt="${message.name}'s Avatar" class="user-avatar" style="width: 40px; height: 40px; border-radius: 50%; margin-right: 10px;">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
${attachmentHtml}
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Append new message to the end of the container
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
|
||||
// Update mostRecentMessage if this message is newer
|
||||
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp || 0)) {
|
||||
mostRecentMessage = {
|
||||
latestIdentifier: message.identifier,
|
||||
latestTimestamp: message.timestamp
|
||||
};
|
||||
}
|
||||
|
||||
// Add the identifier to the existingIdentifiers set
|
||||
existingIdentifiers.add(message.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// Update latestMessageIdentifiers for the room
|
||||
if (mostRecentMessage) {
|
||||
latestMessageIdentifiers[room] = mostRecentMessage;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
}
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
const editor = document.querySelector(".ql-editor");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Render pagination controls
|
||||
const totalMessages = await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`);
|
||||
renderPaginationControls(room, totalMessages, limit);
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Polling function to check for new messages without clearing existing ones
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage, true);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
||||
|
@ -1,491 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
|
||||
|
||||
// NOTE - SET adminGroups in QortalApi.js to enable admin access to forum for specific groups. Minter Admins will be fetched automatically.
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once.
|
||||
|
||||
// If there is a previous latest message identifiers, use them. Otherwise, use an empty.
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
// const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
// const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; align-items: center; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${userState.isAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (userState.isAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
async function renderPaginationControls(room, totalMessages, limit) {
|
||||
const paginationContainer = document.getElementById("pagination-container");
|
||||
if (!paginationContainer) return;
|
||||
|
||||
paginationContainer.innerHTML = ""; // Clear existing buttons
|
||||
|
||||
const totalPages = Math.ceil(totalMessages / limit);
|
||||
|
||||
// Add "Previous" button
|
||||
if (currentPage > 0) {
|
||||
const prevButton = document.createElement("button");
|
||||
prevButton.innerText = "Previous";
|
||||
prevButton.addEventListener("click", () => {
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
loadMessagesFromQDN(room, currentPage, false);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(prevButton);
|
||||
}
|
||||
|
||||
// Add numbered page buttons
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const pageButton = document.createElement("button");
|
||||
pageButton.innerText = i + 1;
|
||||
pageButton.className = i === currentPage ? "active-page" : "";
|
||||
pageButton.addEventListener("click", () => {
|
||||
if (i !== currentPage) {
|
||||
currentPage = i;
|
||||
loadMessagesFromQDN(room, currentPage, false);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(pageButton);
|
||||
}
|
||||
|
||||
// Add "Next" button
|
||||
if (currentPage < totalPages - 1) {
|
||||
const nextButton = document.createElement("button");
|
||||
nextButton.innerText = "Next";
|
||||
nextButton.addEventListener("click", () => {
|
||||
if (currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage, false);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(nextButton);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div id="pagination-container" class="pagination-container" style="margin-top: 20px; text-align: center;"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<div class="attachment-section">
|
||||
<input type="file" id="file-input" class="file-input" multiple>
|
||||
<button id="attach-button" class="attach-button">Attach Files</button>
|
||||
</div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
let selectedFiles = [];
|
||||
|
||||
// Add event listener to handle file selection
|
||||
document.getElementById('file-input').addEventListener('change', (event) => {
|
||||
selectedFiles = Array.from(event.target.files);
|
||||
});
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "" || selectedFiles.length > 0) {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
let attachmentIdentifiers = [];
|
||||
|
||||
// Handle attachments
|
||||
for (const file of selectedFiles) {
|
||||
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
|
||||
try {
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
file: file,
|
||||
});
|
||||
attachmentIdentifiers.push({
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
filename: file.name,
|
||||
mimeType: file.type
|
||||
});
|
||||
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
|
||||
} catch (error) {
|
||||
console.error(`Error publishing attachment ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create message object with unique identifier, HTML content, and attachments
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: attachmentIdentifiers.length > 0,
|
||||
attachments: attachmentIdentifiers,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`);
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
|
||||
console.log("Message published successfully");
|
||||
|
||||
// Clear the editor after sending the message, including any potential attached files and replies.
|
||||
quill.root.innerHTML = "";
|
||||
document.getElementById('file-input').value = "";
|
||||
selectedFiles = [];
|
||||
replyToMessageIdentifier = null;
|
||||
const replyContainer = document.querySelector(".reply-container");
|
||||
if (replyContainer) {
|
||||
replyContainer.remove()
|
||||
}
|
||||
|
||||
// Show success notification
|
||||
const notification = document.createElement('div');
|
||||
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
|
||||
notification.style.color = "green";
|
||||
notification.style.marginTop = "10px";
|
||||
document.querySelector(".message-input-section").appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Add event listener for the load more button
|
||||
const loadMoreContainer = document.getElementById("load-more-container");
|
||||
if (loadMoreContainer) {
|
||||
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMessagesFromQDN(room, page, isPolling = false) {
|
||||
try {
|
||||
const limit = 10;
|
||||
const offset = page * limit;
|
||||
console.log(`Loading messages for room: ${room}, page: ${page}, offset: ${offset}, limit: ${limit}`);
|
||||
|
||||
// Get the messages container
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (!messagesContainer) return;
|
||||
|
||||
// If not polling, clear the message container and the existing identifiers for a fresh load
|
||||
if (!isPolling) {
|
||||
messagesContainer.innerHTML = ""; // Clear the messages container before loading new page
|
||||
existingIdentifiers.clear(); // Clear the existing identifiers set for fresh page load
|
||||
}
|
||||
|
||||
// Get the set of existing identifiers from the messages container
|
||||
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch messages for the current room and page
|
||||
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
|
||||
console.log(`Fetched messages count: ${response.length} for page: ${page}`);
|
||||
|
||||
if (response.length === 0) {
|
||||
// If no messages are fetched and it's not polling, display "no messages" for the initial load
|
||||
if (page === 0 && !isPolling) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Define `mostRecentMessage` to track the latest message during this fetch
|
||||
let mostRecentMessage = latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
|
||||
|
||||
// Fetch all messages that haven't been fetched before
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
if (existingIdentifiers.has(resource.identifier)) {
|
||||
return null; // Skip messages that are already displayed
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return {
|
||||
name: resource.name,
|
||||
content: messageObject.messageHtml,
|
||||
date: formattedTimestamp,
|
||||
identifier: resource.identifier,
|
||||
replyTo: messageObject.replyTo,
|
||||
timestamp,
|
||||
attachments: messageObject.attachments || [] // Include attachments if they exist
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render new messages without duplication
|
||||
for (const message of fetchMessages) {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
|
||||
|
||||
let attachmentHtml = "";
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
if (attachment.mimeType.startsWith('image/')) {
|
||||
try {
|
||||
// OTHER METHOD NOT BEING USED HERE. WE CAN LOAD THE IMAGE DIRECTLY SINCE IT WILL BE PUBLISHED UNENCRYPTED/UNENCODED.
|
||||
// const imageHtml = await loadImageHtml(attachment.service, attachment.name, attachment.identifier, attachment.filename, attachment.mimeType);
|
||||
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
|
||||
|
||||
// Add the image HTML with the direct URL
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image" style="max-width: 100%; height: auto;"/>
|
||||
</div>`;
|
||||
// FOR OTHER METHOD NO LONGER USED
|
||||
// attachmentHtml += imageHtml;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
|
||||
}
|
||||
} else {
|
||||
// Display a button to download other attachments
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
${attachmentHtml}
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Append new message to the end of the container
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
|
||||
// Update mostRecentMessage if this message is newer
|
||||
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp || 0)) {
|
||||
mostRecentMessage = {
|
||||
latestIdentifier: message.identifier,
|
||||
latestTimestamp: message.timestamp
|
||||
};
|
||||
}
|
||||
|
||||
// Add the identifier to the existingIdentifiers set
|
||||
existingIdentifiers.add(message.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// Update latestMessageIdentifiers for the room
|
||||
if (mostRecentMessage) {
|
||||
latestMessageIdentifiers[room] = mostRecentMessage;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
}
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
const editor = document.querySelector(".ql-editor");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Render pagination controls
|
||||
const totalMessages = await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`);
|
||||
renderPaginationControls(room, totalMessages, limit);
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Polling function to check for new messages without clearing existing ones
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage, true);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
||||
|
@ -1,175 +0,0 @@
|
||||
// const cardIdentifier = `minter-board-card-${Date.now()}`;
|
||||
const cardIdentifier = `test-board-card-${await uid()}`;
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]');
|
||||
|
||||
minterBoardLinks.forEach(link => {
|
||||
link.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!userState.isLoggedIn) {
|
||||
await login();
|
||||
}
|
||||
await loadMinterBoardPage();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
async function loadMinterBoardPage() {
|
||||
// Clear existing content on the page
|
||||
const bodyChildren = document.body.children;
|
||||
for (let i = bodyChildren.length - 1; i >= 0; i--) {
|
||||
const child = bodyChildren[i];
|
||||
if (!child.classList.contains('menu')) {
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Add the "Minter Board" content
|
||||
const mainContent = document.createElement("div");
|
||||
mainContent.innerHTML = `
|
||||
<div class="minter-board-main" style="padding: 20px; text-align: center;">
|
||||
<h1 style="color: lightblue;">Minter Board</h1>
|
||||
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button>
|
||||
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
|
||||
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
|
||||
<h3>Create a New Minter Card</h3>
|
||||
<form id="publish-card-form">
|
||||
<label for="card-header">Header:</label>
|
||||
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
|
||||
<label for="card-content">Content:</label>
|
||||
<textarea id="card-content" placeholder="Enter detailed information..." required></textarea>
|
||||
<label for="card-links">Links (qortal://...):</label>
|
||||
<div id="links-container">
|
||||
<input type="text" class="card-link" placeholder="Enter QDN link">
|
||||
</div>
|
||||
<button type="button" id="add-link-button">Add Another Link</button>
|
||||
<button type="submit" style="margin-top: 10px;">Publish Card</button>
|
||||
<button type="button" id="cancel-publish" style="margin-top: 10px;">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
document.getElementById("publish-card-button").addEventListener("click", () => {
|
||||
document.getElementById("publish-card-view").style.display = "block";
|
||||
document.getElementById("cards-container").style.display = "none";
|
||||
});
|
||||
|
||||
document.getElementById("cancel-publish").addEventListener("click", () => {
|
||||
document.getElementById("publish-card-view").style.display = "none";
|
||||
document.getElementById("cards-container").style.display = "block";
|
||||
});
|
||||
|
||||
document.getElementById("add-link-button").addEventListener("click", () => {
|
||||
const linksContainer = document.getElementById("links-container");
|
||||
const newLinkInput = document.createElement("input");
|
||||
newLinkInput.type = "text";
|
||||
newLinkInput.className = "card-link";
|
||||
newLinkInput.placeholder = "Enter QDN link";
|
||||
linksContainer.appendChild(newLinkInput);
|
||||
});
|
||||
|
||||
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
await publishCard();
|
||||
});
|
||||
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
async function publishCard() {
|
||||
const header = document.getElementById("card-header").value.trim();
|
||||
const content = document.getElementById("card-content").value.trim();
|
||||
const links = Array.from(document.querySelectorAll(".card-link"))
|
||||
.map(input => input.value.trim())
|
||||
.filter(link => link.startsWith("qortal://"));
|
||||
|
||||
if (!header || !content) {
|
||||
alert("Header and content are required!");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardData = {
|
||||
header,
|
||||
content,
|
||||
links,
|
||||
creator: userState.accountName,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
||||
|
||||
try {
|
||||
const base64CardData = btoa(JSON.stringify(cardData));
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: cardIdentifier,
|
||||
data64: base64CardData,
|
||||
});
|
||||
|
||||
alert("Card published successfully!");
|
||||
document.getElementById("publish-card-form").reset();
|
||||
document.getElementById("publish-card-view").style.display = "none";
|
||||
document.getElementById("cards-container").style.display = "block";
|
||||
await loadCards();
|
||||
} catch (error) {
|
||||
console.error("Error publishing card:", error);
|
||||
alert("Failed to publish card.");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCards() {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
cardsContainer.innerHTML = "<p>Loading cards...</p>";
|
||||
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
identifierPrefix: "minter-board-card-",
|
||||
});
|
||||
|
||||
if (!response || response.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No cards found.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
cardsContainer.innerHTML = "";
|
||||
for (const card of response) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
});
|
||||
|
||||
const cardData = JSON.parse(atob(cardDataResponse));
|
||||
const cardHTML = createCardHTML(cardData);
|
||||
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading cards:", error);
|
||||
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
|
||||
}
|
||||
}
|
||||
|
||||
function createCardHTML(cardData) {
|
||||
const { header, content, links, creator, timestamp } = cardData;
|
||||
const formattedDate = new Date(timestamp).toLocaleString();
|
||||
const linksHTML = links.map(link => `<a href="${link}" target="_blank">${link}</a>`).join("<br>");
|
||||
|
||||
return `
|
||||
<div class="card" style="border: 1px solid lightblue; padding: 20px; margin-bottom: 20px; background-color: #2a2a2a; color: lightblue;">
|
||||
<h3>${header}</h3>
|
||||
<p>${content}</p>
|
||||
<div>${linksHTML}</div>
|
||||
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
@ -1,302 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
|
||||
// Load the latest message identifiers from local storage
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background image
|
||||
const mainContent = document.createElement('div');
|
||||
const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<span>MINTERSHIP FORUM (Alpha)</span>
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "") {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
|
||||
// Create message object with unique identifier and HTML content
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: false,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
replyToMessageIdentifier = null; // Clear reply reference after sending
|
||||
// Update the latest message identifier
|
||||
latestMessageIdentifiers[room] = messageIdentifier;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
// Reload messages
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for the load more button
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load messages for any given room with pagination
|
||||
async function loadMessagesFromQDN(room, page) {
|
||||
try {
|
||||
const offset = page * 10;
|
||||
const limit = 10;
|
||||
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, offset, limit);
|
||||
|
||||
const qdnMessages = response;
|
||||
console.log("Messages fetched successfully:", qdnMessages);
|
||||
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (messagesContainer) {
|
||||
if (!qdnMessages || !qdnMessages.length) {
|
||||
if (page === 0) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let messagesHTML = messagesContainer.innerHTML;
|
||||
|
||||
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
fetchMessages.forEach(async (message) => {
|
||||
if (message) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
|
||||
|
||||
messagesHTML += `
|
||||
<div class="message-item">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
messagesContainer.innerHTML = messagesHTML;
|
||||
|
||||
setTimeout(() => {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}, 1000);
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Polling function to check for new messages
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
@ -1,289 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
|
||||
// Load the latest message identifiers from local storage
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background image
|
||||
const mainContent = document.createElement('div');
|
||||
const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue;">MINTERSHIP FORUM (Alpha)</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
<div class="user-info" style="float: right; color: lightblue; margin-right: 50px;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
// const quill = new Quill('#editor', {
|
||||
// theme: 'snow',
|
||||
// modules: {
|
||||
// toolbar: '#toolbar' // Link to the external toolbar element
|
||||
// }
|
||||
// });
|
||||
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
['clean'] // remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room);
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "") {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
|
||||
// Create message object with unique identifier and HTML content
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: false,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
replyToMessageIdentifier = null; // Clear reply reference after sending
|
||||
// Update the latest message identifier
|
||||
latestMessageIdentifiers[room] = messageIdentifier;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
// Reload messages
|
||||
loadMessagesFromQDN(room);
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load messages for any given room
|
||||
async function loadMessagesFromQDN(room) {
|
||||
try {
|
||||
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, 0, false);
|
||||
|
||||
const qdnMessages = response;
|
||||
console.log("Messages fetched successfully:", qdnMessages);
|
||||
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (messagesContainer) {
|
||||
if (!qdnMessages || !qdnMessages.length) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let messagesHTML = "";
|
||||
|
||||
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
fetchMessages.forEach(async (message) => {
|
||||
if (message) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
|
||||
|
||||
messagesHTML += `
|
||||
<div class="message-item">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
messagesContainer.innerHTML = messagesHTML;
|
||||
|
||||
setTimeout(() => {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}, 1000);
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Polling function to check for new messages
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
@ -1,192 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLink = document.querySelector('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
if (mintershipForumLink) {
|
||||
mintershipForumLink.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background image
|
||||
const mainContent = document.createElement('div');
|
||||
const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header">MINTERSHIP FORUM (Alpha)</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
<div class="user-info" style="float: right; color: white; margin-right: 20px;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="editor" class="message-input"></div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow'
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room);
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "") {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
|
||||
// Create message object with unique identifier and HTML content
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: false
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
const base64Message = await objectToBase64(messageObject);
|
||||
if (!messageObject) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
// Reload messages
|
||||
loadMessagesFromQDN(room);
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to load messages from QDN for a specific room
|
||||
async function loadMessagesFromQDN(room) {
|
||||
try {
|
||||
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, 0, false);
|
||||
|
||||
const qdnMessages = response;
|
||||
console.log("Messages fetched successfully:", qdnMessages);
|
||||
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (messagesContainer) {
|
||||
if (!qdnMessages || !qdnMessages.length) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let messagesHTML = "";
|
||||
|
||||
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
fetchMessages.forEach(async (message) => {
|
||||
if (message) {
|
||||
messagesHTML += `
|
||||
<div class="message-item">
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
</div>
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
messagesContainer.innerHTML = messagesHTML;
|
||||
setTimeout(() => {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}, 5000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading messages from QDN:", error);
|
||||
}
|
||||
}
|
@ -1,282 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
|
||||
// Load the latest message identifiers from local storage
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background image
|
||||
const mainContent = document.createElement('div');
|
||||
const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue;">MINTERSHIP FORUM (Alpha)</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
<div class="user-info" style="float: right; color: lightblue; margin-right: 50px;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div class="message-input-section" style="background-color: black; padding: 10px; position: relative;">
|
||||
<div id="toolbar" class="message-toolbar" style="margin-bottom: 10px;"></div>
|
||||
<div id="editor" class="message-input" style="height: 150px;"></div>
|
||||
<button id="send-button" class="send-button" style="margin-top: 10px;">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
['clean'] // remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room);
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "") {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
|
||||
// Create message object with unique identifier and HTML content
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: false,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
replyToMessageIdentifier = null; // Clear reply reference after sending
|
||||
// Update the latest message identifier
|
||||
latestMessageIdentifiers[room] = messageIdentifier;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
// Reload messages
|
||||
loadMessagesFromQDN(room);
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load messages for any given room
|
||||
async function loadMessagesFromQDN(room) {
|
||||
try {
|
||||
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, 0, false);
|
||||
|
||||
const qdnMessages = response;
|
||||
console.log("Messages fetched successfully:", qdnMessages);
|
||||
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (messagesContainer) {
|
||||
if (!qdnMessages || !qdnMessages.length) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let messagesHTML = "";
|
||||
|
||||
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
fetchMessages.forEach(async (message) => {
|
||||
if (message) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 10px; padding-left: 10px;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
|
||||
|
||||
messagesHTML += `
|
||||
<div class="message-item">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
messagesContainer.innerHTML = messagesHTML;
|
||||
|
||||
setTimeout(() => {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}, 1000);
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Polling function to check for new messages
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
@ -1,274 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
|
||||
// Load the latest message identifiers from local storage
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
|
||||
// if (mintershipForumLink) {
|
||||
// mintershipForumLink.addEventListener('click', async (event) => {
|
||||
// event.preventDefault();
|
||||
// await login(); // Assuming login is an async function
|
||||
// await loadForumPage();
|
||||
// startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
// });
|
||||
// }
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background image
|
||||
const mainContent = document.createElement('div');
|
||||
const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue;">MINTERSHIP FORUM (Alpha)</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
<div class="user-info" style="float: right; color: lightblue; margin-right: 50px;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="editor" class="message-input"></div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow'
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room);
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "") {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
|
||||
// Create message object with unique identifier and HTML content
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: false,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
const base64Message = await objectToBase64(messageObject);
|
||||
if (!messageObject) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
replyToMessageIdentifier = null; // Clear reply reference after sending
|
||||
// Update the latest message identifier
|
||||
latestMessageIdentifiers[room] = messageIdentifier;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
// Reload messages
|
||||
loadMessagesFromQDN(room);
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to load messages from QDN for a specific room
|
||||
async function loadMessagesFromQDN(room) {
|
||||
try {
|
||||
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, 0, false);
|
||||
|
||||
const qdnMessages = response;
|
||||
console.log("Messages fetched successfully:", qdnMessages);
|
||||
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (messagesContainer) {
|
||||
if (!qdnMessages || !qdnMessages.length) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let messagesHTML = "";
|
||||
|
||||
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
fetchMessages.forEach(async (message) => {
|
||||
if (message) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 10px; padding-left: 10px;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
|
||||
|
||||
messagesHTML += `
|
||||
<div class="message-item">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
messagesContainer.innerHTML = messagesHTML;
|
||||
setTimeout(() => {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}, 1000);
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Polling function to check for new messages
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
@ -1,219 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLink = document.querySelector('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
if (mintershipForumLink) {
|
||||
mintershipForumLink.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background image
|
||||
const mainContent = document.createElement('div');
|
||||
const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue;">MINTERSHIP FORUM (Alpha)</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
<div class="user-info" style="float: right; color: lightblue; margin-right: 50px;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="editor" class="message-input"></div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow'
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room);
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "") {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
|
||||
// Create message object with unique identifier and HTML content
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: false,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
const base64Message = await objectToBase64(messageObject);
|
||||
if (!messageObject) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
replyToMessageIdentifier = null; // Clear reply reference after sending
|
||||
// Reload messages
|
||||
loadMessagesFromQDN(room);
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to load messages from QDN for a specific room
|
||||
async function loadMessagesFromQDN(room) {
|
||||
try {
|
||||
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, 0, false);
|
||||
|
||||
const qdnMessages = response;
|
||||
console.log("Messages fetched successfully:", qdnMessages);
|
||||
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (messagesContainer) {
|
||||
if (!qdnMessages || !qdnMessages.length) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let messagesHTML = "";
|
||||
|
||||
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
fetchMessages.forEach(async (message) => {
|
||||
if (message) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 10px; padding-left: 10px;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
messagesHTML += `
|
||||
<div class="message-item">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
</div>
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
messagesContainer.innerHTML = messagesHTML;
|
||||
setTimeout(() => {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}, 5000);
|
||||
|
||||
// Add event listeners to reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", (event) => {
|
||||
replyToMessageIdentifier = event.target.getAttribute("data-message-identifier");
|
||||
console.log("Replying to message with identifier:", replyToMessageIdentifier);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading messages from QDN:", error);
|
||||
}
|
||||
}
|
@ -1,464 +0,0 @@
|
||||
const cardIdentifierPrefix = "test-board-card";
|
||||
let isExistingCard = false
|
||||
let existingCard = {}
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]');
|
||||
|
||||
minterBoardLinks.forEach(link => {
|
||||
link.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!userState.isLoggedIn) {
|
||||
await login();
|
||||
}
|
||||
await loadMinterBoardPage();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadMinterBoardPage() {
|
||||
// Clear existing content on the page
|
||||
const bodyChildren = document.body.children;
|
||||
for (let i = bodyChildren.length - 1; i >= 0; i--) {
|
||||
const child = bodyChildren[i];
|
||||
if (!child.classList.contains("menu")) {
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Add the "Minter Board" content
|
||||
const mainContent = document.createElement("div");
|
||||
mainContent.innerHTML = `
|
||||
<div class="minter-board-main" style="padding: 20px; text-align: center;">
|
||||
<h1 style="color: lightblue;">Minter Board</h1>
|
||||
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button>
|
||||
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
|
||||
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
|
||||
<h3>Create or Update Your Minter Card</h3>
|
||||
<form id="publish-card-form">
|
||||
<label for="card-header">Header:</label>
|
||||
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
|
||||
<label for="card-content">Content:</label>
|
||||
<textarea id="card-content" placeholder="Enter detailed information..." required></textarea>
|
||||
<label for="card-links">Links (qortal://...):</label>
|
||||
<div id="links-container">
|
||||
<input type="text" class="card-link" placeholder="Enter QDN link">
|
||||
</div>
|
||||
<button type="button" id="add-link-button">Add Another Link</button>
|
||||
<button type="submit" style="margin-top: 10px;">Publish Card</button>
|
||||
<button type="button" id="cancel-publish" style="margin-top: 10px;">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
document.getElementById("publish-card-button").addEventListener("click", async () => {
|
||||
existingCard = await fetchExistingCard();
|
||||
if (existingCard) {
|
||||
const updateCard = confirm("You already have a card. Do you want to update it?");
|
||||
isExistingCard = true
|
||||
if (updateCard) {
|
||||
loadCardIntoForm(existingCard);
|
||||
document.getElementById("publish-card-view").style.display = "block";
|
||||
document.getElementById("cards-container").style.display = "none";
|
||||
}
|
||||
} else {
|
||||
document.getElementById("publish-card-view").style.display = "block";
|
||||
document.getElementById("cards-container").style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("cancel-publish").addEventListener("click", () => {
|
||||
document.getElementById("publish-card-view").style.display = "none";
|
||||
document.getElementById("cards-container").style.display = "block";
|
||||
});
|
||||
|
||||
document.getElementById("add-link-button").addEventListener("click", () => {
|
||||
const linksContainer = document.getElementById("links-container");
|
||||
const newLinkInput = document.createElement("input");
|
||||
newLinkInput.type = "text";
|
||||
newLinkInput.className = "card-link";
|
||||
newLinkInput.placeholder = "Enter QDN link";
|
||||
linksContainer.appendChild(newLinkInput);
|
||||
});
|
||||
|
||||
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
await publishCard();
|
||||
});
|
||||
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
async function fetchExistingCard() {
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
nameListFilter: userState.accountName,
|
||||
query: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
existingCard = response.find(card => card.name === userState.accountName);
|
||||
if (existingCard) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: existingCard.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: existingCard.identifier,
|
||||
});
|
||||
|
||||
return cardDataResponse;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching existing card:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function loadCardIntoForm(cardData) {
|
||||
document.getElementById("card-header").value = cardData.header;
|
||||
document.getElementById("card-content").value = cardData.content;
|
||||
|
||||
const linksContainer = document.getElementById("links-container");
|
||||
linksContainer.innerHTML = ""; // Clear previous links
|
||||
cardData.links.forEach(link => {
|
||||
const linkInput = document.createElement("input");
|
||||
linkInput.type = "text";
|
||||
linkInput.className = "card-link";
|
||||
linkInput.value = link;
|
||||
linksContainer.appendChild(linkInput);
|
||||
});
|
||||
}
|
||||
|
||||
async function publishCard() {
|
||||
const header = document.getElementById("card-header").value.trim();
|
||||
const content = document.getElementById("card-content").value.trim();
|
||||
const links = Array.from(document.querySelectorAll(".card-link"))
|
||||
.map(input => input.value.trim())
|
||||
.filter(link => link.startsWith("qortal://"));
|
||||
|
||||
if (!header || !content) {
|
||||
alert("Header and content are required!");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardIdentifier = isExistingCard ? existingCard.identifier : `${cardIdentifierPrefix}-${await uid()}`;
|
||||
const pollName = `${cardIdentifier}-poll`;
|
||||
const pollDescription = `Mintership Board Poll for ${userState.accountName}`;
|
||||
|
||||
const cardData = {
|
||||
header,
|
||||
content,
|
||||
links,
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
poll: pollName,
|
||||
};
|
||||
// new Date().toISOString()
|
||||
try {
|
||||
|
||||
let base64CardData = await objectToBase64(cardData);
|
||||
if (!base64CardData) {
|
||||
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
||||
base64CardData = btoa(JSON.stringify(cardData));
|
||||
}
|
||||
// const base64CardData = btoa(JSON.stringify(cardData));
|
||||
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: cardIdentifier,
|
||||
data64: base64CardData,
|
||||
});
|
||||
|
||||
await qortalRequest({
|
||||
action: "CREATE_POLL",
|
||||
pollName,
|
||||
pollDescription,
|
||||
pollOptions: ["Yes", "No", "Comment"],
|
||||
pollOwnerAddress: userState.accountAddress,
|
||||
});
|
||||
|
||||
alert("Card and poll published successfully!");
|
||||
document.getElementById("publish-card-form").reset();
|
||||
document.getElementById("publish-card-view").style.display = "none";
|
||||
document.getElementById("cards-container").style.display = "block";
|
||||
await loadCards();
|
||||
} catch (error) {
|
||||
console.error("Error publishing card or poll:", error);
|
||||
alert("Failed to publish card and poll.");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCards() {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
cardsContainer.innerHTML = "<p>Loading cards...</p>";
|
||||
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
if (!response || response.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No cards found.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
cardsContainer.innerHTML = "";
|
||||
const pollResultsCache = {};
|
||||
|
||||
for (const card of response) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
});
|
||||
|
||||
const cardData = cardDataResponse;
|
||||
// Cache poll results
|
||||
if (!pollResultsCache[cardData.poll]) {
|
||||
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
|
||||
}
|
||||
|
||||
const pollResults = pollResultsCache[cardData.poll];
|
||||
const cardHTML = await createCardHTML(cardData, pollResults);
|
||||
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading cards:", error);
|
||||
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
|
||||
}
|
||||
}
|
||||
|
||||
const calculatePollResults = (pollData, minterGroupMembers) => {
|
||||
const memberAddresses = minterGroupMembers.map(member => member.member);
|
||||
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0;
|
||||
|
||||
pollData.votes.forEach(vote => {
|
||||
const voterAddress = vote.voterPublicKey;
|
||||
const isAdmin = minterGroupMembers.some(member => member.member === voterAddress && member.isAdmin);
|
||||
|
||||
if (vote.optionIndex === 1) {
|
||||
isAdmin ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : null;
|
||||
} else if (vote.optionIndex === 0) {
|
||||
isAdmin ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : null;
|
||||
}
|
||||
});
|
||||
|
||||
const totalYes = adminYes + minterYes;
|
||||
const totalNo = adminNo + minterNo;
|
||||
|
||||
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo };
|
||||
};
|
||||
|
||||
const postComment = async (cardIdentifier) => {
|
||||
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
|
||||
const commentText = commentInput.value.trim();
|
||||
if (!commentText) {
|
||||
alert('Comment cannot be empty!');
|
||||
return;
|
||||
}
|
||||
|
||||
const commentData = {
|
||||
content: commentText,
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const commentIdentifier = `${cardIdentifier}-comment-${await uid()}`;
|
||||
|
||||
try {
|
||||
const base64CommentData = await objectToBase64(commentData);
|
||||
if (!base64CommentData) {
|
||||
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
||||
base64CommentData = btoa(JSON.stringify(commentData));
|
||||
}
|
||||
// const base64CommentData = btoa(JSON.stringify(commentData));
|
||||
await qortalRequest({
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: userState.accountName,
|
||||
service: 'BLOG_POST',
|
||||
identifier: commentIdentifier,
|
||||
data64: base64CommentData,
|
||||
});
|
||||
alert('Comment posted successfully!');
|
||||
commentInput.value = ''; // Clear input
|
||||
await displayComments(cardIdentifier); // Refresh comments
|
||||
} catch (error) {
|
||||
console.error('Error posting comment:', error);
|
||||
alert('Failed to post comment.');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCommentsForCard = async (cardIdentifier) => {
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: 'SEARCH_QDN_RESOURCES',
|
||||
service: 'BLOG_POST',
|
||||
query: `${cardIdentifier}-comment`,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching comments for ${cardIdentifier}:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const displayComments = async (cardIdentifier) => {
|
||||
const comments = await fetchCommentsForCard(cardIdentifier);
|
||||
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
|
||||
commentsContainer.innerHTML = comments.map(comment => `
|
||||
<div class="comment" style="border: 1px solid gray; margin: 10px 0; padding: 10px; background: #1c1c1c;">
|
||||
<p><strong>${comment.creator}</strong>:</p>
|
||||
<p>${comment.content}</p>
|
||||
<p>${timestampToHumanReadableDate(comment.timestamp)}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
};
|
||||
|
||||
const toggleComments = async (cardIdentifier) => {
|
||||
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
|
||||
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
|
||||
await displayComments(cardIdentifier);
|
||||
commentsSection.style.display = 'block';
|
||||
} else {
|
||||
commentsSection.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function loadCards() {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
cardsContainer.innerHTML = "<p>Loading cards...</p>";
|
||||
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
if (!response || response.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No cards found.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
cardsContainer.innerHTML = "";
|
||||
const pollResultsCache = {};
|
||||
|
||||
for (const card of response) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
});
|
||||
|
||||
const cardData = cardDataResponse;
|
||||
// Cache poll results
|
||||
if (!pollResultsCache[cardData.poll]) {
|
||||
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
|
||||
}
|
||||
|
||||
const pollResults = pollResultsCache[cardData.poll];
|
||||
const cardHTML = await createCardHTML(cardData, pollResults);
|
||||
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading cards:", error);
|
||||
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullContent(cardIdentifier, fullContent) {
|
||||
const contentPreview = document.getElementById(`content-preview-${cardIdentifier}`);
|
||||
const toggleButton = document.getElementById(`toggle-content-${cardIdentifier}`);
|
||||
const isExpanded = contentPreview.getAttribute("data-expanded") === "true";
|
||||
|
||||
if (isExpanded) {
|
||||
// Collapse the content
|
||||
contentPreview.innerText = `${fullContent.substring(0, 150)}...`;
|
||||
toggleButton.innerText = "Display Full Text";
|
||||
contentPreview.setAttribute("data-expanded", "false");
|
||||
} else {
|
||||
// Expand the content
|
||||
contentPreview.innerText = fullContent;
|
||||
toggleButton.innerText = "Show Less";
|
||||
contentPreview.setAttribute("data-expanded", "true");
|
||||
}
|
||||
}
|
||||
|
||||
async function createCardHTML(cardData, pollResults) {
|
||||
const { header, content, links, creator, timestamp, poll } = cardData;
|
||||
const formattedDate = new Date(timestamp).toLocaleString();
|
||||
const linksHTML = links.map((link, index) => `
|
||||
<button onclick="window.open('${link}', '_blank')">
|
||||
${`Link ${index + 1} - ${link}`}
|
||||
</button>
|
||||
`).join("");
|
||||
|
||||
const minterGroupMembers = await fetchMinterGroupMembers();
|
||||
const { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo } =
|
||||
calculatePollResults(pollResults, minterGroupMembers);
|
||||
|
||||
const trimmedContent = content.length > 150 ? `${content.substring(0, 150)}...` : content;
|
||||
|
||||
return `
|
||||
<div class="minter-card">
|
||||
<div class="minter-card-header">
|
||||
<h3>${creator}</h3>
|
||||
<p>${header}</p>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div><h5>Minter's Message</h5></div>
|
||||
<div id="content-preview-${cardData.identifier}" class="content-preview">
|
||||
${trimmedContent}
|
||||
</div>
|
||||
${
|
||||
content.length > 150
|
||||
? `<button id="toggle-content-${cardData.identifier}" class="toggle-content-button" onclick="toggleFullContent('${cardData.identifier}', '${content}')">Display Full Content</button>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<div class="info-links">
|
||||
${linksHTML}
|
||||
</div>
|
||||
<div class="minter-card-results">
|
||||
<div class="admin-results">
|
||||
<span class="admin-yes">Admin Yes: ${adminYes}</span>
|
||||
<span class="admin-no">Admin No: ${adminNo}</span>
|
||||
</div>
|
||||
<div class="minter-results">
|
||||
<span class="minter-yes">Minter Yes: ${minterYes}</span>
|
||||
<span class="minter-no">Minter No: ${minterNo}</span>
|
||||
</div>
|
||||
<div class="total-results">
|
||||
<span class="total-yes">Total Yes: ${totalYes}</span>
|
||||
<span class="total-no">Total No: ${totalNo}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div><h5>Support Minter</h5></div>
|
||||
<button class="yes" onclick="voteOnPoll('${poll}', 'Yes')">YES</button>
|
||||
<button class="comment" onclick="toggleComments('${cardData.identifier}')">COMMENT</button>
|
||||
<button class="no" onclick="voteOnPoll('${poll}', 'No')">NO</button>
|
||||
</div>
|
||||
<div id="comments-section-${cardData.identifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||
<div id="comments-container-${cardData.identifier}" class="comments-container"></div>
|
||||
<textarea id="new-comment-${cardData.identifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||
<button onclick="postComment('${cardData.identifier}')">Post Comment</button>
|
||||
</div>
|
||||
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -1,461 +0,0 @@
|
||||
const cardIdentifierPrefix = "test-board-card";
|
||||
let isExistingCard = false
|
||||
let existingCard = {}
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]');
|
||||
|
||||
minterBoardLinks.forEach(link => {
|
||||
link.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!userState.isLoggedIn) {
|
||||
await login();
|
||||
}
|
||||
await loadMinterBoardPage();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadMinterBoardPage() {
|
||||
// Clear existing content on the page
|
||||
const bodyChildren = document.body.children;
|
||||
for (let i = bodyChildren.length - 1; i >= 0; i--) {
|
||||
const child = bodyChildren[i];
|
||||
if (!child.classList.contains("menu")) {
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Add the "Minter Board" content
|
||||
const mainContent = document.createElement("div");
|
||||
mainContent.innerHTML = `
|
||||
<div class="minter-board-main" style="padding: 20px; text-align: center;">
|
||||
<h1 style="color: lightblue;">Minter Board</h1>
|
||||
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button>
|
||||
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
|
||||
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
|
||||
<h3>Create or Update Your Minter Card</h3>
|
||||
<form id="publish-card-form">
|
||||
<label for="card-header">Header:</label>
|
||||
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
|
||||
<label for="card-content">Content:</label>
|
||||
<textarea id="card-content" placeholder="Enter detailed information..." required></textarea>
|
||||
<label for="card-links">Links (qortal://...):</label>
|
||||
<div id="links-container">
|
||||
<input type="text" class="card-link" placeholder="Enter QDN link">
|
||||
</div>
|
||||
<button type="button" id="add-link-button">Add Another Link</button>
|
||||
<button type="submit" style="margin-top: 10px;">Publish Card</button>
|
||||
<button type="button" id="cancel-publish" style="margin-top: 10px;">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
document.getElementById("publish-card-button").addEventListener("click", async () => {
|
||||
existingCard = await fetchExistingCard();
|
||||
if (existingCard) {
|
||||
const updateCard = confirm("You already have a card. Do you want to update it?");
|
||||
isExistingCard = true
|
||||
if (updateCard) {
|
||||
loadCardIntoForm(existingCard);
|
||||
document.getElementById("publish-card-view").style.display = "block";
|
||||
document.getElementById("cards-container").style.display = "none";
|
||||
}
|
||||
} else {
|
||||
document.getElementById("publish-card-view").style.display = "block";
|
||||
document.getElementById("cards-container").style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("cancel-publish").addEventListener("click", () => {
|
||||
document.getElementById("publish-card-view").style.display = "none";
|
||||
document.getElementById("cards-container").style.display = "block";
|
||||
});
|
||||
|
||||
document.getElementById("add-link-button").addEventListener("click", () => {
|
||||
const linksContainer = document.getElementById("links-container");
|
||||
const newLinkInput = document.createElement("input");
|
||||
newLinkInput.type = "text";
|
||||
newLinkInput.className = "card-link";
|
||||
newLinkInput.placeholder = "Enter QDN link";
|
||||
linksContainer.appendChild(newLinkInput);
|
||||
});
|
||||
|
||||
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
await publishCard();
|
||||
});
|
||||
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
async function fetchExistingCard() {
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
nameListFilter: userState.accountName,
|
||||
query: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
existingCard = response.find(card => card.name === userState.accountName);
|
||||
if (existingCard) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: existingCard.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: existingCard.identifier,
|
||||
});
|
||||
|
||||
return cardDataResponse;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching existing card:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function loadCardIntoForm(cardData) {
|
||||
document.getElementById("card-header").value = cardData.header;
|
||||
document.getElementById("card-content").value = cardData.content;
|
||||
|
||||
const linksContainer = document.getElementById("links-container");
|
||||
linksContainer.innerHTML = ""; // Clear previous links
|
||||
cardData.links.forEach(link => {
|
||||
const linkInput = document.createElement("input");
|
||||
linkInput.type = "text";
|
||||
linkInput.className = "card-link";
|
||||
linkInput.value = link;
|
||||
linksContainer.appendChild(linkInput);
|
||||
});
|
||||
}
|
||||
|
||||
async function publishCard() {
|
||||
const header = document.getElementById("card-header").value.trim();
|
||||
const content = document.getElementById("card-content").value.trim();
|
||||
const links = Array.from(document.querySelectorAll(".card-link"))
|
||||
.map(input => input.value.trim())
|
||||
.filter(link => link.startsWith("qortal://"));
|
||||
|
||||
if (!header || !content) {
|
||||
alert("Header and content are required!");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardIdentifier = isExistingCard ? existingCard.identifier : `${cardIdentifierPrefix}-${await uid()}`;
|
||||
const pollName = `${cardIdentifier}-poll`;
|
||||
const pollDescription = `Mintership Board Poll for ${userState.accountName}`;
|
||||
|
||||
const cardData = {
|
||||
header,
|
||||
content,
|
||||
links,
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
poll: pollName,
|
||||
};
|
||||
// new Date().toISOString()
|
||||
try {
|
||||
|
||||
let base64CardData = await objectToBase64(cardData);
|
||||
if (!base64CardData) {
|
||||
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
||||
base64CardData = btoa(JSON.stringify(cardData));
|
||||
}
|
||||
// const base64CardData = btoa(JSON.stringify(cardData));
|
||||
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: cardIdentifier,
|
||||
data64: base64CardData,
|
||||
});
|
||||
|
||||
await qortalRequest({
|
||||
action: "CREATE_POLL",
|
||||
pollName,
|
||||
pollDescription,
|
||||
pollOptions: ["Yes", "No", "Comment"],
|
||||
pollOwnerAddress: userState.accountAddress,
|
||||
});
|
||||
|
||||
alert("Card and poll published successfully!");
|
||||
document.getElementById("publish-card-form").reset();
|
||||
document.getElementById("publish-card-view").style.display = "none";
|
||||
document.getElementById("cards-container").style.display = "block";
|
||||
await loadCards();
|
||||
} catch (error) {
|
||||
console.error("Error publishing card or poll:", error);
|
||||
alert("Failed to publish card and poll.");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCards() {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
cardsContainer.innerHTML = "<p>Loading cards...</p>";
|
||||
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
if (!response || response.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No cards found.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
cardsContainer.innerHTML = "";
|
||||
const pollResultsCache = {};
|
||||
|
||||
for (const card of response) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
});
|
||||
|
||||
const cardData = cardDataResponse;
|
||||
// Cache poll results
|
||||
if (!pollResultsCache[cardData.poll]) {
|
||||
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
|
||||
}
|
||||
|
||||
const pollResults = pollResultsCache[cardData.poll];
|
||||
const cardHTML = await createCardHTML(cardData, pollResults);
|
||||
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading cards:", error);
|
||||
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
|
||||
}
|
||||
}
|
||||
|
||||
const calculatePollResults = (pollData, minterGroupMembers) => {
|
||||
const memberAddresses = minterGroupMembers.map(member => member.member);
|
||||
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0;
|
||||
|
||||
pollData.votes.forEach(vote => {
|
||||
const voterAddress = vote.voterPublicKey;
|
||||
const isAdmin = minterGroupMembers.some(member => member.member === voterAddress && member.isAdmin);
|
||||
|
||||
if (vote.optionIndex === 1) {
|
||||
isAdmin ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : null;
|
||||
} else if (vote.optionIndex === 0) {
|
||||
isAdmin ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : null;
|
||||
}
|
||||
});
|
||||
|
||||
const totalYes = adminYes + minterYes;
|
||||
const totalNo = adminNo + minterNo;
|
||||
|
||||
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo };
|
||||
};
|
||||
|
||||
const postComment = async (cardIdentifier) => {
|
||||
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
|
||||
const commentText = commentInput.value.trim();
|
||||
if (!commentText) {
|
||||
alert('Comment cannot be empty!');
|
||||
return;
|
||||
}
|
||||
|
||||
const commentData = {
|
||||
content: commentText,
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const commentIdentifier = `${cardIdentifier}-comment-${await uid()}`;
|
||||
|
||||
try {
|
||||
const base64CommentData = await objectToBase64(commentData);
|
||||
if (!base64CommentData) {
|
||||
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
||||
base64CommentData = btoa(JSON.stringify(commentData));
|
||||
}
|
||||
// const base64CommentData = btoa(JSON.stringify(commentData));
|
||||
await qortalRequest({
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: userState.accountName,
|
||||
service: 'BLOG_POST',
|
||||
identifier: commentIdentifier,
|
||||
data64: base64CommentData,
|
||||
});
|
||||
alert('Comment posted successfully!');
|
||||
commentInput.value = ''; // Clear input
|
||||
await displayComments(cardIdentifier); // Refresh comments
|
||||
} catch (error) {
|
||||
console.error('Error posting comment:', error);
|
||||
alert('Failed to post comment.');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCommentsForCard = async (cardIdentifier) => {
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: 'SEARCH_QDN_RESOURCES',
|
||||
service: 'BLOG_POST',
|
||||
query: `${cardIdentifier}-comment`,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching comments for ${cardIdentifier}:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const displayComments = async (cardIdentifier) => {
|
||||
const comments = await fetchCommentsForCard(cardIdentifier);
|
||||
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
|
||||
commentsContainer.innerHTML = comments.map(comment => `
|
||||
<div class="comment" style="border: 1px solid gray; margin: 10px 0; padding: 10px; background: #1c1c1c;">
|
||||
<p><strong>${comment.creator}</strong>:</p>
|
||||
<p>${comment.content}</p>
|
||||
<p>${timestampToHumanReadableDate(comment.timestamp)}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
};
|
||||
|
||||
const toggleComments = async (cardIdentifier) => {
|
||||
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
|
||||
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
|
||||
await displayComments(cardIdentifier);
|
||||
commentsSection.style.display = 'block';
|
||||
} else {
|
||||
commentsSection.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function loadCards() {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
cardsContainer.innerHTML = "<p>Loading cards...</p>";
|
||||
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
if (!response || response.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No cards found.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
cardsContainer.innerHTML = "";
|
||||
const pollResultsCache = {};
|
||||
|
||||
for (const card of response) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
});
|
||||
|
||||
const cardData = cardDataResponse;
|
||||
// Cache poll results
|
||||
if (!pollResultsCache[cardData.poll]) {
|
||||
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
|
||||
}
|
||||
|
||||
const pollResults = pollResultsCache[cardData.poll];
|
||||
const cardHTML = await createCardHTML(cardData, pollResults);
|
||||
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading cards:", error);
|
||||
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullContent(cardIdentifier, fullContent) {
|
||||
const contentPreview = document.getElementById(`content-preview-${cardIdentifier}`);
|
||||
const toggleButton = document.getElementById(`toggle-content-${cardIdentifier}`);
|
||||
|
||||
if (contentPreview.innerText.length > 150) {
|
||||
// Collapse the content
|
||||
contentPreview.innerText = `${fullContent.substring(0, 150)}...`;
|
||||
toggleButton.innerText = "Display Full Content";
|
||||
} else {
|
||||
// Expand the content
|
||||
contentPreview.innerText = fullContent;
|
||||
toggleButton.innerText = "Show Less";
|
||||
}
|
||||
}
|
||||
|
||||
async function createCardHTML(cardData, pollResults) {
|
||||
const { header, content, links, creator, timestamp, poll } = cardData;
|
||||
const formattedDate = new Date(timestamp).toLocaleString();
|
||||
const linksHTML = links.map((link, index) => `
|
||||
<button onclick="window.open('${link}', '_blank')">
|
||||
${`Link ${index + 1} - ${link}`}
|
||||
</button>
|
||||
`).join("");
|
||||
|
||||
const minterGroupMembers = await fetchMinterGroupMembers();
|
||||
const { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo } =
|
||||
calculatePollResults(pollResults, minterGroupMembers);
|
||||
|
||||
const trimmedContent = content.length > 150 ? `${content.substring(0, 150)}...` : content;
|
||||
|
||||
return `
|
||||
<div class="minter-card">
|
||||
<div class="minter-card-header">
|
||||
<h3>${creator}</h3>
|
||||
<p>${header}</p>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div><h5>Minter's Message</h5></div>
|
||||
<div id="content-preview-${cardData.identifier}" class="content-preview">
|
||||
${trimmedContent}
|
||||
</div>
|
||||
${
|
||||
content.length > 150
|
||||
? `<button id="toggle-content-${cardData.identifier}" class="toggle-content-button" onclick="toggleFullContent('${cardData.identifier}', '${content}')">Display Full Content</button>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<div class="info-links">
|
||||
${linksHTML}
|
||||
</div>
|
||||
<div class="minter-card-results">
|
||||
<div class="admin-results">
|
||||
<span class="admin-yes">Admin Yes: ${adminYes}</span>
|
||||
<span class="admin-no">Admin No: ${adminNo}</span>
|
||||
</div>
|
||||
<div class="minter-results">
|
||||
<span class="minter-yes">Minter Yes: ${minterYes}</span>
|
||||
<span class="minter-no">Minter No: ${minterNo}</span>
|
||||
</div>
|
||||
<div class="total-results">
|
||||
<span class="total-yes">Total Yes: ${totalYes}</span>
|
||||
<span class="total-no">Total No: ${totalNo}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div><h5>Support Minter</h5></div>
|
||||
<button class="yes" onclick="voteOnPoll('${poll}', 'Yes')">YES</button>
|
||||
<button class="comment" onclick="toggleComments('${cardData.identifier}')">COMMENT</button>
|
||||
<button class="no" onclick="voteOnPoll('${poll}', 'No')">NO</button>
|
||||
</div>
|
||||
<div id="comments-section-${cardData.identifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||
<div id="comments-container-${cardData.identifier}" class="comments-container"></div>
|
||||
<textarea id="new-comment-${cardData.identifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||
<button onclick="postComment('${cardData.identifier}')">Post Comment</button>
|
||||
</div>
|
||||
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -1,450 +0,0 @@
|
||||
const cardIdentifierPrefix = "test-board-card";
|
||||
let isExistingCard = false
|
||||
let existingCard = {}
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]');
|
||||
|
||||
minterBoardLinks.forEach(link => {
|
||||
link.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!userState.isLoggedIn) {
|
||||
await login();
|
||||
}
|
||||
await loadMinterBoardPage();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadMinterBoardPage() {
|
||||
// Clear existing content on the page
|
||||
const bodyChildren = document.body.children;
|
||||
for (let i = bodyChildren.length - 1; i >= 0; i--) {
|
||||
const child = bodyChildren[i];
|
||||
if (!child.classList.contains("menu")) {
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Add the "Minter Board" content
|
||||
const mainContent = document.createElement("div");
|
||||
mainContent.innerHTML = `
|
||||
<div class="minter-board-main" style="padding: 20px; text-align: center;">
|
||||
<h1 style="color: lightblue;">Minter Board</h1>
|
||||
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button>
|
||||
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
|
||||
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
|
||||
<h3>Create or Update Your Minter Card</h3>
|
||||
<form id="publish-card-form">
|
||||
<label for="card-header">Header:</label>
|
||||
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
|
||||
<label for="card-content">Content:</label>
|
||||
<textarea id="card-content" placeholder="Enter detailed information..." required></textarea>
|
||||
<label for="card-links">Links (qortal://...):</label>
|
||||
<div id="links-container">
|
||||
<input type="text" class="card-link" placeholder="Enter QDN link">
|
||||
</div>
|
||||
<button type="button" id="add-link-button">Add Another Link</button>
|
||||
<button type="submit" style="margin-top: 10px;">Publish Card</button>
|
||||
<button type="button" id="cancel-publish" style="margin-top: 10px;">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
document.getElementById("publish-card-button").addEventListener("click", async () => {
|
||||
const existingCard = await fetchExistingCard();
|
||||
if (existingCard) {
|
||||
const updateCard = confirm("You already have a card. Do you want to update it?");
|
||||
isExistingCard = true
|
||||
if (updateCard) {
|
||||
loadCardIntoForm(existingCard);
|
||||
document.getElementById("publish-card-view").style.display = "block";
|
||||
document.getElementById("cards-container").style.display = "none";
|
||||
}
|
||||
} else {
|
||||
document.getElementById("publish-card-view").style.display = "block";
|
||||
document.getElementById("cards-container").style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("cancel-publish").addEventListener("click", () => {
|
||||
document.getElementById("publish-card-view").style.display = "none";
|
||||
document.getElementById("cards-container").style.display = "block";
|
||||
});
|
||||
|
||||
document.getElementById("add-link-button").addEventListener("click", () => {
|
||||
const linksContainer = document.getElementById("links-container");
|
||||
const newLinkInput = document.createElement("input");
|
||||
newLinkInput.type = "text";
|
||||
newLinkInput.className = "card-link";
|
||||
newLinkInput.placeholder = "Enter QDN link";
|
||||
linksContainer.appendChild(newLinkInput);
|
||||
});
|
||||
|
||||
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
await publishCard();
|
||||
});
|
||||
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
async function fetchExistingCard() {
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
nameListFilter: userState.accountName,
|
||||
query: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
existingCard = response.find(card => card.name === userState.accountName);
|
||||
if (existingCard) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: existingCard.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: existingCard.identifier,
|
||||
});
|
||||
|
||||
return cardDataResponse;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching existing card:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function loadCardIntoForm(cardData) {
|
||||
document.getElementById("card-header").value = cardData.header;
|
||||
document.getElementById("card-content").value = cardData.content;
|
||||
|
||||
const linksContainer = document.getElementById("links-container");
|
||||
linksContainer.innerHTML = ""; // Clear previous links
|
||||
cardData.links.forEach(link => {
|
||||
const linkInput = document.createElement("input");
|
||||
linkInput.type = "text";
|
||||
linkInput.className = "card-link";
|
||||
linkInput.value = link;
|
||||
linksContainer.appendChild(linkInput);
|
||||
});
|
||||
}
|
||||
|
||||
async function publishCard() {
|
||||
const header = document.getElementById("card-header").value.trim();
|
||||
const content = document.getElementById("card-content").value.trim();
|
||||
const links = Array.from(document.querySelectorAll(".card-link"))
|
||||
.map(input => input.value.trim())
|
||||
.filter(link => link.startsWith("qortal://"));
|
||||
|
||||
if (!header || !content) {
|
||||
alert("Header and content are required!");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardIdentifier = isExistingCard ? existingCard.identifier : `${cardIdentifierPrefix}-${await uid()}`;
|
||||
const pollName = `${cardIdentifier}-poll`;
|
||||
const pollDescription = `Mintership Board Poll for ${userState.accountName}`;
|
||||
|
||||
const cardData = {
|
||||
header,
|
||||
content,
|
||||
links,
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
poll: pollName,
|
||||
};
|
||||
// new Date().toISOString()
|
||||
try {
|
||||
|
||||
let base64CardData = await objectToBase64(cardData);
|
||||
if (!base64CardData) {
|
||||
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
||||
base64CardData = btoa(JSON.stringify(cardData));
|
||||
}
|
||||
// const base64CardData = btoa(JSON.stringify(cardData));
|
||||
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: cardIdentifier,
|
||||
data64: base64CardData,
|
||||
});
|
||||
|
||||
await qortalRequest({
|
||||
action: "CREATE_POLL",
|
||||
pollName,
|
||||
pollDescription,
|
||||
pollOptions: ["Yes", "No", "Comment"],
|
||||
pollOwnerAddress: userState.accountAddress,
|
||||
});
|
||||
|
||||
alert("Card and poll published successfully!");
|
||||
document.getElementById("publish-card-form").reset();
|
||||
document.getElementById("publish-card-view").style.display = "none";
|
||||
document.getElementById("cards-container").style.display = "block";
|
||||
await loadCards();
|
||||
} catch (error) {
|
||||
console.error("Error publishing card or poll:", error);
|
||||
alert("Failed to publish card and poll.");
|
||||
}
|
||||
}
|
||||
|
||||
const postComment = async (cardIdentifier) => {
|
||||
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
|
||||
const commentText = commentInput.value.trim();
|
||||
if (!commentText) {
|
||||
alert('Comment cannot be empty!');
|
||||
return;
|
||||
}
|
||||
|
||||
const commentData = {
|
||||
content: commentText,
|
||||
creator: userState.accountName,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const commentIdentifier = `${cardIdentifier}-comment-${await uid()}`;
|
||||
|
||||
try {
|
||||
const base64CommentData = btoa(JSON.stringify(commentData));
|
||||
await qortalRequest({
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: userState.accountName,
|
||||
service: 'BLOG_POST',
|
||||
identifier: commentIdentifier,
|
||||
data64: base64CommentData,
|
||||
});
|
||||
alert('Comment posted successfully!');
|
||||
commentInput.value = ''; // Clear input
|
||||
await displayComments(cardIdentifier); // Refresh comments
|
||||
} catch (error) {
|
||||
console.error('Error posting comment:', error);
|
||||
alert('Failed to post comment.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function loadCards() {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
cardsContainer.innerHTML = "<p>Loading cards...</p>";
|
||||
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
if (!response || response.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No cards found.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
cardsContainer.innerHTML = "";
|
||||
const pollResultsCache = {};
|
||||
|
||||
for (const card of response) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
});
|
||||
|
||||
const cardData = cardDataResponse;
|
||||
// Cache poll results
|
||||
if (!pollResultsCache[cardData.poll]) {
|
||||
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
|
||||
}
|
||||
|
||||
const pollResults = pollResultsCache[cardData.poll];
|
||||
const cardHTML = await createCardHTML(cardData, pollResults);
|
||||
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading cards:", error);
|
||||
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const calculatePollResults = (pollData, minterGroupMembers) => {
|
||||
const memberAddresses = minterGroupMembers.map(member => member.member);
|
||||
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0;
|
||||
|
||||
pollData.votes.forEach(vote => {
|
||||
const voterAddress = vote.voterPublicKey;
|
||||
const isAdmin = minterGroupMembers.some(member => member.member === voterAddress && member.isAdmin);
|
||||
|
||||
if (vote.optionIndex === 1) {
|
||||
isAdmin ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : null;
|
||||
} else if (vote.optionIndex === 0) {
|
||||
isAdmin ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : null;
|
||||
}
|
||||
});
|
||||
|
||||
const totalYes = adminYes + minterYes;
|
||||
const totalNo = adminNo + minterNo;
|
||||
|
||||
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo };
|
||||
};
|
||||
|
||||
const fetchCommentsForCard = async (cardIdentifier) => {
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: 'SEARCH_QDN_RESOURCES',
|
||||
service: 'BLOG_POST',
|
||||
identifier: `${cardIdentifier}-comments`,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching comments for ${cardIdentifier}:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const displayComments = async (cardIdentifier) => {
|
||||
const comments = await fetchCommentsForCard(cardIdentifier);
|
||||
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
|
||||
commentsContainer.innerHTML = comments.map(comment => `
|
||||
<div class="comment" style="border: 1px solid gray; margin: 10px 0; padding: 10px; background: #1c1c1c;">
|
||||
<p><strong>${comment.creator}</strong>:</p>
|
||||
<p>${comment.content}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
};
|
||||
|
||||
const toggleComments = async (cardIdentifier) => {
|
||||
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
|
||||
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
|
||||
await displayComments(cardIdentifier);
|
||||
commentsSection.style.display = 'block';
|
||||
} else {
|
||||
commentsSection.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function loadCards() {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
cardsContainer.innerHTML = "<p>Loading cards...</p>";
|
||||
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
if (!response || response.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No cards found.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
cardsContainer.innerHTML = "";
|
||||
const pollResultsCache = {};
|
||||
|
||||
for (const card of response) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
});
|
||||
|
||||
const cardData = cardDataResponse;
|
||||
// Cache poll results
|
||||
if (!pollResultsCache[cardData.poll]) {
|
||||
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
|
||||
}
|
||||
|
||||
const pollResults = pollResultsCache[cardData.poll];
|
||||
const cardHTML = await createCardHTML(cardData, pollResults);
|
||||
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading cards:", error);
|
||||
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullContent(cardIdentifier, fullContent) {
|
||||
const contentPreview = document.getElementById(`content-preview-${cardIdentifier}`);
|
||||
const toggleButton = document.getElementById(`toggle-content-${cardIdentifier}`);
|
||||
|
||||
if (contentPreview.innerText.length > 150) {
|
||||
// Collapse the content
|
||||
contentPreview.innerText = `${fullContent.substring(0, 150)}...`;
|
||||
toggleButton.innerText = "Display Full Content";
|
||||
} else {
|
||||
// Expand the content
|
||||
contentPreview.innerText = fullContent;
|
||||
toggleButton.innerText = "Show Less";
|
||||
}
|
||||
}
|
||||
|
||||
async function createCardHTML(cardData, pollResults) {
|
||||
const { header, content, links, creator, timestamp, poll } = cardData;
|
||||
const formattedDate = new Date(timestamp).toLocaleString();
|
||||
const linksHTML = links.map((link, index) => `
|
||||
<button onclick="window.open('${link}', '_blank')">
|
||||
${`Link ${index + 1}`}
|
||||
</button>
|
||||
`).join("");
|
||||
|
||||
const minterGroupMembers = await fetchMinterGroupMembers();
|
||||
const { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo } =
|
||||
calculatePollResults(pollResults, minterGroupMembers);
|
||||
|
||||
const trimmedContent = content.length > 150 ? `${content.substring(0, 150)}...` : content;
|
||||
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>${creator}</h3>
|
||||
<p>${header}</p>
|
||||
</div>
|
||||
<div class="results">
|
||||
<div class="admin-yes">Admin Yes: ${adminYes}</div>
|
||||
<div class="admin-no">Admin No: ${adminNo}</div>
|
||||
<div class="minter-yes">Minter Yes: ${minterYes}</div>
|
||||
<div class="minter-no">Minter No: ${minterNo}</div>
|
||||
<div class="total-yes">Total Yes: ${totalYes}</div>
|
||||
<div class="total-no">Total No: ${totalNo}</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div id="content-preview-${cardData.identifier}" class="content-preview">
|
||||
${trimmedContent}
|
||||
</div>
|
||||
${
|
||||
content.length > 150
|
||||
? `<button id="toggle-content-${cardData.identifier}" class="toggle-content-button" onclick="toggleFullContent('${cardData.identifier}', '${content}')">Display Full Content</button>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<div class="info-links">
|
||||
<button>Minter Introduction</button>
|
||||
${linksHTML}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="yes" onclick="voteOnPoll('${poll}', 'Yes')">YES</button>
|
||||
<button class="comment" onclick="toggleComments('${cardData.identifier}')">COMMENT</button>
|
||||
<button class="no" onclick="voteOnPoll('${poll}', 'No')">NO</button>
|
||||
</div>
|
||||
<div id="comments-section-${cardData.identifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||
<div id="comments-container-${cardData.identifier}" class="comments-container"></div>
|
||||
<textarea id="new-comment-${cardData.identifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||
<button onclick="postComment('${cardData.identifier}')">Post Comment</button>
|
||||
</div>
|
||||
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -1,195 +0,0 @@
|
||||
/* forum-styles.css */
|
||||
|
||||
.forum-main {
|
||||
color: #ffffff;
|
||||
background-color: #000000;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100vw;
|
||||
box-sizing: border-box;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.forum-header {
|
||||
width: 100%;
|
||||
padding: 2vh;
|
||||
background-color: #000000;
|
||||
color: #add8e6; /* Light blue color */
|
||||
text-align: center;
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.forum-submenu {
|
||||
width: 100%;
|
||||
padding: 1vh 2vh;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.forum-rooms {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2vh; /* Increased gap for better spacing */
|
||||
margin-top: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.room-button {
|
||||
background-color: #317e78;
|
||||
color: #ffffff;
|
||||
border: 2px solid #317e78;
|
||||
border-radius: 2vh;
|
||||
padding: 1vh 2vh;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.room-button:hover {
|
||||
background-color: #19403d;
|
||||
}
|
||||
|
||||
.forum-content {
|
||||
flex-grow: 1;
|
||||
width: 90%;
|
||||
padding: 3vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
box-sizing: border-box;
|
||||
border: 3px solid #ffffff; /* Increased border width */
|
||||
}
|
||||
|
||||
.room-content {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
padding: 2vh;
|
||||
border-radius: 1vh;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.room-title {
|
||||
color: #add8e6; /* Light blue color for room name */
|
||||
text-align: center;
|
||||
margin-bottom: 2vh;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
background: #1c1c1c;
|
||||
color: #ffffff;
|
||||
padding: 1vh;
|
||||
margin-bottom: 1vh;
|
||||
border-radius: 0.8vh;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: 1vh;
|
||||
font-size: 1.25rem;
|
||||
color: white
|
||||
}
|
||||
|
||||
.message-header.username {
|
||||
color: #228ec0;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: bold;
|
||||
color:#228ec0
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-style: italic;
|
||||
color: rgb(157, 167, 151)
|
||||
}
|
||||
|
||||
.message-text {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.reply-button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1vh;
|
||||
background-color: #167089;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 1vh;
|
||||
padding: 0.3vh 0.6vh;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-button:hover {
|
||||
background-color: #19403d;
|
||||
}
|
||||
|
||||
/* forum-styles.css additions */
|
||||
|
||||
.message-input-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
gap: 1vh; /* Spacing between toolbar and editor */
|
||||
background-color: black;
|
||||
padding: 1vh;
|
||||
}
|
||||
|
||||
.ql-editor {
|
||||
flex-grow: 1;
|
||||
text-size: 1.25rem;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
flex-grow: 1;
|
||||
padding: 2vh;
|
||||
border-radius: 1vh;
|
||||
border: 1px solid #cccccc;
|
||||
font-size: 1.25rem;
|
||||
/* margin-right: 8vh; */
|
||||
box-sizing: border-box;
|
||||
min-height: 15vh;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
background-color: #13a97c;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 1vh;
|
||||
padding: 2vh 4vh;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send-button:hover {
|
||||
background-color: #19403d;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
width: 100%;
|
||||
margin-bottom: 5vh; /* Ensure space above input section */
|
||||
overflow-y: auto;
|
||||
padding-bottom: 1vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,508 +0,0 @@
|
||||
const cardIdentifierPrefix = "test-board-card";
|
||||
let isExistingCard = false;
|
||||
let existingCardData = {};
|
||||
let existingCardInfo ={};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]');
|
||||
|
||||
minterBoardLinks.forEach(link => {
|
||||
link.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!userState.isLoggedIn) {
|
||||
await login();
|
||||
}
|
||||
await loadMinterBoardPage();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadMinterBoardPage() {
|
||||
// Clear existing content on the page
|
||||
const bodyChildren = document.body.children;
|
||||
for (let i = bodyChildren.length - 1; i >= 0; i--) {
|
||||
const child = bodyChildren[i];
|
||||
if (!child.classList.contains("menu")) {
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Add the "Minter Board" content
|
||||
const mainContent = document.createElement("div");
|
||||
mainContent.innerHTML = `
|
||||
<div class="minter-board-main" style="padding: 20px; text-align: center;">
|
||||
<h1 style="color: lightblue;">Minter Board</h1>
|
||||
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button>
|
||||
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
|
||||
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
|
||||
<h3>Create or Update Your Minter Card</h3>
|
||||
<form id="publish-card-form">
|
||||
<label for="card-header">Header:</label>
|
||||
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
|
||||
<label for="card-content">Content:</label>
|
||||
<textarea id="card-content" placeholder="Enter detailed information about why you deserve to be a minter..." required></textarea>
|
||||
<label for="card-links">Links (qortal://...):</label>
|
||||
<div id="links-container">
|
||||
<input type="text" class="card-link" placeholder="Enter QDN link">
|
||||
</div>
|
||||
<button type="button" id="add-link-button">Add Another Link</button>
|
||||
<button type="submit" id="submit-publish-button">Publish Card</button>
|
||||
<button type="button" id="cancel-publish-button">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
document.getElementById("publish-card-button").addEventListener("click", async () => {
|
||||
try {
|
||||
const {cardData, cardIdentifier} = await fetchExistingCard();
|
||||
|
||||
if (cardIdentifier) {
|
||||
// Update existing card
|
||||
const updateCard = confirm("A card already exists. Do you want to update it?");
|
||||
isExistingCard = true;
|
||||
|
||||
if (updateCard) {
|
||||
// Load existing card into the form for editing
|
||||
loadCardIntoForm(cardData);
|
||||
alert("Edit your existing card and publish.");
|
||||
} else {
|
||||
// Allow creating a new card for testing purposes
|
||||
alert("You can now create a new card for testing.");
|
||||
isExistingCard = false;
|
||||
existingCardData = {}; // Reset to allow new card creation
|
||||
document.getElementById("publish-card-form").reset();
|
||||
}
|
||||
} else {
|
||||
alert("No existing card found. Create a new card.");
|
||||
isExistingCard = false;
|
||||
}
|
||||
|
||||
// Show the form for publishing a card
|
||||
const publishCardView = document.getElementById("publish-card-view");
|
||||
publishCardView.style.display = "flex";
|
||||
document.getElementById("cards-container").style.display = "none";
|
||||
} catch (error) {
|
||||
console.error("Error checking for existing card:", error);
|
||||
alert("Failed to check for existing card. Please try again.");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("cancel-publish-button").addEventListener("click", () => {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
cardsContainer.style.display = "flex"; // Restore visibility
|
||||
const publishCardView = document.getElementById("publish-card-view");
|
||||
publishCardView.style.display = "none"; // Hide the publish form
|
||||
});
|
||||
|
||||
document.getElementById("add-link-button").addEventListener("click", () => {
|
||||
const linksContainer = document.getElementById("links-container");
|
||||
const newLinkInput = document.createElement("input");
|
||||
newLinkInput.type = "text";
|
||||
newLinkInput.className = "card-link";
|
||||
newLinkInput.placeholder = "Enter QDN link";
|
||||
linksContainer.appendChild(newLinkInput);
|
||||
});
|
||||
|
||||
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
await publishCard();
|
||||
});
|
||||
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
async function fetchExistingCard() {
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: userState.accountName,
|
||||
identifier: cardIdentifierPrefix
|
||||
});
|
||||
|
||||
existingCardInfo = response;
|
||||
|
||||
if (existingCard) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: existingCardInfo.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: existingCardInfo.identifier,
|
||||
});
|
||||
|
||||
const existingCardIdentifier = existingCardInfo.identifier;
|
||||
existingCardData = cardDataResponse
|
||||
return {cardDataResponse, existingCardIdentifier};
|
||||
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching existing card:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function loadCardIntoForm(cardData) {
|
||||
document.getElementById("card-header").value = cardData.header;
|
||||
document.getElementById("card-content").value = cardData.content;
|
||||
|
||||
const linksContainer = document.getElementById("links-container");
|
||||
linksContainer.innerHTML = ""; // Clear previous links
|
||||
cardData.links.forEach(link => {
|
||||
const linkInput = document.createElement("input");
|
||||
linkInput.type = "text";
|
||||
linkInput.className = "card-link";
|
||||
linkInput.value = link;
|
||||
linksContainer.appendChild(linkInput);
|
||||
});
|
||||
}
|
||||
|
||||
async function publishCard() {
|
||||
const header = document.getElementById("card-header").value.trim();
|
||||
const content = document.getElementById("card-content").value.trim();
|
||||
const links = Array.from(document.querySelectorAll(".card-link"))
|
||||
.map(input => input.value.trim())
|
||||
.filter(link => link.startsWith("qortal://"));
|
||||
|
||||
if (!header || !content) {
|
||||
alert("Header and content are required!");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardIdentifier = isExistingCard ? existingCardInfo.identifier : `${cardIdentifierPrefix}-${await uid()}`;
|
||||
const pollName = `${cardIdentifier}-poll`;
|
||||
const pollDescription = `Mintership Board Poll for ${userState.accountName}`;
|
||||
|
||||
const cardData = {
|
||||
header,
|
||||
content,
|
||||
links,
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
poll: pollName,
|
||||
};
|
||||
// new Date().toISOString()
|
||||
try {
|
||||
|
||||
let base64CardData = await objectToBase64(cardData);
|
||||
if (!base64CardData) {
|
||||
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
||||
base64CardData = btoa(JSON.stringify(cardData));
|
||||
}
|
||||
// const base64CardData = btoa(JSON.stringify(cardData));
|
||||
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: cardIdentifier,
|
||||
data64: base64CardData,
|
||||
});
|
||||
|
||||
await qortalRequest({
|
||||
action: "CREATE_POLL",
|
||||
pollName,
|
||||
pollDescription,
|
||||
pollOptions: ["Allow", "Deny"],
|
||||
pollOwnerAddress: userState.accountAddress,
|
||||
});
|
||||
|
||||
alert("Card and poll published successfully!");
|
||||
document.getElementById("publish-card-form").reset();
|
||||
document.getElementById("publish-card-view").style.display = "none";
|
||||
document.getElementById("cards-container").style.display = "flex";
|
||||
await loadCards();
|
||||
} catch (error) {
|
||||
console.error("Error publishing card or poll:", error);
|
||||
alert("Failed to publish card and poll.");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCards() {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
cardsContainer.innerHTML = "<p>Loading cards...</p>";
|
||||
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
if (!response || response.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No cards found.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
cardsContainer.innerHTML = ""
|
||||
|
||||
for (const card of response) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
});
|
||||
|
||||
const cardData = cardDataResponse;
|
||||
// Cache poll results
|
||||
|
||||
const pollResults = await fetchPollResults(cardData.poll)
|
||||
const cardHTML = await createCardHTML(cardData, pollResults, card.identifier);
|
||||
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading cards:", error);
|
||||
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
|
||||
}
|
||||
}
|
||||
|
||||
const calculatePollResults = (pollData, minterGroupMembers) => {
|
||||
const memberAddresses = minterGroupMembers.map(member => member.member);
|
||||
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0;
|
||||
|
||||
pollData.votes.forEach(vote => {
|
||||
const voterAddress = vote.voterPublicKey;
|
||||
const isAdmin = minterGroupMembers.some(member => member.member === voterAddress && member.isAdmin);
|
||||
|
||||
if (vote.optionIndex === 1) {
|
||||
isAdmin ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : null;
|
||||
} else if (vote.optionIndex === 0) {
|
||||
isAdmin ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : null;
|
||||
}
|
||||
});
|
||||
|
||||
const totalYes = adminYes + minterYes;
|
||||
const totalNo = adminNo + minterNo;
|
||||
|
||||
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo };
|
||||
};
|
||||
|
||||
const postComment = async (cardIdentifier) => {
|
||||
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
|
||||
const commentText = commentInput.value.trim();
|
||||
if (!commentText) {
|
||||
alert('Comment cannot be empty!');
|
||||
return;
|
||||
}
|
||||
|
||||
const commentData = {
|
||||
content: commentText,
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const commentIdentifier = `${cardIdentifier}-comment-${await uid()}`;
|
||||
|
||||
try {
|
||||
const base64CommentData = await objectToBase64(commentData);
|
||||
if (!base64CommentData) {
|
||||
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
||||
base64CommentData = btoa(JSON.stringify(commentData));
|
||||
}
|
||||
// const base64CommentData = btoa(JSON.stringify(commentData));
|
||||
await qortalRequest({
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: userState.accountName,
|
||||
service: 'BLOG_POST',
|
||||
identifier: commentIdentifier,
|
||||
data64: base64CommentData,
|
||||
});
|
||||
alert('Comment posted successfully!');
|
||||
commentInput.value = ''; // Clear input
|
||||
await displayComments(cardIdentifier); // Refresh comments
|
||||
} catch (error) {
|
||||
console.error('Error posting comment:', error);
|
||||
alert('Failed to post comment.');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCommentsForCard = async (cardIdentifier) => {
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: 'SEARCH_QDN_RESOURCES',
|
||||
service: 'BLOG_POST',
|
||||
query: `${cardIdentifier}-comment`,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching comments for ${cardIdentifier}:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const displayComments = async (cardIdentifier) => {
|
||||
try {
|
||||
const comments = await fetchCommentsForCard(cardIdentifier);
|
||||
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
|
||||
|
||||
// Clear previous comments
|
||||
commentsContainer.innerHTML = '';
|
||||
|
||||
// Fetch and display each comment
|
||||
for (const comment of comments) {
|
||||
const commentDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: comment.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: comment.identifier,
|
||||
});
|
||||
const timestamp = await timestampToHumanReadableDate(commentDataResponse.timestamp);
|
||||
const commentHTML = `
|
||||
<div class="comment" style="border: 1px solid gray; margin: 10px 0; padding: 10px; background: #1c1c1c;">
|
||||
<p><strong>${commentDataResponse.creator}</strong>:</p>
|
||||
<p>${commentDataResponse.content}</p>
|
||||
<p>${timestamp}</p>
|
||||
</div>
|
||||
`;
|
||||
commentsContainer.insertAdjacentHTML('beforeend', commentHTML);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error displaying comments for ${cardIdentifier}:`, error);
|
||||
alert("Failed to load comments. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const toggleComments = async (cardIdentifier) => {
|
||||
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
|
||||
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
|
||||
await displayComments(cardIdentifier);
|
||||
commentsSection.style.display = 'block';
|
||||
} else {
|
||||
commentsSection.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function loadCards() {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
cardsContainer.innerHTML = "<p>Loading cards...</p>";
|
||||
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
if (!response || response.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No cards found.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
cardsContainer.innerHTML = "";
|
||||
const pollResultsCache = {};
|
||||
|
||||
for (const card of response) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
});
|
||||
|
||||
const cardData = cardDataResponse;
|
||||
// Cache poll results
|
||||
if (!pollResultsCache[cardData.poll]) {
|
||||
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
|
||||
}
|
||||
|
||||
const pollResults = pollResultsCache[cardData.poll];
|
||||
const cardHTML = await createCardHTML(cardData, pollResults, card.identifier);
|
||||
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading cards:", error);
|
||||
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullContent(cardIdentifier, fullContent) {
|
||||
const contentPreview = document.getElementById(`content-preview-${cardIdentifier}`);
|
||||
const toggleButton = document.getElementById(`toggle-content-${cardIdentifier}`);
|
||||
const isExpanded = contentPreview.getAttribute("data-expanded") === "true";
|
||||
|
||||
if (isExpanded) {
|
||||
// Collapse the content
|
||||
contentPreview.innerText = `${fullContent.substring(0, 150)}...`;
|
||||
toggleButton.innerText = "Display Full Text";
|
||||
contentPreview.setAttribute("data-expanded", "false");
|
||||
} else {
|
||||
// Expand the content
|
||||
contentPreview.innerText = fullContent;
|
||||
toggleButton.innerText = "Show Less";
|
||||
contentPreview.setAttribute("data-expanded", "true");
|
||||
}
|
||||
}
|
||||
|
||||
async function createCardHTML(cardData, pollResults, cardIdentifier) {
|
||||
const { header, content, links, creator, timestamp, poll } = cardData;
|
||||
const formattedDate = new Date(timestamp).toLocaleString();
|
||||
const linksHTML = links.map((link, index) => `
|
||||
<button onclick="window.open('${link}', '_blank')">
|
||||
${`Link ${index + 1} - ${link}`}
|
||||
</button>
|
||||
`).join("");
|
||||
|
||||
const minterGroupMembers = await fetchMinterGroupMembers();
|
||||
const { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo } =
|
||||
calculatePollResults(pollResults, minterGroupMembers);
|
||||
|
||||
const trimmedContent = content.length > 150 ? `${content.substring(0, 150)}...` : content;
|
||||
|
||||
return `
|
||||
<div class="minter-card">
|
||||
<div class="minter-card-header">
|
||||
<h3>${creator}</h3>
|
||||
<p>${header}</p>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div id="content-preview-${cardIdentifier}" class="content-preview">
|
||||
${trimmedContent}
|
||||
</div>
|
||||
${
|
||||
content.length > 150
|
||||
? `<button id="toggle-content-${cardIdentifier}" class="toggle-content-button" onclick="toggleFullContent('${cardIdentifier}', '${content}')">Display Full Content</button>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<div class="info-links">
|
||||
${linksHTML}
|
||||
</div>
|
||||
<div class="minter-card-results">
|
||||
<div><h5>Current Support Results</h5></div>
|
||||
<div class="admin-results">
|
||||
<span class="admin-yes">Admin Yes: ${adminYes}</span>
|
||||
<span class="admin-no">Admin No: ${adminNo}</span>
|
||||
</div>
|
||||
<div class="minter-results">
|
||||
<span class="minter-yes">Minter Yes: ${minterYes}</span>
|
||||
<span class="minter-no">Minter No: ${minterNo}</span>
|
||||
</div>
|
||||
<div class="total-results">
|
||||
<span class="total-yes">Total Yes: ${totalYes}</span>
|
||||
<span class="total-no">Total No: ${totalNo}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div><h5>Support Minter</h5></div> <!-- Move this heading above the buttons -->
|
||||
<div class="button-group">
|
||||
<button class="yes" onclick="voteOnPoll('${poll}', 'Yes')">YES</button>
|
||||
<button class="comment" onclick="toggleComments('${cardIdentifier}')">COMMENT</button>
|
||||
<button class="no" onclick="voteOnPoll('${poll}', 'No')">NO</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
|
||||
<textarea id="new-comment-${cardIdentifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||
<button onclick="postComment('${cardIdentifier}')">Post Comment</button>
|
||||
</div>
|
||||
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@ -1,329 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once.
|
||||
|
||||
// If there is a previous latest message identifiers, use them. Otherwise, use an empty.
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
${(existingIdentifiers.size > 10)? '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>' : ''}
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "") {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
|
||||
// Create message object with unique identifier and HTML content
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: false,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
|
||||
console.log("Message published successfully");
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
|
||||
// Clear reply reference after sending if it exists.
|
||||
if (replyToMessageIdentifier) {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
}
|
||||
// Update the latest message identifier
|
||||
latestMessageIdentifiers[room] = messageIdentifier;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for the load more button
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load messages for any given room with pagination
|
||||
async function loadMessagesFromQDN(room, page, isPolling = false) {
|
||||
try {
|
||||
const offset = page * 10;
|
||||
const limit = 10;
|
||||
|
||||
// Get the set of existing identifiers from the messages container
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch only messages that are not already present in the messages container
|
||||
const response = await searchAllWithoutDuplicates(`${messageIdentifierPrefix}-${room}`, limit, offset, existingIdentifiers);
|
||||
|
||||
if (messagesContainer) {
|
||||
// If there are no messages and we're not polling, display "no messages" message
|
||||
if (!response || !response.length) {
|
||||
if (page === 0 && !isPolling) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all messages that haven't been fetched before
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render new messages without duplication
|
||||
fetchMessages.forEach((message) => {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
let mostRecentMessage = null;
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
|
||||
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Append new message to the end of the container
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
// Track the most recent message
|
||||
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage.timestamp)) {
|
||||
mostRecentMessage = message;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Update latestMessageIdentifiers for the room
|
||||
if (mostRecentMessage) {
|
||||
latestMessageIdentifiers[room] = {
|
||||
latestIdentifier: mostRecentMessage.identifier,
|
||||
latestTimestamp: mostRecentMessage.timestamp
|
||||
};
|
||||
}
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
const editor = document.querySelector(".ql-editor");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Polling function to check for new messages without clearing existing ones
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage, true);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
||||
|
@ -1,319 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
|
||||
// Load the latest message identifiers from local storage
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
// const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
|
||||
const backgroundImage = "url('/assets/images/background.jpg')"
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "") {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
|
||||
// Create message object with unique identifier and HTML content
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: false,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
replyToMessageIdentifier = null; // Clear reply reference after sending
|
||||
// Clear reply reference after sending if it exists.
|
||||
if (replyToMessageIdentifier) {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
}
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
// Update the latest message identifier
|
||||
latestMessageIdentifiers[room] = messageIdentifier;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
// Reload messages
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for the load more button
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load messages for any given room with pagination
|
||||
async function loadMessagesFromQDN(room, page) {
|
||||
try {
|
||||
const offset = page * 10;
|
||||
const limit = 10;
|
||||
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, limit, 0, false);
|
||||
|
||||
const qdnMessages = response;
|
||||
console.log("Messages fetched successfully:", qdnMessages);
|
||||
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (messagesContainer) {
|
||||
if (!qdnMessages || !qdnMessages.length) {
|
||||
if (page === 0) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all messages
|
||||
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render messages without duplication
|
||||
const existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
fetchMessages.forEach((message) => {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
|
||||
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
}
|
||||
});
|
||||
|
||||
// Only scroll to bottom if user is not interacting with the input section
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
if (document.activeElement !== messageInputSection) {
|
||||
setTimeout(() => {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Polling function to check for new messages
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
@ -133,6 +133,7 @@ const loadCards = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate cards
|
||||
const validatedCards = await Promise.all(
|
||||
response.map(async card => {
|
||||
console.log("Validating card:", card);
|
||||
@ -141,14 +142,28 @@ const loadCards = async () => {
|
||||
})
|
||||
);
|
||||
|
||||
// Filter valid cards
|
||||
const validCards = validatedCards.filter(card => card !== null);
|
||||
cardsContainer.innerHTML = "";
|
||||
|
||||
if (validCards.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No valid cards found.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort valid cards by timestamp (oldest to newest)
|
||||
validCards.sort((a, b) => {
|
||||
const timestampA = a.updated || a.created || 0; // Use updated or created timestamp
|
||||
const timestampB = b.updated || b.created || 0;
|
||||
return timestampA - timestampB;
|
||||
});
|
||||
|
||||
// Reverse the sorted order (newest first)
|
||||
validCards.reverse();
|
||||
|
||||
// Clear the container before adding cards
|
||||
cardsContainer.innerHTML = "";
|
||||
|
||||
// Process and display each card
|
||||
await Promise.all(
|
||||
validCards.map(async card => {
|
||||
try {
|
||||
@ -165,9 +180,16 @@ const loadCards = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch poll results and check admin votes
|
||||
const pollResults = await fetchPollResults(cardData.poll);
|
||||
console.log(`Poll Results Fetched - totalVotes: ${pollResults.totalVotes}`);
|
||||
|
||||
// Check if more than 3 admins voted "No"
|
||||
if (pollResults.adminNo > 3) {
|
||||
console.log(`Card ${card.identifier} hidden due to more than 3 admin downvotes.`);
|
||||
return; // Skip this card
|
||||
}
|
||||
|
||||
const cardHTML = await createCardHTML(cardData, pollResults, card.identifier);
|
||||
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
|
||||
} catch (error) {
|
||||
@ -182,6 +204,8 @@ const loadCards = async () => {
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// Function to check and fech an existing Minter Card if attempting to publish twice ----------------------------------------
|
||||
const fetchExistingCard = async () => {
|
||||
try {
|
||||
@ -339,7 +363,11 @@ const publishCard = async () => {
|
||||
//Calculate the poll results passed from other functions with minterGroupMembers and minterAdmins ---------------------------
|
||||
const calculatePollResults = async (pollData, minterGroupMembers, minterAdmins) => {
|
||||
const memberAddresses = minterGroupMembers.map(member => member.member)
|
||||
const adminAddresses = minterAdmins.map(member => member.member)
|
||||
const minterAdminAddresses = minterAdmins.map(member => member.member)
|
||||
const adminGroupsMembers = await fetchAllAdminGroupsMembers()
|
||||
const groupAdminAddresses = adminGroupsMembers.map(member => member.member)
|
||||
const adminAddresses = [];
|
||||
adminAddresses.push(...minterAdminAddresses,...groupAdminAddresses);
|
||||
|
||||
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, yesWeight = 0 , noWeight = 0
|
||||
|
||||
@ -408,7 +436,7 @@ const postComment = async (cardIdentifier) => {
|
||||
|
||||
alert('Comment posted successfully!');
|
||||
commentInput.value = ''; // Clear input
|
||||
await displayComments(cardIdentifier); // Refresh comments
|
||||
// await displayComments(cardIdentifier); // Refresh comments - We don't need to do this as comments will be displayed only after confirmation.
|
||||
} catch (error) {
|
||||
console.error('Error posting comment:', error);
|
||||
alert('Failed to post comment.');
|
||||
|
@ -1,5 +1,6 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
|
||||
let adminPublicKeys = []
|
||||
|
||||
// NOTE - SET adminGroups in QortalApi.js to enable admin access to forum for specific groups. Minter Admins will be fetched automatically.
|
||||
|
||||
@ -42,14 +43,25 @@ const loadForumPage = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof userState.isAdmin === 'undefined') {
|
||||
try {
|
||||
// Fetch and verify the admin status asynchronously
|
||||
userState.isAdmin = await verifyUserIsAdmin();
|
||||
} catch (error) {
|
||||
console.error('Error verifying admin status:', error);
|
||||
userState.isAdmin = false; // Default to non-admin if there's an issue
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`;
|
||||
const isAdmin = userState.isAdmin;
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background image: style="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background cid-ttRnlSkg2R">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: center; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: white; display: flex; align-items: center; justify-content: center;">
|
||||
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
|
||||
<span>${userState.accountName || 'Guest'}</span>
|
||||
</div>
|
||||
@ -57,7 +69,7 @@ const loadForumPage = async () => {
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${userState.isAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
${isAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -85,7 +97,7 @@ const loadForumPage = async () => {
|
||||
}
|
||||
|
||||
// Function to add the pagination buttons and related control mechanisms ------------------------
|
||||
const renderPaginationControls = async(room, totalMessages, limit) => {
|
||||
const renderPaginationControls = (room, totalMessages, limit) => {
|
||||
const paginationContainer = document.getElementById("pagination-container");
|
||||
if (!paginationContainer) return;
|
||||
|
||||
@ -301,71 +313,110 @@ const setupFileInputs = (room) => {
|
||||
const processSelectedImages = async (selectedImages, multiResource, room) => {
|
||||
|
||||
for (const file of selectedImages) {
|
||||
let attachmentID = generateAttachmentID(room, selectedImages.indexOf(file))
|
||||
try {
|
||||
multiResource.push({
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
file
|
||||
});
|
||||
attachmentIdentifiers.push({
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
filename: file.name,
|
||||
mimeType: file.type
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error processing image ${file.name}:`, error);
|
||||
}
|
||||
const attachmentID = generateAttachmentID(room, selectedImages.indexOf(file));
|
||||
|
||||
multiResource.push({
|
||||
name: userState.accountName,
|
||||
service: room === "admins" ? "FILE_PRIVATE" : "FILE",
|
||||
identifier: attachmentID,
|
||||
file: file, // Use encrypted file for admins
|
||||
});
|
||||
|
||||
attachmentIdentifiers.push({
|
||||
name: userState.accountName,
|
||||
service: room === "admins" ? "FILE_PRIVATE" : "FILE",
|
||||
identifier: attachmentID,
|
||||
filename: file.name,
|
||||
mimeType: file.type,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle send message
|
||||
const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImages, multiResource) => {
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${Date.now()}`;
|
||||
const messageIdentifier = room === "admins"
|
||||
? `${messageIdentifierPrefix}-${room}-e-${Date.now()}`
|
||||
: `${messageIdentifierPrefix}-${room}-${Date.now()}`;
|
||||
|
||||
const adminPublicKeys = room === "admins" && userState.isAdmin
|
||||
? await fetchAdminGroupsMembersPublicKeys()
|
||||
: [];
|
||||
|
||||
try {
|
||||
// Process selected images
|
||||
if (selectedImages.length > 0) {
|
||||
await processSelectedImages(selectedImages, multiResource, room);
|
||||
}
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
let attachmentID = generateAttachmentID(room, selectedFiles.indexOf(file))
|
||||
multiResource.push({
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
file
|
||||
});
|
||||
attachmentIdentifiers.push({
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
filename: file.name,
|
||||
mimeType: file.type
|
||||
})
|
||||
// Process selected files
|
||||
if (selectedFiles && selectedFiles.length > 0) {
|
||||
for (const file of selectedFiles) {
|
||||
const attachmentID = generateAttachmentID(room, selectedFiles.indexOf(file));
|
||||
|
||||
multiResource.push({
|
||||
name: userState.accountName,
|
||||
service: room === "admins" ? "FILE_PRIVATE" : "FILE",
|
||||
identifier: attachmentID,
|
||||
file: file, // Use encrypted file for admins
|
||||
});
|
||||
|
||||
attachmentIdentifiers.push({
|
||||
name: userState.accountName,
|
||||
service: room === "admins" ? "FILE_PRIVATE" : "FILE",
|
||||
identifier: attachmentID,
|
||||
filename: file.name,
|
||||
mimeType: file.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build the message object
|
||||
const messageObject = {
|
||||
messageHtml,
|
||||
hasAttachment: multiResource.length > 0,
|
||||
attachments: attachmentIdentifiers,
|
||||
replyTo: replyToMessageIdentifier || null // Add replyTo
|
||||
replyTo: replyToMessageIdentifier || null, // Include replyTo if applicable
|
||||
};
|
||||
|
||||
const base64Message = btoa(JSON.stringify(messageObject));
|
||||
// Encode the message object
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
multiResource.push({
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
if (room === "admins" && userState.isAdmin) {
|
||||
console.log("Encrypting message for admins...");
|
||||
|
||||
multiResource.push({
|
||||
name: userState.accountName,
|
||||
service: "MAIL_PRIVATE",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message,
|
||||
});
|
||||
} else {
|
||||
multiResource.push({
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message,
|
||||
});
|
||||
}
|
||||
|
||||
await publishMultipleResources(multiResource);
|
||||
// Publish resources
|
||||
if (room === "admins") {
|
||||
if (!userState.isAdmin || adminPublicKeys.length === 0) {
|
||||
console.error("User is not an admin or no admin public keys found. Aborting publish.");
|
||||
window.alert("You are not authorized to post in the Admin room.");
|
||||
return;
|
||||
}
|
||||
console.log("Publishing encrypted resources for Admin room...");
|
||||
await publishMultipleResources(multiResource, adminPublicKeys, true);
|
||||
} else {
|
||||
console.log("Publishing resources for non-admin room...");
|
||||
await publishMultipleResources(multiResource);
|
||||
}
|
||||
|
||||
// Clear inputs and show success notification
|
||||
clearInputs();
|
||||
showSuccessNotification();
|
||||
} catch (error) {
|
||||
@ -373,6 +424,8 @@ const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImage
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Modify clearInputs to reset replyTo
|
||||
const clearInputs = () => {
|
||||
const quill = new Quill('#editor');
|
||||
@ -407,11 +460,31 @@ const showSuccessNotification = () => {
|
||||
|
||||
// Generate unique attachment ID
|
||||
const generateAttachmentID = (room, fileIndex = null) => {
|
||||
const baseID = `${messageAttachmentIdentifierPrefix}-${room}-${Date.now()}`;
|
||||
const baseID = room === "admins" ? `${messageAttachmentIdentifierPrefix}-${room}-e-${Date.now()}` : `${messageAttachmentIdentifierPrefix}-${room}-${Date.now()}`;
|
||||
return fileIndex !== null ? `${baseID}-${fileIndex}` : baseID;
|
||||
};
|
||||
|
||||
const decryptObject = async (encryptedData) => {
|
||||
// const publicKey = await getPublicKeyFromAddress(userState.accountAddress)
|
||||
const response = await qortalRequest({
|
||||
action: 'DECRYPT_DATA',
|
||||
encryptedData, // has to be in base64 format
|
||||
// publicKey: publisherPublicKey // requires the public key of the opposite user with whom you've created the encrypted data. For DIRECT messages only.
|
||||
});
|
||||
const decryptedObject = response
|
||||
return decryptedObject
|
||||
}
|
||||
|
||||
const decryptFile = async (encryptedData) => {
|
||||
const publicKey = await getPublicKeyByName(userState.accountName)
|
||||
const response = await qortalRequest({
|
||||
action: 'DECRYPT_DATA',
|
||||
encryptedData, // has to be in base64 format
|
||||
// publicKey: publicKey // requires the public key of the opposite user with whom you've created the encrypted data.
|
||||
});
|
||||
const decryptedObject = response
|
||||
return decryptedObject
|
||||
}
|
||||
|
||||
|
||||
const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
||||
@ -434,7 +507,10 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
||||
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch messages for the current room and page
|
||||
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
|
||||
const service = room === "admins" ? "MAIL_PRIVATE" : "BLOG_POST"
|
||||
const query = room === "admins" ? `${messageIdentifierPrefix}-${room}-e` : `${messageIdentifierPrefix}-${room}`
|
||||
|
||||
const response = await searchAllWithOffset(service, query, limit, offset, room);
|
||||
console.log(`Fetched messages count: ${response.length} for page: ${page}`);
|
||||
|
||||
if (response.length === 0) {
|
||||
@ -447,46 +523,82 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
||||
|
||||
// Define `mostRecentMessage` to track the latest message during this fetch
|
||||
let mostRecentMessage = latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
|
||||
let firstNewMessageIdentifier = null
|
||||
|
||||
// Fetch all messages that haven't been fetched before
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
if (existingIdentifiers.has(resource.identifier)) {
|
||||
return null; // Skip messages that are already displayed
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
service,
|
||||
identifier: resource.identifier,
|
||||
...(room === "admins" ? { encoding: "base64" } : {}),
|
||||
});
|
||||
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return {
|
||||
name: resource.name,
|
||||
content: messageObject.messageHtml,
|
||||
date: formattedTimestamp,
|
||||
identifier: resource.identifier,
|
||||
replyTo: messageObject.replyTo,
|
||||
timestamp,
|
||||
attachments: messageObject.attachments || [] // Include attachments if they exist
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
let messageObject;
|
||||
|
||||
if (room === "admins") {
|
||||
try {
|
||||
const decryptedData = await decryptObject(messageResponse);
|
||||
messageObject = JSON.parse(atob(decryptedData))
|
||||
} catch (error) {
|
||||
console.error(`Failed to decrypt message: ${error.message}`);
|
||||
return {
|
||||
name: resource.name,
|
||||
content: "<em>Encrypted message cannot be displayed</em>",
|
||||
date: formattedTimestamp,
|
||||
identifier: resource.identifier,
|
||||
replyTo: null,
|
||||
timestamp,
|
||||
attachments: [],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
messageObject = messageResponse;
|
||||
}
|
||||
|
||||
return {
|
||||
name: resource.name,
|
||||
content: messageObject?.messageHtml || "<em>Message content missing</em>",
|
||||
date: formattedTimestamp,
|
||||
identifier: resource.identifier,
|
||||
replyTo: messageObject?.replyTo || null,
|
||||
timestamp,
|
||||
attachments: messageObject?.attachments || [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return {
|
||||
name: resource.name,
|
||||
content: "<em>Error loading message</em>",
|
||||
date: "Unknown",
|
||||
identifier: resource.identifier,
|
||||
replyTo: null,
|
||||
timestamp: resource.updated || resource.created,
|
||||
attachments: [],
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Render new messages without duplication
|
||||
for (const message of fetchMessages) {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
const isNewMessage = !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
|
||||
if (isNewMessage && !firstNewMessageIdentifier) {
|
||||
firstNewMessageIdentifier = message.identifier;
|
||||
}
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
@ -500,12 +612,10 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
|
||||
|
||||
let attachmentHtml = "";
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
if (attachment.mimeType && attachment.mimeType.startsWith('image/')) {
|
||||
if (room !== "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
|
||||
try {
|
||||
// Construct the image URL
|
||||
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
|
||||
@ -538,7 +648,6 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
@ -546,6 +655,7 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
||||
<div style="display: flex; align-items: center;">
|
||||
<img src="${avatarUrl}" alt="Avatar" class="user-avatar" style="width: 30px; height: 30px; border-radius: 50%; margin-right: 10px;">
|
||||
<span class="username">${message.name}</span>
|
||||
${isNewMessage ? `<span class="new-indicator" style="margin-left: 10px; color: red; font-weight: bold;">NEW</span>` : ''}
|
||||
</div>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
</div>
|
||||
@ -574,6 +684,14 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (firstNewMessageIdentifier && !isPolling) {
|
||||
// Scroll to the first new message
|
||||
const newMessageElement = document.querySelector(`.message-item[data-identifier="${firstNewMessageIdentifier}"]`);
|
||||
if (newMessageElement) {
|
||||
newMessageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
|
||||
// Update latestMessageIdentifiers for the room
|
||||
if (mostRecentMessage) {
|
||||
latestMessageIdentifiers[room] = mostRecentMessage;
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Set the forumAdminGroups variable
|
||||
let adminGroups = ["Q-Mintership-admin", "dev-group", "Mintership-Forum-Admins"];
|
||||
|
||||
let adminGroupIDs = ["721", "1", "673"];
|
||||
// Settings to allow non-devmode development with 'live-server' module
|
||||
let baseUrl = '';
|
||||
let isOutsideOfUiDevelopment = false;
|
||||
@ -344,6 +344,28 @@ const fetchMinterGroupAdmins = async () => {
|
||||
//use what is returned .member to obtain each member... {"member": "memberAddress", "isAdmin": "true"}
|
||||
}
|
||||
|
||||
const fetchAllAdminGroupsMembers = async () => {
|
||||
try {
|
||||
let adminGroupMemberAddresses = []; // Declare outside loop to accumulate results
|
||||
for (const groupID of adminGroupIDs) {
|
||||
const response = await fetch(`${baseUrl}/groups/members/${groupID}?limit=0`, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
});
|
||||
|
||||
const groupData = await response.json();
|
||||
if (groupData.members && Array.isArray(groupData.members)) {
|
||||
adminGroupMemberAddresses.push(...groupData.members); // Merge members into the array
|
||||
} else {
|
||||
console.warn(`Group ${groupID} did not return valid members.`);
|
||||
}
|
||||
}
|
||||
return adminGroupMemberAddresses;
|
||||
} catch (error) {
|
||||
console.log('Error fetching admin group members', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMinterGroupMembers = async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/groups/members/694?limit=0`, {
|
||||
@ -389,6 +411,48 @@ const fetchAllGroups = async (limit) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAdminGroupsMembersPublicKeys = async () => {
|
||||
try {
|
||||
let adminGroupMemberAddresses = []; // Declare outside loop to accumulate results
|
||||
for (const groupID of adminGroupIDs) {
|
||||
const response = await fetch(`${baseUrl}/groups/members/${groupID}?limit=0`, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
});
|
||||
|
||||
const groupData = await response.json();
|
||||
if (groupData.members && Array.isArray(groupData.members)) {
|
||||
adminGroupMemberAddresses.push(...groupData.members); // Merge members into the array
|
||||
} else {
|
||||
console.warn(`Group ${groupID} did not return valid members.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if adminGroupMemberAddresses has valid data
|
||||
if (!Array.isArray(adminGroupMemberAddresses)) {
|
||||
throw new Error("Expected 'adminGroupMemberAddresses' to be an array but got a different structure");
|
||||
}
|
||||
|
||||
let allMemberPublicKeys = []; // Declare outside loop to accumulate results
|
||||
for (const member of adminGroupMemberAddresses) {
|
||||
const memberPublicKey = await getPublicKeyFromAddress(member.member);
|
||||
allMemberPublicKeys.push(memberPublicKey);
|
||||
}
|
||||
|
||||
// Check if allMemberPublicKeys has valid data
|
||||
if (!Array.isArray(allMemberPublicKeys)) {
|
||||
throw new Error("Expected 'allMemberPublicKeys' to be an array but got a different structure");
|
||||
}
|
||||
|
||||
console.log(`AdminGroupMemberPublicKeys have been fetched.`);
|
||||
return allMemberPublicKeys;
|
||||
} catch (error) {
|
||||
console.error('Error fetching admin group member public keys:', error);
|
||||
return []; // Return an empty array to prevent further errors
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// QDN data calls
|
||||
const searchLatestDataByIdentifier = async (identifier) => {
|
||||
console.log('fetchAllDataByIdentifier called');
|
||||
@ -476,23 +540,45 @@ const searchAllResources = async (query, limit, after, reverse=false) => {
|
||||
}
|
||||
};
|
||||
|
||||
const searchAllWithOffset = async (query, limit, offset) =>{
|
||||
const searchAllWithOffset = async (service, query, limit, offset, room) => {
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: query,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
mode: "ALL",
|
||||
reverse: false
|
||||
});
|
||||
return response
|
||||
if (!service || (service === "BLOG_POST" && room !== "admins")) {
|
||||
console.log("Performing search for BLOG_POST...");
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
mode: "ALL",
|
||||
reverse: false,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
if (room === "admins") {
|
||||
service = service || "MAIL_PRIVATE"; // Default to MAIL_PRIVATE if no service provided
|
||||
console.log("Performing search for MAIL_PRIVATE in Admin room...");
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service,
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
mode: "ALL",
|
||||
reverse: false,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
console.warn("Invalid parameters passed to searchAllWithOffset");
|
||||
return []; // Return empty array if no valid conditions match
|
||||
} catch (error) {
|
||||
console.error("Error during SEARCH_QDN_RESOURCES:", error);
|
||||
return [];
|
||||
return []; // Return empty array on error
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const searchAllCountOnly = async (query) => {
|
||||
try {
|
||||
|
@ -1,305 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
|
||||
// Load the latest message identifiers from local storage
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "") {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
|
||||
// Create message object with unique identifier and HTML content
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: false,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
replyToMessageIdentifier = null; // Clear reply reference after sending
|
||||
// Clear reply reference after sending if it exists.
|
||||
if (replyToMessageIdentifier) {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
}
|
||||
// Update the latest message identifier
|
||||
latestMessageIdentifiers[room] = messageIdentifier;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
// Reload messages
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for the load more button
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load messages for any given room with pagination
|
||||
async function loadMessagesFromQDN(room, page) {
|
||||
try {
|
||||
const offset = page * 10;
|
||||
const limit = 10;
|
||||
|
||||
// Get the set of existing identifiers from the messages container
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
const existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch only messages that are not already present in the messages container
|
||||
const response = await searchAllWithoutDuplicates(`${messageIdentifierPrefix}-${room}`, limit, offset, existingIdentifiers);
|
||||
|
||||
if (messagesContainer) {
|
||||
if (!response || !response.length) {
|
||||
if (page === 0) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all messages
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render messages without duplication
|
||||
fetchMessages.forEach((message) => {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
|
||||
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Polling function to check for new messages
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
@ -1,392 +0,0 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once.
|
||||
|
||||
// If there is a previous latest message identifiers, use them. Otherwise, use an empty.
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
${(existingIdentifiers.size > 10)? '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>' : ''}
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<div class="attachment-section">
|
||||
<input type="file" id="file-input" class="file-input" multiple>
|
||||
<button id="attach-button" class="attach-button">Attach Files</button>
|
||||
</div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
let selectedFiles = [];
|
||||
|
||||
// Add event listener to handle file selection
|
||||
document.getElementById('file-input').addEventListener('change', (event) => {
|
||||
selectedFiles = Array.from(event.target.files);
|
||||
});
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "" || selectedFiles.length > 0) {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
let attachmentIdentifiers = [];
|
||||
|
||||
// Handle attachments
|
||||
for (const file of selectedFiles) {
|
||||
const attachmentID = `${messageAttachmentIdentifierPrefix}-${randomID}-${file.name}`;
|
||||
try {
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "IMAGE", // Adjust based on file type
|
||||
identifier: attachmentID,
|
||||
file: file
|
||||
});
|
||||
attachmentIdentifiers.push(attachmentID);
|
||||
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
|
||||
} catch (error) {
|
||||
console.error(`Error publishing attachment ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create message object with unique identifier, HTML content, and attachments
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: attachmentIdentifiers.length > 0,
|
||||
attachments: attachmentIdentifiers,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
if (!messageObject.attachments) {
|
||||
messageObject.attachments = [];
|
||||
}
|
||||
messageObject.attachments.push({
|
||||
identifier: attachmentIdentifier,
|
||||
filename: file.name,
|
||||
mimeType: file.type
|
||||
});
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
replyToMessageIdentifier = null;
|
||||
document.getElementById('file-input').value = "";
|
||||
selectedFiles = [];
|
||||
|
||||
// Clear reply reference after sending if it exists.
|
||||
if (replyToMessageIdentifier) {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
}
|
||||
|
||||
// Update the latest message identifier
|
||||
latestMessageIdentifiers[room] = messageIdentifier;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
|
||||
// Reload messages - - - - - - - - - - - - - - - - - - CHANGE THIS TO DISPLAY THE LAST MESSAGE IN THE CONTAINER INSTEAD OF RELOADING ALL MESSAGES, AND DISPLAY NOTIFICATION OF SUCCESSFUL PUBLISH.
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for the load more button
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load messages for any given room with pagination
|
||||
async function loadMessagesFromQDN(room, page, isPolling = false) {
|
||||
try {
|
||||
const offset = page * 10;
|
||||
const limit = 10;
|
||||
|
||||
// Get the set of existing identifiers from the messages container
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch only messages that are not already present in the messages container
|
||||
const response = await searchAllWithoutDuplicates(`${messageIdentifierPrefix}-${room}`, limit, offset, existingIdentifiers);
|
||||
|
||||
if (messagesContainer) {
|
||||
// If there are no messages and we're not polling, display "no messages" message
|
||||
if (!response || !response.length) {
|
||||
if (page === 0 && !isPolling) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all messages that haven't been fetched before
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render new messages without duplication
|
||||
fetchMessages.forEach((message) => {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
let mostRecentMessage = null;
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
|
||||
|
||||
let attachmentHtml = "";
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
if (attachment.mimeType.startsWith('image/')) {
|
||||
// Display images inline
|
||||
const url = `/arbitrary/${attachment.service}/${message.name}/${attachment.identifier}`;
|
||||
attachmentHtml += `<div class="attachment"><img src="${url}" alt="${attachment.filename}" class="inline-image"></div>`;
|
||||
} else {
|
||||
// Display a button to download other attachments
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<button onclick="fetchAttachment('${attachment.service}', '${message.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
${attachmentHtml}
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Append new message to the end of the container
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
// Track the most recent message
|
||||
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage.timestamp)) {
|
||||
mostRecentMessage = message;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Update latestMessageIdentifiers for the room
|
||||
if (mostRecentMessage) {
|
||||
latestMessageIdentifiers[room] = {
|
||||
latestIdentifier: mostRecentMessage.identifier,
|
||||
latestTimestamp: mostRecentMessage.timestamp
|
||||
};
|
||||
}
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
const editor = document.querySelector(".ql-editor");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Polling function to check for new messages without clearing existing ones
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage, true);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user