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:
crowetic 2024-12-14 19:40:31 -08:00
parent f2887c7a6a
commit eaecae79c7
34 changed files with 340 additions and 9573 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.vscode
/.sync*

View File

@ -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...&nbsp;</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.&nbsp;</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>

View File

@ -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...&nbsp;</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.&nbsp;</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>

View File

@ -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;
}

View File

@ -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">

View File

@ -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).`);
}

View File

@ -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).`);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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>
`;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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>
`
}

View File

@ -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>
`
}

View File

@ -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>
`;
}

View File

@ -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;
}

View File

@ -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>
`;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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.');

View File

@ -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;

View File

@ -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 {

View File

@ -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);
}

View File

@ -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);
}