5226 lines
322 KiB
HTML
5226 lines
322 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>RanchiMall Messenger</title>
|
|
<link rel="shortcut icon" href="assets/messenger-favicon_1.png" type="image/png">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700;900&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="css/main.min.css">
|
|
<script id="floGlobals">
|
|
/* Constants for FLO blockchain operations !!Make sure to add this at beginning!! */
|
|
const floGlobals = {
|
|
blockchain: "FLO",
|
|
application: "messenger",
|
|
adminID: "FMRsefPydWznGWneLqi4ABeQAJeFvtS3aQ"
|
|
}
|
|
</script>
|
|
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
|
|
<script src="scripts/lib.js"></script>
|
|
<script src="scripts/floCrypto.js"></script>
|
|
<script src="scripts/btcOperator.js"></script>
|
|
<script src="scripts/floBlockchainAPI.js"></script>
|
|
<script src="scripts/compactIDB.js"></script>
|
|
<script src="scripts/floCloudAPI.js"></script>
|
|
<script src="scripts/floDapps.js"></script>
|
|
<script src="scripts/keccak.js"></script>
|
|
<script src="scripts/messengerEthereum.js"></script>
|
|
<script src="scripts/messenger.js"></script>
|
|
<script id="onLoadStartUp">
|
|
function onLoadStartUp() {
|
|
routeTo('loading')
|
|
document.body.classList.remove('hidden')
|
|
|
|
floDapps.setCustomPrivKeyInput(getSignedIn)
|
|
getRef('emoji_picker').shadowRoot.append(style);
|
|
//invoke the startup functions
|
|
floDapps.launchStartUp().then(result => {
|
|
console.log(result)
|
|
floGlobals.myFloID = floCrypto.getFloID(floDapps.user.public);
|
|
floGlobals.myBtcID = btcOperator.convert.legacy2bech(floGlobals.myFloID)
|
|
floGlobals.myEthID = floEthereum.ethAddressFromCompressedPublicKey(floDapps.user.public)
|
|
document.querySelectorAll('.user-profile-id').forEach(el => el.textContent = floGlobals.myFloID)
|
|
//load messages from IDB and render them
|
|
console.log(`Loading Data! Please Wait...`)
|
|
//Set UI render functions
|
|
messenger.renderUI.chats = renderChatList;
|
|
messenger.renderUI.directChat = renderDirectUI;
|
|
messenger.renderUI.groupChat = renderGroupUI;
|
|
messenger.renderUI.pipeline = renderPipelineUI;
|
|
messenger.renderUI.mails = m => renderMailList(m, false);
|
|
messenger.renderUI.marked = renderMarked;
|
|
//init messenger
|
|
messenger.init().then(result => {
|
|
console.log(result);
|
|
//Check for available bg image
|
|
setBgImage();
|
|
routeTo(window.location.hash, { firstLoad: true })
|
|
floGlobals.loaded = true;
|
|
}).catch(error => {
|
|
let isBrave = navigator.brave !== undefined
|
|
if (error === 'Cloud offline') {
|
|
document.body.prepend(document.createElement('adblocker-warning'))
|
|
} else if (error === "App database initiation failed") {
|
|
|
|
} else {
|
|
notify(error, "error")
|
|
}
|
|
})
|
|
}).catch(error => notify(error, "error"))
|
|
}
|
|
</script>
|
|
</head>
|
|
|
|
<body data-theme="dark" onload="onLoadStartUp()" class="hidden">
|
|
<idb-support></idb-support>
|
|
<sm-notifications id="notification_drawer"></sm-notifications>
|
|
<sm-popup id="confirmation_popup">
|
|
<h4 id="confirm_title"></h4>
|
|
<p id="confirm_message"></p>
|
|
<div class="flex align-center gap-0-5 margin-left-auto">
|
|
<button class="button cancel-button">Cancel</button>
|
|
<button class="button button--primary confirm-button">OK</button>
|
|
</div>
|
|
</sm-popup>
|
|
<sm-popup id="prompt_popup">
|
|
<h4 id="prompt_title"></h4>
|
|
<p id="prompt_message"></p>
|
|
<sm-form>
|
|
<sm-input id="prompt_input" required></sm-input>
|
|
<div class="flex align-center gap-0-5 margin-left-auto">
|
|
<button class="button cancel-button">Cancel</button>
|
|
<button class="button confirm-button button--primary" type="submit">OK</button>
|
|
</div>
|
|
</sm-form>
|
|
</sm-popup>
|
|
<div id="adblocker_warning"></div>
|
|
<div id="secondary_pages" class="page hidden">
|
|
<header class="flex align-center gap-1 space-between">
|
|
<div class="app-brand">
|
|
<svg id="main_logo" class="icon" viewBox="0 0 27.25 32">
|
|
<title>RanchiMall</title>
|
|
<path
|
|
d="M27.14,30.86c-.74-2.48-3-4.36-8.25-6.94a20,20,0,0,1-4.2-2.49,6,6,0,0,1-1.25-1.67,4,4,0,0,1,0-2.26c.37-1.08.79-1.57,3.89-4.55a11.66,11.66,0,0,0,3.34-4.67,6.54,6.54,0,0,0,.05-2.82C20,3.6,18.58,2,16.16.49c-.89-.56-1.29-.64-1.3-.24a3,3,0,0,1-.3.72l-.3.55L13.42.94C13,.62,12.4.26,12.19.15c-.4-.2-.73-.18-.72.05a9.39,9.39,0,0,1-.61,1.33s-.14,0-.27-.13C8.76.09,8-.27,8,.23A11.73,11.73,0,0,1,6.76,2.6C4.81,5.87,2.83,7.49.77,7.49c-.89,0-.88,0-.61,1,.22.85.33.92,1.09.69A5.29,5.29,0,0,0,3,8.33c.23-.17.45-.29.49-.26a2,2,0,0,1,.22.63A1.31,1.31,0,0,0,4,9.34a5.62,5.62,0,0,0,2.27-.87L7,8l.13.55c.19.74.32.82,1,.65a7.06,7.06,0,0,0,3.46-2.47l.6-.71-.06.64c-.17,1.63-1.3,3.42-3.39,5.42L6.73,14c-3.21,3.06-3,5.59.6,8a46.77,46.77,0,0,0,4.6,2.41c.28.13,1,.52,1.59.87,3.31,2,4.95,3.92,4.95,5.93a2.49,2.49,0,0,0,.07.77h0c.09.09,0,.1.9-.14a2.61,2.61,0,0,0,.83-.32,3.69,3.69,0,0,0-.55-1.83A11.14,11.14,0,0,0,17,26.81a35.7,35.7,0,0,0-5.1-2.91C9.37,22.64,8.38,22,7.52,21.17a3.53,3.53,0,0,1-1.18-2.48c0-1.38.71-2.58,2.5-4.23,2.84-2.6,3.92-3.91,4.67-5.65a3.64,3.64,0,0,0,.42-2A3.37,3.37,0,0,0,13.61,5l-.32-.74.29-.48c.17-.27.37-.63.46-.8l.15-.3.44.64a5.92,5.92,0,0,1,1,2.81,5.86,5.86,0,0,1-.42,1.94c0,.12-.12.3-.15.4a9.49,9.49,0,0,1-.67,1.1,28,28,0,0,1-4,4.29C8.62,15.49,8.05,16.44,8,17.78a3.28,3.28,0,0,0,1.11,2.76c.95,1,2.07,1.74,5.25,3.32,3.64,1.82,5.22,2.9,6.41,4.38A4.78,4.78,0,0,1,21.94,31a3.21,3.21,0,0,0,.14.92,1.06,1.06,0,0,0,.43-.05l.83-.22.46-.12-.06-.46c-.21-1.53-1.62-3.25-3.94-4.8a37.57,37.57,0,0,0-5.22-2.82A13.36,13.36,0,0,1,11,21.19a3.36,3.36,0,0,1-.8-4.19c.41-.85.83-1.31,3.77-4.15,2.39-2.31,3.43-4.13,3.43-6a5.85,5.85,0,0,0-2.08-4.29c-.23-.21-.44-.43-.65-.65A2.5,2.5,0,0,1,15.27.69a10.6,10.6,0,0,1,2.91,2.78A4.16,4.16,0,0,1,19,6.16a4.91,4.91,0,0,1-.87,3c-.71,1.22-1.26,1.82-4.27,4.67a9.47,9.47,0,0,0-2.07,2.6,2.76,2.76,0,0,0-.33,1.54,2.76,2.76,0,0,0,.29,1.47c.57,1.21,2.23,2.55,4.65,3.73a32.41,32.41,0,0,1,5.82,3.24c2.16,1.6,3.2,3.16,3.2,4.8a1.94,1.94,0,0,0,.09.76,4.54,4.54,0,0,0,1.66-.4C27.29,31.42,27.29,31.37,27.14,30.86ZM6.1,7h0a3.77,3.77,0,0,1-1.46.45L4,7.51l.68-.83a25.09,25.09,0,0,0,3-4.82A12,12,0,0,1,8.28.76c.11-.12.77.32,1.53,1l.63.58-.57.84A10.34,10.34,0,0,1,6.1,7Zm5.71-1.78A9.77,9.77,0,0,1,9.24,7.18h0a5.25,5.25,0,0,1-1.17.28l-.58,0,.65-.78a21.29,21.29,0,0,0,2.1-3.12c.22-.41.42-.76.44-.79s.5.43.9,1.24L12,5ZM13.41,3a2.84,2.84,0,0,1-.45.64,11,11,0,0,1-.9-.91l-.84-.9.19-.45c.34-.79.39-.8,1-.31A9.4,9.4,0,0,1,13.8,2.33q-.18.34-.39.69Z" />
|
|
</svg>
|
|
<div class="app-name">
|
|
<div class="app-name__company">RanchiMall</div>
|
|
<h4 class="app-name__title">
|
|
Messenger
|
|
</h4>
|
|
</div>
|
|
</div>
|
|
<theme-toggle></theme-toggle>
|
|
</header>
|
|
<div id="landing" class="grid inner-page hidden">
|
|
<div class="left">
|
|
<h1 class="title-font">
|
|
Secure, Private and Reliable.
|
|
</h1>
|
|
<p class="margin-block-1">A messenger made with Blockchain and Open Source technologies. Your data
|
|
remains with you, as it should.</p>
|
|
<div class="flex">
|
|
<a href="#/sign_up" class="button">Get started</a>
|
|
<a href="#/sign_in" class="button button--primary">Sign In</a>
|
|
</div>
|
|
</div>
|
|
<div id="landing_illustration" class="right">
|
|
<img src="assets/message-background.svg" alt="">
|
|
</div>
|
|
</div>
|
|
<article id="sign_in" class="inner-page hidden">
|
|
<section>
|
|
<h1 style="font-size: 2rem;">Sign in</h1>
|
|
<p>Welcome back, glad to see you again</p>
|
|
<sm-form id="sign_in_form">
|
|
<sm-input id="private_key_field" class="password-field" type="password"
|
|
placeholder="FLO/BTC private key" error-text="Private key is invalid" data-private-key required>
|
|
<label slot="right" class="interact">
|
|
<input type="checkbox" class="hidden" autocomplete="off" readonly
|
|
onchange="togglePrivateKeyVisibility(this)">
|
|
<svg class="icon invisible" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<title>Hide password</title>
|
|
<path d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" fill="none" />
|
|
<path
|
|
d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
|
|
</svg>
|
|
<svg class="icon visible" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<title>Show password</title>
|
|
<path d="M0 0h24v24H0z" fill="none" />
|
|
<path
|
|
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
|
|
</svg>
|
|
</label>
|
|
</sm-input>
|
|
<button id="sign_in_button" class="button button--primary" type="submit" disabled>Sign in</button>
|
|
</sm-form>
|
|
<p>
|
|
New here? <a href="#/sign_up">get your FLO login credentials</a>
|
|
</p>
|
|
</section>
|
|
</article>
|
|
<article id="sign_up" class="inner-page hidden">
|
|
<keys-generator id="keys_generator"></keys-generator>
|
|
</article>
|
|
<div id="loading" class="inner-page hidden">
|
|
<svg class="page__loader" viewBox="0 0 512 512">
|
|
<defs>
|
|
<style>
|
|
.a {
|
|
fill: #fff;
|
|
}
|
|
|
|
.b {
|
|
fill: #ccc;
|
|
}
|
|
|
|
.c {
|
|
fill: none;
|
|
}
|
|
|
|
.c,
|
|
.d,
|
|
.e {
|
|
stroke: #000;
|
|
stroke-miterlimit: 10;
|
|
stroke-width: 20;
|
|
}
|
|
|
|
.d {
|
|
fill: #ed1c24;
|
|
}
|
|
|
|
.e {
|
|
fill: #d30d41;
|
|
}
|
|
</style>
|
|
</defs>
|
|
<title>mascot</title>
|
|
<path class="a"
|
|
d="M506,367.94v51.19a52,52,0,0,1-52,52H130.4a52,52,0,0,1-52-52V183.69L9.58,40.88,454,43.24a52,52,0,0,1,52,52.05V225.38" />
|
|
<path class="b"
|
|
d="M506,98.69V431.58c.57,56.06-149.95,44-149.95,44a52,52,0,0,0,52-52V44.86L454,46.65A52,52,0,0,1,506,98.69Z" />
|
|
<line class="c" x1="506" y1="273.75" x2="506" y2="327.21" />
|
|
<path class="c"
|
|
d="M506,367.94v51.19a52,52,0,0,1-52,52H130.4a52,52,0,0,1-52-52V183.69L9.58,40.88,454,43.24a52,52,0,0,1,52,52.05V225.38" />
|
|
<path class="d" d="M361.83,340a69.63,69.63,0,1,1-139.26,0C222.57,301.5,361.83,301.5,361.83,340Z" />
|
|
<path class="c" d="M152,242.43a34.32,34.32,0,0,1,64.68.2" />
|
|
<path class="c" d="M367.79,242.43a34.32,34.32,0,0,1,64.68.2" />
|
|
<path class="e" d="M325,314.13c21,4.22,36.85,12.82,36.85,25.8a69.7,69.7,0,0,1-41.09,63.58" />
|
|
</svg>
|
|
<div class="shadow"></div>
|
|
<h4 class="page__tag-line margin-block-1">Getting everything ready, hang on.</h4>
|
|
<button class="button" onclick="clearCredentials()">Reset</button>
|
|
</div>
|
|
</div>
|
|
<main id="main_page" class="page grid hidden">
|
|
<section id="chat_page" class="inner-page">
|
|
<div id="contacts" class="grid">
|
|
<div class="header flex flex-direction-column">
|
|
<div class="flex gap-0-5">
|
|
<button class="user-profile-button button--small" onclick="openPopup('profile_popup')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon margin-right-0-5" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none"></path>
|
|
<path
|
|
d="M12 5.9c1.16 0 2.1.94 2.1 2.1s-.94 2.1-2.1 2.1S9.9 9.16 9.9 8s.94-2.1 2.1-2.1m0 9c2.97 0 6.1 1.46 6.1 2.1v1.1H5.9V17c0-.64 3.13-2.1 6.1-2.1M12 4C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 9c-2.67 0-8 1.34-8 4v3h16v-3c0-2.66-5.33-4-8-4z">
|
|
</path>
|
|
</svg>
|
|
<div class="user-profile-id overflow-ellipsis"> </div>
|
|
</button>
|
|
<button class="icon-only button button--rounded button--danger" onclick="signOut()"
|
|
title="Sign out">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24"
|
|
height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<g>
|
|
<path d="M0,0h24v24H0V0z" fill="none" />
|
|
</g>
|
|
<g>
|
|
<path
|
|
d="M17,8l-1.41,1.41L17.17,11H9v2h8.17l-1.58,1.58L17,16l4-4L17,8z M5,5h7V3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h7v-2H5V5z" />
|
|
</g>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<sm-chips id="feature_mode">
|
|
<sm-chip value="dm" selected>Chat</sm-chip>
|
|
<sm-chip value="multisig">Multisig</sm-chip>
|
|
<sm-chip value="requests" id="notification_panel_button">Requests</sm-chip>
|
|
</sm-chips>
|
|
</div>
|
|
<div id="chat_sections">
|
|
<div class="flex flex-direction-column gap-0-5" style="overflow: hidden;">
|
|
<div class="flex align-center gap-0-5" style="padding: 0 1rem;">
|
|
<sm-input id="search_chats" type="search" placeholder="FLO/BTC/ETH address or name">
|
|
<svg slot="icon" class="icon" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
|
</svg>
|
|
</sm-input>
|
|
<sm-menu align-options="right">
|
|
<menu-option onclick="openCreationPopup('group')">
|
|
Create new group
|
|
</menu-option>
|
|
</sm-menu>
|
|
</div>
|
|
<div id="chats_list" class="flex observe-empty-state"></div>
|
|
<div class="empty-state flex flex-direction-column align-center text-center align-self-center">
|
|
<svg class="icon icon--big" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0z" fill="none" />
|
|
<path
|
|
d="M21 6h-2v9H6v2c0 .55.45 1 1 1h11l4 4V7c0-.55-.45-1-1-1zm-4 6V3c0-.55-.45-1-1-1H3c-.55 0-1 .45-1 1v14l4-4h10c.55 0 1-.45 1-1z" />
|
|
</svg>
|
|
<h4 class="margin-bottom-0-5">Start your first conversation</h4>
|
|
<p>Tap/click on 'New chat' to add or select a contact.</p>
|
|
</div>
|
|
<button id="new_message_button" onclick="openPopup('new_message_popup')"
|
|
class="button button--primary fab round">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM5.92 19H5v-.92l9.06-9.06.92.92L5.92 19zM20.71 5.63l-2.34-2.34c-.2-.2-.45-.29-.71-.29s-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83c.39-.39.39-1.02 0-1.41z" />
|
|
</svg>
|
|
New chat
|
|
</button>
|
|
</div>
|
|
<div class="hidden flex flex-direction-column gap-0-5" style="overflow-y: auto;">
|
|
<button class="button button--primary fab round" onclick="openCreationPopup('multisig')">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0z" fill="none" />
|
|
<path
|
|
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z" />
|
|
</svg>
|
|
Create multisig address
|
|
</button>
|
|
<div class="flex align-center space-between" style="margin: 0.5rem 0.5rem 0 1rem;">
|
|
Multisig mode
|
|
<sm-chips id="multisig_mode_selector" onchange="handleMultisigModeChange(event)">
|
|
<sm-chip value="btc" selected>BTC</sm-chip>
|
|
<sm-chip value="flo">FLO</sm-chip>
|
|
</sm-chips>
|
|
</div>
|
|
<ul id="select_multisig_list" class="grid gap-0-3 observe-empty-state"
|
|
style="padding-bottom: 6rem; overflow-y:auto;"></ul>
|
|
<div class="empty-state" style="padding: 1rem;">
|
|
<p>
|
|
There are no multisig addresses created, use the button below to create one.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div id="notifications_wrapper" class="hidden">
|
|
<ul id="notifications_list" class="observe-empty-state"></ul>
|
|
<div class="empty-state flex flex-direction-column align-center text-center align-self-center">
|
|
<svg class="icon icon--medium" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path
|
|
d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z" />
|
|
</svg>
|
|
<h4 class="margin-bottom-0-5">No requests</h4>
|
|
<p>When you receive a request, it will appear here.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="chat_view" class="grid hidden">
|
|
<header id="chat_header" class="grid align-center">
|
|
<a class="hide-on-desktop button icon-only back-button" href="#/chat_page">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none"></path>
|
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"></path>
|
|
</svg>
|
|
</a>
|
|
<button id="chat_details_button" class="flex align-center interactive">
|
|
<div id="receiver_initial" class="initial flex align-center margin-right-0-5"></div>
|
|
<h4 id="receiver_name"></h4>
|
|
<svg class="icon margin-left-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6-6-6z" />
|
|
</svg>
|
|
<div id="pseudo_background"></div>
|
|
</button>
|
|
</header>
|
|
<section id="messages_container" class="flex flex-direction-column"></section>
|
|
<div id="scroll_to_bottom">
|
|
<button onclick="scrollToBottom(true)">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<footer id="chat_footer" class="grid">
|
|
<emoji-picker id="emoji_picker" class="hidden"></emoji-picker>
|
|
<div class="flex">
|
|
<svg id="emoji_toggle" onclick="toggleEmoji('toggle')" xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 64 64">
|
|
<path
|
|
d="M32,0A32,32,0,1,0,64,32,32,32,0,0,0,32,0ZM43.84,17.51a4.92,4.92,0,1,1-4.92,4.92A4.92,4.92,0,0,1,43.84,17.51Zm-23.62-.06a5,5,0,1,1-5,5A5,5,0,0,1,20.22,17.45ZM32,54.42A19.68,19.68,0,0,1,12.31,34.73H51.69A19.68,19.68,0,0,1,32,54.42Z" />
|
|
</svg>
|
|
<sm-textarea id="type_message" placeholder="Type a message" class="w-100"></sm-textarea>
|
|
<button id="send_message_button" disabled> Send </button>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
<div class="flex flex-direction-column gap-2 align-center justify-center text-center hide-on-mobile">
|
|
<div class="messenger-illustration"></div>
|
|
<h4>Chat, Mail and use Bitcoin Multisig</h4>
|
|
</div>
|
|
</section>
|
|
<section class="inner-page hidden" id="mail_page">
|
|
<div id="mails" class="grid">
|
|
<header class="flex flex-direction-column gap-0-5 header">
|
|
<div class="flex gap-0-5">
|
|
<button class="user-profile-button button--small" onclick="openPopup('profile_popup')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon margin-right-0-5" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none"></path>
|
|
<path
|
|
d="M12 5.9c1.16 0 2.1.94 2.1 2.1s-.94 2.1-2.1 2.1S9.9 9.16 9.9 8s.94-2.1 2.1-2.1m0 9c2.97 0 6.1 1.46 6.1 2.1v1.1H5.9V17c0-.64 3.13-2.1 6.1-2.1M12 4C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 9c-2.67 0-8 1.34-8 4v3h16v-3c0-2.66-5.33-4-8-4z">
|
|
</path>
|
|
</svg>
|
|
<div class="user-profile-id overflow-ellipsis"> </div>
|
|
</button>
|
|
<button class="icon-only button button--rounded button--danger" onclick="signOut()"
|
|
title="Sign out">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24"
|
|
height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<g>
|
|
<path d="M0,0h24v24H0V0z" fill="none" />
|
|
</g>
|
|
<g>
|
|
<path
|
|
d="M17,8l-1.41,1.41L17.17,11H9v2h8.17l-1.58,1.58L17,16l4-4L17,8z M5,5h7V3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h7v-2H5V5z" />
|
|
</g>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="flex align-center space-between">
|
|
<h4>Mail</h4>
|
|
<sm-chips id="mail_type_selector">
|
|
<sm-chip value="inbox" selected>Inbox </sm-chip>
|
|
<sm-chip value="sent">Sent </sm-chip>
|
|
</sm-chips>
|
|
</div>
|
|
</header>
|
|
<button class="button button--primary fab" id="new_mail_button"
|
|
onclick="openPopup('compose_mail_popup')">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM5.92 19H5v-.92l9.06-9.06.92.92L5.92 19zM20.71 5.63l-2.34-2.34c-.2-.2-.45-.29-.71-.29s-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83c.39-.39.39-1.02 0-1.41z" />
|
|
</svg>
|
|
New Mail
|
|
</button>
|
|
<div id="mail_sections">
|
|
<div class="flex h-100">
|
|
<ul id="inbox_mail_container" class="mail-container flex observe-empty-state"></ul>
|
|
<div class="empty-state flex flex-direction-column align-center text-center align-self-center">
|
|
<svg class="icon icon--big" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5v-3h3.56c.69 1.19 1.97 2 3.45 2s2.75-.81 3.45-2H19v3zm0-5h-4.99c0 1.1-.9 2-2 2s-2-.9-2-2H5V5h14v9z" />
|
|
</svg>
|
|
<h4 class="margin-bottom-0-5">All your received mails will appear here.</h4>
|
|
<p>Tap/click on 'New Mail' button below to compose new mail.</p>
|
|
</div>
|
|
</div>
|
|
<div class="hidden flex h-100">
|
|
<ul id="sent_mail_container" class="mail-container flex observe-empty-state"></ul>
|
|
<div class="empty-state flex flex-direction-column align-center text-center align-self-center">
|
|
<svg class="icon icon--big" xmlns="http://www.w3.org/2000/svg"
|
|
enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<g>
|
|
<rect fill="none" height="24" width="24" />
|
|
</g>
|
|
<g>
|
|
<g>
|
|
<polygon
|
|
points="11,9.83 11,14 13,14 13,9.83 14.59,11.41 16,10 12,6 8,10 9.41,11.41" />
|
|
<path
|
|
d="M19,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V5C21,3.9,20.1,3,19,3z M19,19H5v-3h3.02 c0.91,1.21,2.35,2,3.98,2s3.06-0.79,3.98-2H19V19z M19,14h-4.18c-0.41,1.16-1.51,2-2.82,2s-2.4-0.84-2.82-2H5V5h14V14z" />
|
|
</g>
|
|
</g>
|
|
</svg>
|
|
<h4 class="margin-bottom-0-5">All your sent mails will appear here.</h4>
|
|
<p>Tap/click on 'New Mail' button below to compose new mail.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="mail" class="flex hide-on-mobile hidden">
|
|
<div id="mail_container"></div>
|
|
<div class="flex">
|
|
<button class="button" id="prev_mail">View Previous Mail</button>
|
|
<button class="button" id="show_reply_popup">reply</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section id="settings" class="inner-page hidden">
|
|
<aside id="settings_sidebar">
|
|
<header class="grid header">
|
|
<h4>Settings</h4>
|
|
</header>
|
|
<a class="sidebar-item interactive" href="#/settings/personalize">personalize</a>
|
|
<a class="sidebar-item interactive" href="#/settings/chat">chat</a>
|
|
<a class="sidebar-item interactive" href="#/settings/blocked">Blocked</a>
|
|
<a class="sidebar-item interactive" href="#/settings/backup">backup & restore</a>
|
|
<a class="sidebar-item interactive" href="#/settings/about">About</a>
|
|
</aside>
|
|
<div id="settings_panel" class=" hide-on-mobile">
|
|
<header id="settings_header" class="flex flex-direction-column gap-0-5 hide-on-desktop">
|
|
<a href="#/settings" class="button icon-only back-button">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
|
</svg>
|
|
</a>
|
|
<h4 id="settings_title"></h4>
|
|
</header>
|
|
<div id="chat" class="panel hidden grid gap-0-5">
|
|
<sm-switch id="is_enter_send_toggle" class="card w-100">
|
|
<div slot="left" class="grid gap-1">
|
|
<h4>Send by Enter</h4>
|
|
<p>If this toggle is ON then pressing 'Enter' key will send messages</p>
|
|
</div>
|
|
</sm-switch>
|
|
</div>
|
|
<div id="blocked" class="panel grid gap-0-5 hidden">
|
|
<section class="card">
|
|
<h4>Blocked</h4>
|
|
<ul id="blocked_list" class="observe-empty-state"></ul>
|
|
<div class="empty-state">
|
|
<p>No blocked FLO addresss</p>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<div id="personalize" class="panel grid gap-0-5 hidden">
|
|
<section class="card">
|
|
<h4>Chat preview</h4>
|
|
<div id="chat_preview">
|
|
<div class="message sent">
|
|
<p class="message-body">Hey there!</p>
|
|
<time class="time">12:00PM</time>
|
|
</div>
|
|
<div class="message received">
|
|
<p class="message-body">How are you?</p>
|
|
<time class="time">12:00PM</time>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section class="grid gap-0-5 card">
|
|
<h4>Toggle dark theme</h4>
|
|
<div class="flex align-center space-between">
|
|
<p>Only applied to this browser</p>
|
|
<theme-toggle></theme-toggle>
|
|
</div>
|
|
</section>
|
|
<section class="grid gap-1 card">
|
|
<h4>Set chat and mail background image</h4>
|
|
<fieldset id="bg_preview_container" class="flex">
|
|
<label id="selected_bg_preview" class="bg-preview hidden">
|
|
<input type="radio" name="bg" value="img">
|
|
<img src="" alt="background preview" class="bg-preview__image">
|
|
</label>
|
|
<label id="default_bg_preview" class="bg-preview bg-preview--selected">
|
|
<input type="radio" name="bg" value="default">
|
|
Default
|
|
</label>
|
|
</fieldset>
|
|
<label class="button select-file">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M4 4h7V2H4c-1.1 0-2 .9-2 2v7h2V4zm6 9l-4 5h12l-3-4-2.03 2.71L10 13zm7-4.5c0-.83-.67-1.5-1.5-1.5S14 7.67 14 8.5s.67 1.5 1.5 1.5S17 9.33 17 8.5zM20 2h-7v2h7v7h2V4c0-1.1-.9-2-2-2zm0 18h-7v2h7c1.1 0 2-.9 2-2v-7h-2v7zM4 13H2v7c0 1.1.9 2 2 2h7v-2H4v-7z" />
|
|
</svg>
|
|
<span id="select_bg_button">Select background</span>
|
|
<input type="file" id="select_bg_image" accept="image/*" />
|
|
</label>
|
|
</section>
|
|
<section id="backdrop_options" class="hidden grid gap-1 card">
|
|
<h4>App backdrop</h4>
|
|
<label class="grid gap-0-3">
|
|
<span>Wallpaper dimming</span>
|
|
<div class="flex gap-0-5">
|
|
<input type="range" min="0" max="100" id="backdrop_opacity" class="w-100">
|
|
<output id="backdrop_opacity_value"></output>
|
|
</div>
|
|
</label>
|
|
<label class="grid gap-0-3">
|
|
<span>Blur</span>
|
|
<div class="flex gap-0-3">
|
|
<input type="range" min="0" max="100" id="backdrop_blur" class="w-100">
|
|
<output id="backdrop_blur_value"></output>
|
|
</div>
|
|
</label>
|
|
</section>
|
|
<section class="grid gap-1 card">
|
|
<h4>Accent color</h4>
|
|
<color-grid id="accent_color_selector"></color-grid>
|
|
</section>
|
|
</div>
|
|
<div id="backup" class="panel grid gap-0-5 hidden">
|
|
<section class="grid gap-1 card">
|
|
<h4>Backup data</h4>
|
|
<p>Create a backup of contacts, conversations and mails. Which can later be used to restore
|
|
these in case of data is cleared. </p>
|
|
<button class="button" id="backup_data">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg"
|
|
enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<g>
|
|
<rect fill="none" height="24" width="24" />
|
|
</g>
|
|
<g>
|
|
<path
|
|
d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z" />
|
|
</g>
|
|
</svg>
|
|
Backup Data
|
|
</button>
|
|
<span id="backup_info"></span>
|
|
</section>
|
|
<section class="grid gap-1 card">
|
|
<h4>Restore backup</h4>
|
|
<p>Select backup file with extension '.json'. Which was downloaded when backup was performed.
|
|
</p>
|
|
<label class="button select-file">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M13 3c-4.97 0-9 4.03-9 9H1l4 3.99L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.25 2.52.77-1.28-3.52-2.09V8z" />
|
|
</svg>
|
|
<span>Select File</span>
|
|
<input type="file" id="restore_data" accept=".json" />
|
|
</label>
|
|
</section>
|
|
</div>
|
|
<div id="about" class="panel grid gap-0-5 hidden">
|
|
<section class="card">
|
|
<p>Created by RanchiMall, a Blockchain incorporated entity</p>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<nav id="main_navbar" class="flex hidden">
|
|
<ul>
|
|
<li>
|
|
<a id="chat_page_button" class="nav-item flex align-center active" href="#/chat_page" title="Chat">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M15 4v7H5.17l-.59.59-.58.58V4h11m1-2H3c-.55 0-1 .45-1 1v14l4-4h10c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm5 4h-2v9H6v2c0 .55.45 1 1 1h11l4 4V7c0-.55-.45-1-1-1z" />
|
|
</svg>
|
|
<svg class="icon filled" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0z" fill="none" />
|
|
<path
|
|
d="M21 6h-2v9H6v2c0 .55.45 1 1 1h11l4 4V7c0-.55-.45-1-1-1zm-4 6V3c0-.55-.45-1-1-1H3c-.55 0-1 .45-1 1v14l4-4h10c.55 0 1-.45 1-1z" />
|
|
</svg>
|
|
<span class="nav-item__title">Chat</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="nav-item flex align-center" id="mail_page_button" href="#/mail_page" title="Mail">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6zm-2 0l-8 5-8-5h16zm0 12H4V8l8 5 8-5v10z" />
|
|
</svg>
|
|
<svg class="icon filled" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0z" fill="none" />
|
|
<path
|
|
d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z" />
|
|
</svg>
|
|
<span class="nav-item__title">Mail</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="nav-item flex align-center" href="#/settings" title="Settings">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.09-.16-.26-.25-.44-.25-.06 0-.12.01-.17.03l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.06-.02-.12-.03-.18-.03-.17 0-.34.09-.43.25l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.09.16.26.25.44.25.06 0 .12-.01.17-.03l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.06.02.12.03.18.03.17 0 .34-.09.43-.25l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zm-1.98-1.71c.04.31.05.52.05.73 0 .21-.02.43-.05.73l-.14 1.13.89.7 1.08.84-.7 1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2 1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43c-.43-.18-.83-.41-1.23-.71l-.91-.7-1.06.43-1.27.51-.7-1.21 1.08-.84.89-.7-.14-1.13c-.03-.31-.05-.54-.05-.74s.02-.43.05-.73l.14-1.13-.89-.7-1.08-.84.7-1.21 1.27.51 1.04.42.9-.68c.43-.32.84-.56 1.25-.73l1.06-.43.16-1.13.2-1.35h1.39l.19 1.35.16 1.13 1.06.43c.43.18.83.41 1.23.71l.91.7 1.06-.43 1.27-.51.7 1.21-1.07.85-.89.7.14 1.13zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" />
|
|
</svg>
|
|
<svg class="icon filled" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24"
|
|
height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<g>
|
|
<path d="M0,0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z" />
|
|
</g>
|
|
</svg>
|
|
<span class="nav-item__title">Settings</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
<div id="background_overlay"></div>
|
|
<img id="background_image"></img>
|
|
</main>
|
|
<sm-popup id="add_contact_popup">
|
|
<header class="popup__header" slot="header">
|
|
<button class="popup__header__close justify-self-start">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h4>Add contact</h4>
|
|
</header>
|
|
<sm-form>
|
|
<sm-input id="add_contact_floID" data-flo-address placeholder="FLO/BTC/ETH address"
|
|
error-text="Invalid address" animate autofocus required></sm-input>
|
|
<sm-input id="add_contact_name" placeholder="Name" animate required></sm-input>
|
|
<button class="button button--primary" id="add_contact_button" type="submit" disabled>Add</button>
|
|
</sm-form>
|
|
</sm-popup>
|
|
<sm-popup id="compose_mail_popup">
|
|
<header class="popup__header" slot="header">
|
|
<button class="popup__header__close justify-self-start">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h4>Compose Mail</h4>
|
|
</header>
|
|
<sm-form>
|
|
<p>You can send mail to multiple FLO addresses by entering commas </p>
|
|
<div id="auto_complete_contact" class="flex flex-direction-column">
|
|
<sm-input id="send_mail_to" placeholder="To" animate required></sm-input>
|
|
<div id="mail_contact_list" class="hidden contact-list"></div>
|
|
</div>
|
|
<sm-input id="subject_of_mail" placeholder="Subject" animate></sm-input>
|
|
<sm-textarea id="mail_content" placeholder="Type a mail" name="" id="" rows="10" required></sm-textarea>
|
|
<div class="multi-state-button">
|
|
<button id="send_mail_button" class="button button--primary" type="submit" disabled>Send</button>
|
|
</div>
|
|
</sm-form>
|
|
</sm-popup>
|
|
<sm-popup id="reply_mail_popup">
|
|
<header class="popup__header" slot="header">
|
|
<button class="popup__header__close justify-self-start">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h4>Reply</h4>
|
|
</header>
|
|
<sm-form>
|
|
<sm-input id="subject_of_reply_mail" placeholder="Subject" animate></sm-input>
|
|
<sm-textarea id="reply_mail_content" placeholder="Type a mail" id="" rows="10" required></sm-textarea>
|
|
<button id="reply_mail_button" class="button button--primary" disabled>Send</button>
|
|
</sm-form>
|
|
</sm-popup>
|
|
<!-- Contact popup -->
|
|
<sm-popup id="contact_details_popup">
|
|
<header class="popup__header" slot="header">
|
|
<button class="popup__header__close justify-self-start">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
</header>
|
|
<section id="contact_details_section"></section>
|
|
</sm-popup>
|
|
|
|
<sm-popup id="new_message_popup">
|
|
<header class="grid popup__header" slot="header">
|
|
<button class="popup__header__close justify-self-start">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h4>Contacts</h4>
|
|
</header>
|
|
<div class="scrolling-wrapper grid gap-1-5">
|
|
<div class="grid gap-1">
|
|
<sm-input id="search_contacts" type="search" placeholder="Search"> </sm-input>
|
|
<fieldset id="all_contacts_options" class="flex align-center flex-wrap gap-0-5">
|
|
<button id="create_group_option" class="interactive button button--colored button--small gap-0-5"
|
|
onclick="openCreationPopup('group')">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M9 13.75c-2.34 0-7 1.17-7 3.5V19h14v-1.75c0-2.33-4.66-3.5-7-3.5zM4.34 17c.84-.58 2.87-1.25 4.66-1.25s3.82.67 4.66 1.25H4.34zM9 12c1.93 0 3.5-1.57 3.5-3.5S10.93 5 9 5 5.5 6.57 5.5 8.5 7.07 12 9 12zm0-5c.83 0 1.5.67 1.5 1.5S9.83 10 9 10s-1.5-.67-1.5-1.5S8.17 7 9 7zm7.04 6.81c1.16.84 1.96 1.96 1.96 3.44V19h4v-1.75c0-2.02-3.5-3.17-5.96-3.44zM15 12c1.93 0 3.5-1.57 3.5-3.5S16.93 5 15 5c-.54 0-1.04.13-1.5.35.63.89 1 1.98 1 3.15s-.37 2.26-1 3.15c.46.22.96.35 1.5.35z" />
|
|
</svg>
|
|
Create new group
|
|
</button>
|
|
<button id="add_contact_option" class="interactive button button--colored button--small gap-0-5"
|
|
onclick="openPopup('add_contact_popup')">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24"
|
|
height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<g>
|
|
<rect fill="none" height="24" width="24" />
|
|
</g>
|
|
<g>
|
|
<path
|
|
d="M20,9V6h-2v3h-3v2h3v3h2v-3h3V9H20z M9,12c2.21,0,4-1.79,4-4c0-2.21-1.79-4-4-4S5,5.79,5,8C5,10.21,6.79,12,9,12z M9,6 c1.1,0,2,0.9,2,2c0,1.1-0.9,2-2,2S7,9.1,7,8C7,6.9,7.9,6,9,6z M15.39,14.56C13.71,13.7,11.53,13,9,13c-2.53,0-4.71,0.7-6.39,1.56 C1.61,15.07,1,16.1,1,17.22V20h16v-2.78C17,16.1,16.39,15.07,15.39,14.56z M15,18H3v-0.78c0-0.38,0.2-0.72,0.52-0.88 C4.71,15.73,6.63,15,9,15c2.37,0,4.29,0.73,5.48,1.34C14.8,16.5,15,16.84,15,17.22V18z" />
|
|
</g>
|
|
</svg>
|
|
Add contact
|
|
</button>
|
|
</fieldset>
|
|
<div id="contacts_container" class="observe-empty-state"></div>
|
|
<div class="empty-state">
|
|
<h4 class="margin-bottom-0-5">No saved contacts</h4>
|
|
<p>Use 'Add contact' to add new FLO/BTC/ETH address as a contact.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</sm-popup>
|
|
<sm-popup id="creation_popup">
|
|
<header class="grid popup__header" slot="header">
|
|
<button class="popup__header__close justify-self-start">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3 id="creation_popup__title">Create group</h3>
|
|
</header>
|
|
<div id="creation_process">
|
|
<div>
|
|
<div id="selected_contacts">
|
|
<div class="empty-state" style="padding-top: 0">
|
|
<p class="info info--warning">*Contacts that haven't yet replied to you, can't be selected. you
|
|
can send a request or message them.</p>
|
|
</div>
|
|
<div class="flex align-center space-between">
|
|
<h4>Select members</h4>
|
|
<button id="skip_members_button" class="button button--primary hidden">
|
|
Skip </button>
|
|
</div>
|
|
<label class="flex align-center hidden" style="padding: 1rem 0;">
|
|
<input id="add_self_check" class="margin-right-0-5" style="width: 1.3em; height: 1.3em"
|
|
type="checkbox" checked />
|
|
<span>Include self as a member</span>
|
|
</label>
|
|
<div id="selected_contacts_container" class="observe-empty-state"></div>
|
|
</div>
|
|
<div class="scrolling-wrapper">
|
|
<div id="select_contacts_container" class="observe-empty-state"></div>
|
|
<div class="empty-state">
|
|
<h4 class="margin-bottom-0-5">No saved contacts.</h4>
|
|
<p class="margin-bottom-1">Use 'Add contact' to add new FLO/BTC/ETH address as a contact.</p>
|
|
<button class="button interactive" onclick="openPopup('add_contact_popup')">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg"
|
|
enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<g>
|
|
<rect fill="none" height="24" width="24"></rect>
|
|
</g>
|
|
<g>
|
|
<path
|
|
d="M20,9V6h-2v3h-3v2h3v3h2v-3h3V9H20z M9,12c2.21,0,4-1.79,4-4c0-2.21-1.79-4-4-4S5,5.79,5,8C5,10.21,6.79,12,9,12z M9,6 c1.1,0,2,0.9,2,2c0,1.1-0.9,2-2,2S7,9.1,7,8C7,6.9,7.9,6,9,6z M15.39,14.56C13.71,13.7,11.53,13,9,13c-2.53,0-4.71,0.7-6.39,1.56 C1.61,15.07,1,16.1,1,17.22V20h16v-2.78C17,16.1,16.39,15.07,15.39,14.56z M15,18H3v-0.78c0-0.38,0.2-0.72,0.52-0.88 C4.71,15.73,6.63,15,9,15c2.37,0,4.29,0.73,5.48,1.34C14.8,16.5,15,16.84,15,17.22V18z">
|
|
</path>
|
|
</g>
|
|
</svg>
|
|
Add contact
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<sm-form class="hidden">
|
|
<button class="button icon-only back-button justify-self-start"
|
|
onclick="showChildElement('creation_process',0,{entry: slideInRight, exit: slideOutRight})">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
|
</svg>
|
|
</button>
|
|
<svg class="icon group-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
<path
|
|
d="M13.61,28.09c-1.63,0-4.72-2.35-5.33-3.58a21.65,21.65,0,0,1-1.35-7.32s-.26-6.07,6.68-6.07a6.38,6.38,0,0,1,6.69,6.07A21.65,21.65,0,0,1,19,24.51c-.62,1.23-3.7,3.58-5.34,3.58" />
|
|
<path
|
|
d="M50.39,28.09c-1.64,0-4.72-2.35-5.34-3.58a21.9,21.9,0,0,1-1.35-7.32s-.26-6.07,6.69-6.07a6.37,6.37,0,0,1,6.68,6.07,21.65,21.65,0,0,1-1.35,7.32c-.61,1.23-3.7,3.58-5.33,3.58" />
|
|
<path
|
|
d="M32,31.74c-2.21,0-6.37-3.17-7.2-4.83A29.3,29.3,0,0,1,23,17s-.35-8.21,9-8.21c8.68,0,9,8.21,9,8.21a29.3,29.3,0,0,1-1.83,9.88c-.82,1.66-5,4.83-7.2,4.83" />
|
|
<path
|
|
d="M48.29,38.58c-4.16-1.83-8.57-3.08-10.34-6.4a12,12,0,0,1-6,3.73,12,12,0,0,1-5.95-3.73c-1.77,3.32-6.18,4.57-10.34,6.4-1.7.71-3.11,9.88-1.13,9.88A33.06,33.06,0,0,0,31.23,53h1.54a33.06,33.06,0,0,0,16.65-4.53C51.4,48.46,50,39.29,48.29,38.58Z" />
|
|
<path
|
|
d="M14.82,36.57c.76-.33,1.54-.65,2.3-1,2.49-1,4.85-2,6.22-3.44C21.07,31.23,19,30.25,18,28.41a8.83,8.83,0,0,1-4.41,2.76,8.83,8.83,0,0,1-4.4-2.76c-1.31,2.46-4.58,3.38-7.66,4.74-1.26.52-2.3,7.31-.84,7.31a24.55,24.55,0,0,0,10.86,3.31C11.89,40.81,12.86,37.39,14.82,36.57Z" />
|
|
<path
|
|
d="M62.45,33.15c-3.08-1.36-6.35-2.28-7.66-4.74a8.83,8.83,0,0,1-4.4,2.76A8.83,8.83,0,0,1,46,28.41c-1,1.84-3,2.82-5.32,3.76,1.37,1.43,3.73,2.41,6.22,3.44.76.31,1.54.63,2.26,1,2,.83,3,4.25,3.29,7.21a24.55,24.55,0,0,0,10.86-3.31C64.75,40.46,63.71,33.67,62.45,33.15Z" />
|
|
</svg>
|
|
<sm-input id="group_name_field" placeholder="Group name" animate required></sm-input>
|
|
<sm-textarea id="group_description_field" rows="3" placeholder="Group description"></sm-textarea>
|
|
<div class="multi-state-button">
|
|
<button id="create_group_button" class="button button--primary" type="submit" disabled>
|
|
Create
|
|
</button>
|
|
</div>
|
|
</sm-form>
|
|
<div class="grid gap-1-5 hidden">
|
|
<button class="button icon-only back-button justify-self-start"
|
|
onclick="showChildElement('creation_process',0,{entry: slideInRight, exit: slideOutRight})">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
|
</svg>
|
|
</button>
|
|
<p id="multisig_creation__warning" class="info info--warning"></p>
|
|
<sm-form>
|
|
<sm-input id="multisig_label" placeholder="Label" error-text="Please add a label" animated
|
|
required></sm-input>
|
|
<div class="grid gap-0-5">
|
|
<p>Minimum signatures required for transaction approval</p>
|
|
<sm-input id="min_sign_required" placeholder="Min required" min="2" type="number"
|
|
error-text="At least 2 members are required" animate required>
|
|
</sm-input>
|
|
</div>
|
|
<div class="multi-state-button">
|
|
<button id="create_multisig_button" class="button button--primary" type="submit" disabled>
|
|
Create
|
|
</button>
|
|
</div>
|
|
</sm-form>
|
|
</div>
|
|
</div>
|
|
</sm-popup>
|
|
|
|
<!-- all contacts popup -->
|
|
|
|
<sm-popup id="contacts_popup">
|
|
<header class="popup__header" slot="header">
|
|
<button class="popup__header__close justify-self-start">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<div class="flex align-center space-between">
|
|
<h4>Select contacts to add</h4>
|
|
<button id="add_members_button" class="button button--primary" disabled>Add</button>
|
|
</div>
|
|
</header>
|
|
<p class="info info--warning">*Contacts that haven't yet replied to you, can't be added to a group. So they
|
|
won't be
|
|
visible here.</p>
|
|
<div id="popup_contacts_container" class="observe-empty-state"></div>
|
|
<div class="empty-state">
|
|
<p>You don't have any other contacts to show.</p>
|
|
</div>
|
|
</sm-popup>
|
|
<sm-popup id="secure_pwd_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close justify-self-start">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3 id="secure_pwd_title">Set password</h3>
|
|
</header>
|
|
<sm-form>
|
|
<sm-input id="secure_pwd_input" type="password" placeholder="Password" animate required autofocus>
|
|
</sm-input>
|
|
<button class="button button--primary cta secure-priv-key" type="submit" onclick="setSecurePassword()">
|
|
Set
|
|
</button>
|
|
</sm-form>
|
|
</sm-popup>
|
|
<sm-popup id="multisig_detail_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close justify-self-start">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Multisig address</h3>
|
|
</header>
|
|
<div id="multisig_details" class="grid gap-1-5"></div>
|
|
</sm-popup>
|
|
<sm-popup id="multisig_tx_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close justify-self-start">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Initiate multisig transaction </h3>
|
|
</header>
|
|
|
|
<sm-form id="send_tx">
|
|
<div class="grid gap-0-5 card" style="padding: 1rem;">
|
|
<span style="font-size: 0.9rem;">Selected multisig address</span>
|
|
<h4 id="selected_multisig" class="wrap-around" style="font-size: 0.9rem;"></h4>
|
|
<div id="selected_multisig__balance" class="flex align-center"></div>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<b style="font-size: 0.9rem;">Enter receiver(s)</b>
|
|
<div id="receiver_container"> </div>
|
|
<button id="add_receiver" class="button button--small margin-right-auto" onclick="addReceiver()">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
|
</svg>
|
|
Add a receiver</button>
|
|
</div>
|
|
<div id="send_tx__dynamic_content"></div>
|
|
<div class="multi-state-button margin-bottom-1-5">
|
|
<button id="initiate_transaction" type="submit" class="button button--primary cta w-100"
|
|
disabled>Initiate</button>
|
|
</div>
|
|
</sm-form>
|
|
</sm-popup>
|
|
<sm-popup id="profile_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close justify-self-start" onclick="closePopup()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Profile</h3>
|
|
</header>
|
|
<div id="profile_popup__content" class="grid gap-3"></div>
|
|
</sm-popup>
|
|
|
|
<!-- Templates -->
|
|
<template id="receiver_template">
|
|
<fieldset class="receiver-card">
|
|
<sm-input class="receiver-input" placeholder="Receiver address" animate required>
|
|
<div class="currency-symbol flex" slot="icon">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<g>
|
|
<rect fill="none" height="24" width="24"></rect>
|
|
</g>
|
|
<g>
|
|
<path
|
|
d="M17.06,11.57C17.65,10.88,18,9.98,18,9c0-1.86-1.27-3.43-3-3.87L15,3h-2v2h-2V3H9v2H6v2h2v10H6v2h3v2h2v-2h2v2h2v-2 c2.21,0,4-1.79,4-4C19,13.55,18.22,12.27,17.06,11.57z M10,7h4c1.1,0,2,0.9,2,2s-0.9,2-2,2h-4V7z M15,17h-5v-4h5c1.1,0,2,0.9,2,2 S16.1,17,15,17z">
|
|
</path>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
</sm-input>
|
|
<div class="flex align-start gap-0-5 remove-card-wrapper">
|
|
<sm-input type="number" class="amount-input w-100" placeholder="Amount" min="0.0000001"
|
|
step="0.00000001" error-text="Invalid amount" animate required>
|
|
<div class="currency-symbol flex" slot="icon">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24"
|
|
height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<g>
|
|
<rect fill="none" height="24" width="24"></rect>
|
|
</g>
|
|
<g>
|
|
<path
|
|
d="M17.06,11.57C17.65,10.88,18,9.98,18,9c0-1.86-1.27-3.43-3-3.87L15,3h-2v2h-2V3H9v2H6v2h2v10H6v2h3v2h2v-2h2v2h2v-2 c2.21,0,4-1.79,4-4C19,13.55,18.22,12.27,17.06,11.57z M10,7h4c1.1,0,2,0.9,2,2s-0.9,2-2,2h-4V7z M15,17h-5v-4h5c1.1,0,2,0.9,2,2 S16.1,17,15,17z">
|
|
</path>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
</sm-input>
|
|
<button class="remove-card button--small" onclick="this.closest('.receiver-card').remove()">
|
|
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
|
|
</svg>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</fieldset>
|
|
</template>
|
|
|
|
|
|
<script src="scripts/components.min.js"></script>
|
|
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
|
|
<script>
|
|
/*jshint esversion: 8 */
|
|
/**
|
|
* @yaireo/relative-time - javascript function to transform timestamp or date to local relative-time
|
|
*
|
|
* @version v1.0.0
|
|
* @homepage https://github.com/yairEO/relative-time
|
|
*/
|
|
|
|
!function (e, t) { var o = o || {}; "function" == typeof o && o.amd ? o([], t) : "object" == typeof exports && "object" == typeof module ? module.exports = t() : "object" == typeof exports ? exports.RelativeTime = t() : e.RelativeTime = t() }(this, (function () { const e = { year: 31536e6, month: 2628e6, day: 864e5, hour: 36e5, minute: 6e4, second: 1e3 }, t = "en", o = { numeric: "auto" }; function n(e) { e = { locale: (e = e || {}).locale || t, options: { ...o, ...e.options } }, this.rtf = new Intl.RelativeTimeFormat(e.locale, e.options) } return n.prototype = { from(t, o) { const n = t - (o || new Date); for (let t in e) if (Math.abs(n) > e[t] || "second" == t) return this.rtf.format(Math.round(n / e[t]), t) } }, n }));
|
|
|
|
const relativeTime = new RelativeTime({ style: 'narrow' });
|
|
</script>
|
|
<script id="standard_UI_functions">
|
|
const { html, render: renderElem } = uhtml;
|
|
const uiGlobals = {
|
|
// Use to store global variables
|
|
}
|
|
//Checks for internet connection status
|
|
uiGlobals.connectionErrorNotification = []
|
|
if (!navigator.onLine)
|
|
uiGlobals.connectionErrorNotification.push(notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error'))
|
|
window.addEventListener('offline', () => {
|
|
uiGlobals.connectionErrorNotification.push(notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error'))
|
|
})
|
|
window.addEventListener('online', () => {
|
|
uiGlobals.connectionErrorNotification.forEach(notification => getRef("notification_drawer").remove(notification))
|
|
notify('We are back online.', 'success')
|
|
location.reload()
|
|
uiGlobals.connectionErrorNotification = []
|
|
})
|
|
// Use instead of document.getElementById
|
|
function getRef(elementId) {
|
|
return document.getElementById(elementId)
|
|
}
|
|
// implement querySelector as $ function
|
|
function $(selector) {
|
|
return document.querySelector(selector)
|
|
}
|
|
|
|
// returns dom with specified element
|
|
function createElement(tagName, options = {}) {
|
|
const { className, textContent, innerHTML, attributes = {} } = options
|
|
const elem = document.createElement(tagName)
|
|
for (let attribute in attributes) {
|
|
elem.setAttribute(attribute, attributes[attribute])
|
|
}
|
|
if (className)
|
|
elem.className = className
|
|
if (textContent)
|
|
elem.textContent = textContent
|
|
if (innerHTML)
|
|
elem.innerHTML = innerHTML
|
|
return elem
|
|
}
|
|
|
|
// Use when a function needs to be executed after user finishes changes
|
|
const debounce = (callback, wait) => {
|
|
let timeoutId = null;
|
|
return (...args) => {
|
|
window.clearTimeout(timeoutId);
|
|
timeoutId = window.setTimeout(() => {
|
|
callback.apply(null, args);
|
|
}, wait);
|
|
};
|
|
}
|
|
// adds a class to all elements in an array
|
|
function addClass(elements, className) {
|
|
elements.forEach((element) => {
|
|
document.querySelector(element).classList.add(className);
|
|
});
|
|
}
|
|
// removes a class from all elements in an array
|
|
function removeClass(elements, className) {
|
|
elements.forEach((element) => {
|
|
document.querySelector(element).classList.remove(className);
|
|
});
|
|
}
|
|
// toggle class on condition
|
|
function conditionalClass(elem, condition, className) {
|
|
if (condition) {
|
|
elem.classList.add(className)
|
|
} else {
|
|
elem.classList.remove(className)
|
|
}
|
|
}
|
|
// return querySelectorAll elements as an array
|
|
function getAllElements(selector) {
|
|
return Array.from(document.querySelectorAll(selector));
|
|
}
|
|
// toggle attribute based on condition
|
|
function toggleAttr(elem, condition, attr) {
|
|
if (condition) {
|
|
elem.setAttribute(attr, '')
|
|
} else {
|
|
elem.removeAttribute(attr)
|
|
}
|
|
}
|
|
|
|
let zIndex = 50
|
|
// function required for popups or modals to appear
|
|
function openPopup(popupId, pinned) {
|
|
zIndex++
|
|
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
|
|
return getRef(popupId).show({ pinned })
|
|
}
|
|
|
|
// hides the popup or modal
|
|
function closePopup(options = {}) {
|
|
if (popupStack.peek() === undefined)
|
|
return;
|
|
popupStack.peek().popup.hide(options)
|
|
}
|
|
|
|
function getAddressType(floID) {
|
|
if (messenger.groups.hasOwnProperty(floID))
|
|
return 'group';
|
|
else if (messenger.pipeline.hasOwnProperty(floID))
|
|
return 'pipeline';
|
|
else return 'plain';
|
|
}
|
|
|
|
document.addEventListener('popupopened', async (e) => {
|
|
getRef('main_page').setAttribute('inert', '')
|
|
//pushes popup as septate entry in history
|
|
history.pushState({ type: 'popup' }, null, null)
|
|
switch (e.target.id) {
|
|
case 'contact_details_popup':
|
|
const chatAddress = floGlobals.viewingDetailsOfAddress;
|
|
const addressType = getAddressType(chatAddress);
|
|
let isAdmin = false;
|
|
let contactInitial = '';
|
|
let disableContactName = false;
|
|
let groupDescription = ''
|
|
let lastInteraction = '';
|
|
let floAddressType = '';
|
|
let contactFloAddress;
|
|
let contactBtcAddress;
|
|
let contactEthAddress;
|
|
if (addressType === 'group')
|
|
isAdmin = messenger.groups[chatAddress].admin === floDapps.user.id;
|
|
let addAsContact
|
|
if (!floGlobals.contacts.hasOwnProperty(chatAddress) && addressType === 'plain')
|
|
addAsContact = html`
|
|
<button id="add_as_contact_option" class="option" onclick="addAsContact()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24" /> </g> <g> <path d="M20,9V6h-2v3h-3v2h3v3h2v-3h3V9H20z M9,12c2.21,0,4-1.79,4-4c0-2.21-1.79-4-4-4S5,5.79,5,8C5,10.21,6.79,12,9,12z M9,6 c1.1,0,2,0.9,2,2c0,1.1-0.9,2-2,2S7,9.1,7,8C7,6.9,7.9,6,9,6z M15.39,14.56C13.71,13.7,11.53,13,9,13c-2.53,0-4.71,0.7-6.39,1.56 C1.61,15.07,1,16.1,1,17.22V20h16v-2.78C17,16.1,16.39,15.07,15.39,14.56z M15,18H3v-0.78c0-0.38,0.2-0.72,0.52-0.88 C4.71,15.73,6.63,15,9,15c2.37,0,4.29,0.73,5.48,1.34C14.8,16.5,15,16.84,15,17.22V18z" /> </g> </svg>
|
|
Add as contact
|
|
</button>`;
|
|
let markReadUnread
|
|
if (messenger.marked[chatAddress] && messenger.marked[chatAddress].includes('unread')) {
|
|
markReadUnread = html`
|
|
<button id="mark_read_option" class="option" onclick="markAsRead()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24" /> <path d="M12,18l-6,0l-4,4V4c0-1.1,0.9-2,2-2h16c1.1,0,2,0.9,2,2v7l-2,0V4H4v12l8,0V18z M23,14.34l-1.41-1.41l-4.24,4.24l-2.12-2.12 l-1.41,1.41L17.34,20L23,14.34z" /> </g> </svg>
|
|
Mark as read
|
|
</button>
|
|
`;
|
|
} else {
|
|
markReadUnread = html`
|
|
<button id="mark_unread_option" class="option" onclick="markAsUnread()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24" /> </g> <g> <g> <path d="M20,16H4V4h10.1c-0.08-0.39-0.18-1.11,0-2H4C2.9,2,2,2.9,2,4v18l4-4h14c1.1,0,2-0.9,2-2V6.98c-0.58,0.44-1.26,0.77-2,0.92 V16z" /> <circle cx="19" cy="3" r="3" /> <rect height="2" width="8" x="6" y="12" /> <rect height="2" width="12" x="6" y="9" /> <path d="M6,8h12V7.9c-1.21-0.25-2.25-0.95-2.97-1.9H6V8z" /> </g> </g> </svg>
|
|
Mark as unread
|
|
</button>
|
|
`;
|
|
}
|
|
let blockUnblock
|
|
if (messenger.blocked.has(chatAddress)) {
|
|
blockUnblock = html`
|
|
<button class="option" onclick="unblockUser()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>
|
|
Unblock
|
|
</button>
|
|
`;
|
|
} else {
|
|
blockUnblock = html`
|
|
<button class="option" onclick="blockUser()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z"/></svg>
|
|
Block
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
let deleteChat
|
|
if (addressType === 'plain') {
|
|
deleteChat = html`
|
|
<button id="delete_chat_option" class="option option--danger" onclick="deleteChat()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0V0z" fill="none" /> <path d="M16 9v10H8V9h8m-1.5-6h-5l-1 1H5v2h14V4h-3.5l-1-1zM18 7H6v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7z" /> </svg>
|
|
Delete chat
|
|
</button>
|
|
`
|
|
}
|
|
if (addressType === 'group') {
|
|
const { description, created } = messenger.groups[chatAddress]
|
|
contactInitial = html.node`<svg class="icon group-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M13.61,28.09c-1.63,0-4.72-2.35-5.33-3.58a21.65,21.65,0,0,1-1.35-7.32s-.26-6.07,6.68-6.07a6.38,6.38,0,0,1,6.69,6.07A21.65,21.65,0,0,1,19,24.51c-.62,1.23-3.7,3.58-5.34,3.58"/><path d="M50.39,28.09c-1.64,0-4.72-2.35-5.34-3.58a21.9,21.9,0,0,1-1.35-7.32s-.26-6.07,6.69-6.07a6.37,6.37,0,0,1,6.68,6.07,21.65,21.65,0,0,1-1.35,7.32c-.61,1.23-3.7,3.58-5.33,3.58"/><path d="M32,31.74c-2.21,0-6.37-3.17-7.2-4.83A29.3,29.3,0,0,1,23,17s-.35-8.21,9-8.21c8.68,0,9,8.21,9,8.21a29.3,29.3,0,0,1-1.83,9.88c-.82,1.66-5,4.83-7.2,4.83"/><path d="M48.29,38.58c-4.16-1.83-8.57-3.08-10.34-6.4a12,12,0,0,1-6,3.73,12,12,0,0,1-5.95-3.73c-1.77,3.32-6.18,4.57-10.34,6.4-1.7.71-3.11,9.88-1.13,9.88A33.06,33.06,0,0,0,31.23,53h1.54a33.06,33.06,0,0,0,16.65-4.53C51.4,48.46,50,39.29,48.29,38.58Z"/><path d="M14.82,36.57c.76-.33,1.54-.65,2.3-1,2.49-1,4.85-2,6.22-3.44C21.07,31.23,19,30.25,18,28.41a8.83,8.83,0,0,1-4.41,2.76,8.83,8.83,0,0,1-4.4-2.76c-1.31,2.46-4.58,3.38-7.66,4.74-1.26.52-2.3,7.31-.84,7.31a24.55,24.55,0,0,0,10.86,3.31C11.89,40.81,12.86,37.39,14.82,36.57Z"/><path d="M62.45,33.15c-3.08-1.36-6.35-2.28-7.66-4.74a8.83,8.83,0,0,1-4.4,2.76A8.83,8.83,0,0,1,46,28.41c-1,1.84-3,2.82-5.32,3.76,1.37,1.43,3.73,2.41,6.22,3.44.76.31,1.54.63,2.26,1,2,.83,3,4.25,3.29,7.21a24.55,24.55,0,0,0,10.86-3.31C64.75,40.46,63.71,33.67,62.45,33.15Z"/></svg>`
|
|
lastInteraction = `Created ${getFormattedTime(created)}`;
|
|
disableContactName = !isAdmin
|
|
groupDescription = description === '' ? isAdmin ? 'Add group description' : null : description
|
|
getRef('contact_details_popup').classList.add('is-group');
|
|
floAddressType = 'Group FLO address'
|
|
} else if (addressType === 'pipeline') {
|
|
const { model, members } = messenger.pipeline[chatAddress]
|
|
contactInitial = html.node`<svg class="icon group-icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"></rect></g><g><g><path d="M6,15c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S4.9,15,6,15 M6,13c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4S8.2,13,6,13z M12,5 c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S10.9,5,12,5 M12,3C9.8,3,8,4.8,8,7s1.8,4,4,4s4-1.8,4-4S14.2,3,12,3z M18,15 c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S16.9,15,18,15 M18,13c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4S20.2,13,18,13z"></path></g></g></svg>`
|
|
getRef('contact_details_popup').classList.add('is-group');
|
|
floAddressType = `${messenger.pipeline[chatAddress].model === 'flo_multisig' ? 'FLO' : 'BTC'} Multisig address`;
|
|
} else {
|
|
disableContactName = false;
|
|
contactInitial = getContactName(chatAddress).charAt(0);
|
|
floAddressType = 'FLO address';
|
|
getRef('contact_details_popup').classList.remove('is-group');
|
|
}
|
|
if (addressType === 'pipeline') {
|
|
try {
|
|
// Get pipeline origin multisig address
|
|
getTxHex(chatAddress).then(async tx_hex => {
|
|
let details
|
|
switch (messenger.pipeline[chatAddress].model) {
|
|
case 'flo_multisig':
|
|
details = await floBlockchainAPI.parseTransaction(tx_hex)
|
|
break;
|
|
case 'btc_multisig':
|
|
details = await btcOperator.parseTransaction(tx_hex)
|
|
break;
|
|
}
|
|
contactFloAddress = details.inputs[0].address
|
|
}).catch(e => {
|
|
console.error(e)
|
|
})
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
} else {
|
|
contactFloAddress = validateEthAddress(chatAddress) ? floCrypto.getFloID(floGlobals.pubKeys[chatAddress] || getEthPubKey(chatAddress)) : floCrypto.toFloID(chatAddress);
|
|
}
|
|
if (addressType === 'plain') {
|
|
if (contactFloAddress)
|
|
contactBtcAddress = btcOperator.convert.legacy2bech(contactFloAddress);
|
|
if (validateEthAddress(chatAddress))
|
|
contactEthAddress = chatAddress;
|
|
else if (floGlobals.pubKeys[chatAddress])
|
|
contactEthAddress = floEthereum.ethAddressFromCompressedPublicKey(floGlobals.pubKeys[chatAddress])
|
|
}
|
|
const contactName = getContactName(chatAddress) === chatAddress ? 'Unnamed' : getContactName(chatAddress);
|
|
renderElem(getRef('contact_details_section'), html`
|
|
<div>
|
|
<div class="flex flex-direction-column align-center">
|
|
<div id="contact_initial" class="initial flex align-center" style=${`--contact-color: var(${contactColor(chatAddress)})`}>
|
|
${contactInitial}
|
|
</div>
|
|
<text-field id="contact_name" value=${contactName} onchange=${(e) => changeContactName(e.target.value.trim())} ?disabled=${disableContactName}></text-field>
|
|
<p id="last_interaction_time">${lastInteraction}</p>
|
|
</div>
|
|
<fieldset id="contact_options" class="flex flex-direction-column gap-0-3">
|
|
${addAsContact}
|
|
${markReadUnread}
|
|
${blockUnblock}
|
|
<div class="flex gap-0-3">
|
|
<button class="option option--danger" onclick="clearChat()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24" /> </g> <g> <g> <path d="M16,11h-1V3c0-1.1-0.9-2-2-2h-2C9.9,1,9,1.9,9,3v8H8c-2.76,0-5,2.24-5,5v7h18v-7C21,13.24,18.76,11,16,11z M11,3h2v8h-2V3 z M19,21h-2v-3c0-0.55-0.45-1-1-1s-1,0.45-1,1v3h-2v-3c0-0.55-0.45-1-1-1s-1,0.45-1,1v3H9v-3c0-0.55-0.45-1-1-1s-1,0.45-1,1v3H5 v-5c0-1.65,1.35-3,3-3h8c1.65,0,3,1.35,3,3V21z" /> </g> </g> </svg>
|
|
Clear chat
|
|
</button>
|
|
${deleteChat}
|
|
</div>
|
|
</fieldset>
|
|
${contactFloAddress ? html`
|
|
<div class="popup-section">
|
|
<h5>${floAddressType}</h5>
|
|
<sm-copy value=${contactFloAddress}></sm-copy>
|
|
</div>
|
|
`: ''}
|
|
${contactBtcAddress ? html`
|
|
<div class="popup-section">
|
|
<h5>BTC address</h5>
|
|
<sm-copy value=${contactBtcAddress}></sm-copy>
|
|
</div>
|
|
`: ''}
|
|
${contactEthAddress ? html`
|
|
<div class="popup-section">
|
|
<h5>Eth address</h5>
|
|
<sm-copy value=${contactEthAddress}></sm-copy>
|
|
</div>
|
|
`: ''}
|
|
${addressType === 'group' && groupDescription ? html`
|
|
<div class="popup-section">
|
|
<h5>Group description</h5>
|
|
<text-field value=${groupDescription} onchange=${e => handleGroupDescriptionChange(e)} ?disabled=${!isAdmin}></text-field>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
${addressType !== 'plain' ? html`
|
|
<div id="group_members_card">
|
|
<div class="flex align-center">
|
|
<h4 class="h4">Members</h4>
|
|
${isAdmin ? html`
|
|
<button id="edit_group_button" class="button justify-right" onclick=${() => editGroupMembers(chatAddress)}>
|
|
Edit
|
|
</button>
|
|
`: ''}
|
|
</div>
|
|
${isAdmin ? html`
|
|
<p id="group_members_tip" class=${`tip`}>Select members to remove or add new members</p>
|
|
`: ''}
|
|
<div id="member_options" class="flex hidden">
|
|
<button id="remove_members_button" class="button button--danger hidden"
|
|
onclick="removeGroupMembers()">
|
|
Remove selected</button>
|
|
<button id="init_add_members_button" class="button" onclick="openPopup('contacts_popup')">Add
|
|
member</button>
|
|
</div>
|
|
<div id="group_members_list" onchange=${e => selectMemberToRemove(e.target.value)}></div>
|
|
</div>
|
|
`: ''}
|
|
`);
|
|
if (addressType === 'group') {
|
|
render.groupMembers(chatAddress)
|
|
} else if (addressType === 'pipeline') {
|
|
render.pipelineMembers(chatAddress)
|
|
} else {
|
|
}
|
|
break;
|
|
case 'contacts_popup':
|
|
const contacts = []
|
|
const groupID = getRef('edit_group_button').dataset.groupId;
|
|
for (const contact in floGlobals.contacts) {
|
|
if (!messenger.groups[groupID].members.includes(contact) && floDapps.user.get_pubKey(contact)) {
|
|
contacts.push(render.selectableContact(contact))
|
|
}
|
|
}
|
|
renderElem(getRef('popup_contacts_container'), html`${contacts}`)
|
|
isRemovingMember = false
|
|
break
|
|
case 'new_message_popup':
|
|
if (Object.keys(floGlobals.contacts).length) {
|
|
getRef('search_contacts').classList.remove('hidden')
|
|
} else {
|
|
getRef('search_contacts').classList.add('hidden')
|
|
}
|
|
renderContactList()
|
|
break
|
|
case 'creation_popup': {
|
|
renderCreationList()
|
|
break;
|
|
}
|
|
case 'compose_mail_popup':
|
|
const mailingContacts = []
|
|
for (const chatAddress in floGlobals.contacts) {
|
|
mailingContacts.push(render.contactOnly(chatAddress))
|
|
}
|
|
renderElem(getRef('mail_contact_list'), html`${mailingContacts}`)
|
|
break;
|
|
case 'profile_popup':
|
|
renderElem(getRef('profile_popup__content'), render.profile())
|
|
break;
|
|
}
|
|
})
|
|
|
|
document.addEventListener('popupclosed', e => {
|
|
switch (e.target.id) {
|
|
case 'contact_details_popup':
|
|
renderElem(getRef('contact_details_section'), html``)
|
|
if (messenger.groups.hasOwnProperty(floGlobals.viewingDetailsOfAddress)) {
|
|
isGroupEditable = true;
|
|
}
|
|
break;
|
|
case 'contacts_popup':
|
|
renderElem(getRef('popup_contacts_container'), html``)
|
|
isRemovingMember = true
|
|
membersToAdd.clear()
|
|
break;
|
|
case 'new_message_popup':
|
|
getRef('search_contacts').value = ''
|
|
break;
|
|
case 'creation_popup':
|
|
showChildElement('creation_process', 0)
|
|
renderElem(getRef('select_contacts_container'), html``)
|
|
clearAllMembers()
|
|
break;
|
|
case 'multisig_tx_popup':
|
|
getRef('receiver_container').innerHTML = ''
|
|
resetMultisigProcess()
|
|
buttonLoader('initiate_transaction', false)
|
|
getRef('initiate_transaction').disabled = true
|
|
break;
|
|
case 'compose_mail_popup':
|
|
renderElem(getRef('mail_contact_list'), html``)
|
|
break;
|
|
}
|
|
if (popupStack.items.length === 0) {
|
|
getRef('main_page').removeAttribute('inert')
|
|
}
|
|
zIndex--;
|
|
})
|
|
window.addEventListener('popstate', e => {
|
|
if (!e.state) return
|
|
switch (e.state.type) {
|
|
case 'popup':
|
|
closePopup()
|
|
break;
|
|
}
|
|
})
|
|
|
|
const selectedColors = [
|
|
'--dark-red',
|
|
'--red',
|
|
'--kinda-pink',
|
|
'--purple',
|
|
'--shady-blue',
|
|
'--nice-blue',
|
|
'--maybe-cyan',
|
|
'--teal',
|
|
'--mint-green',
|
|
'--greenish-yellow',
|
|
'--yellowish-green',
|
|
'--dark-teal',
|
|
'--orange',
|
|
'--tangerine',
|
|
'--redish-orange',
|
|
]
|
|
function randomColor() {
|
|
return selectedColors[Math.floor(Math.random() * selectedColors.length)]
|
|
}
|
|
|
|
const contactsInfo = {}
|
|
function contactColor(floID) {
|
|
if (!contactsInfo[floID]) {
|
|
contactsInfo[floID] = randomColor()
|
|
}
|
|
return contactsInfo[floID]
|
|
}
|
|
|
|
// displays a popup for asking permission. Use this instead of JS confirm
|
|
const getConfirmation = (title, options = {}) => {
|
|
return new Promise(resolve => {
|
|
const { message = '', cancelText = 'Cancel', confirmText = 'OK', danger = false } = options
|
|
getRef('confirm_title').innerText = title;
|
|
getRef('confirm_message').innerText = message;
|
|
const cancelButton = getRef('confirmation_popup').querySelector('.cancel-button');
|
|
const confirmButton = getRef('confirmation_popup').querySelector('.confirm-button')
|
|
confirmButton.textContent = confirmText
|
|
cancelButton.textContent = cancelText
|
|
if (danger)
|
|
confirmButton.classList.add('button--danger')
|
|
else
|
|
confirmButton.classList.remove('button--danger')
|
|
const { opened, closed } = openPopup('confirmation_popup')
|
|
confirmButton.onclick = () => {
|
|
closePopup({ payload: true })
|
|
}
|
|
cancelButton.onclick = () => {
|
|
closePopup()
|
|
}
|
|
closed.then((payload) => {
|
|
confirmButton.onclick = null
|
|
cancelButton.onclick = null
|
|
if (payload)
|
|
resolve(true)
|
|
else
|
|
resolve(false)
|
|
})
|
|
})
|
|
}
|
|
// displays a popup for asking user input. Use this instead of JS prompt
|
|
function getPromptInput(title, message = '', options = {}) {
|
|
let { placeholder = '', isPassword = false, cancelText = 'Cancel', confirmText = 'OK', attributes = {} } = options
|
|
getRef('prompt_title').innerText = title;
|
|
getRef('prompt_message').innerText = message;
|
|
const cancelButton = getRef('prompt_popup').querySelector('.cancel-button');
|
|
const confirmButton = getRef('prompt_popup').querySelector('.confirm-button')
|
|
// remove all attribute except id
|
|
while (getRef('prompt_input').attributes.length > 0 && getRef('prompt_input').attributes[0].name !== 'id') {
|
|
getRef('prompt_input').removeAttribute(getRef('prompt_input').attributes[0].name)
|
|
}
|
|
if (isPassword) {
|
|
placeholder = 'Password'
|
|
getRef('prompt_input').setAttribute("type", "password")
|
|
}
|
|
for (const attr in attributes) {
|
|
getRef('prompt_input').setAttribute(attr, attributes[attr])
|
|
}
|
|
getRef('prompt_input').setAttribute("placeholder", placeholder)
|
|
getRef('prompt_input').focusIn()
|
|
cancelButton.textContent = cancelText;
|
|
confirmButton.textContent = confirmText;
|
|
openPopup('prompt_popup', true)
|
|
return new Promise((resolve, reject) => {
|
|
cancelButton.addEventListener('click', () => {
|
|
closePopup()
|
|
resolve(null)
|
|
}, { once: true })
|
|
confirmButton.addEventListener('click', () => {
|
|
closePopup()
|
|
resolve(getRef('prompt_input').value)
|
|
}, { once: true })
|
|
})
|
|
}
|
|
|
|
//Function for displaying toast notifications. pass in error for mode param if you want to show an error.
|
|
function notify(message, mode, options = {}) {
|
|
let icon
|
|
switch (mode) {
|
|
case 'success':
|
|
icon = `<svg class="icon icon--success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></svg>`
|
|
break;
|
|
case 'error':
|
|
icon = `<svg class="icon icon--error" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>`
|
|
options.timeout = 15000
|
|
break;
|
|
}
|
|
if (mode === 'error') {
|
|
console.error(message)
|
|
}
|
|
return getRef("notification_drawer").push(message, { icon, ...options });
|
|
}
|
|
|
|
function getFormattedTime(timestamp, format) {
|
|
try {
|
|
if (String(timestamp).length < 13)
|
|
timestamp *= 1000
|
|
let [day, month, date, year] = new Date(timestamp).toString().split(' '),
|
|
minutes = new Date(timestamp).getMinutes(),
|
|
hours = new Date(timestamp).getHours(),
|
|
currentTime = new Date().toString().split(' ')
|
|
|
|
minutes = minutes < 10 ? `0${minutes}` : minutes
|
|
let finalHours = ``;
|
|
if (hours > 12)
|
|
finalHours = `${hours - 12}:${minutes}`
|
|
else if (hours === 0)
|
|
finalHours = `12:${minutes}`
|
|
else
|
|
finalHours = `${hours}:${minutes}`
|
|
|
|
finalHours = hours >= 12 ? `${finalHours} PM` : `${finalHours} AM`
|
|
switch (format) {
|
|
case 'date-only':
|
|
return `${month} ${date}, ${year}`;
|
|
break;
|
|
case 'time-only':
|
|
return finalHours;
|
|
case 'relative':
|
|
// check if timestamp is older than a day
|
|
if (Date.now() - new Date(timestamp) < 60 * 60 * 24 * 1000)
|
|
return `${finalHours}`;
|
|
else
|
|
return relativeTime.from(timestamp)
|
|
default:
|
|
return `${month} ${date}, ${year} at ${finalHours}`;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
return timestamp;
|
|
}
|
|
}
|
|
// implement event delegation
|
|
function delegate(el, event, selector, fn) {
|
|
el.addEventListener(event, function (e) {
|
|
const potentialTarget = e.target.closest(selector)
|
|
if (potentialTarget) {
|
|
e.delegateTarget = potentialTarget
|
|
fn.call(this, e)
|
|
}
|
|
})
|
|
}
|
|
|
|
function getContactName(contactAddress) {
|
|
if (floDapps.user.get_contact(contactAddress))
|
|
return floDapps.user.get_contact(contactAddress)
|
|
else if (messenger.groups[contactAddress])
|
|
return messenger.groups[contactAddress].name
|
|
else if (floCrypto.isSameAddr(contactAddress, floDapps.user.id))
|
|
return 'You'
|
|
else
|
|
return contactAddress
|
|
}
|
|
window.addEventListener('hashchange', e => routeTo(window.location.hash))
|
|
window.addEventListener("load", () => {
|
|
document.body.classList.remove('hidden')
|
|
document.querySelectorAll('sm-input[data-address]').forEach(input => input.customValidation = (value) => {
|
|
if (!value) return { isValid: false, errorText: 'Please enter a FLO address' }
|
|
return {
|
|
isValid: floCrypto.validateAddr(value),
|
|
errorText: `Invalid FLO address.<br> It usually starts with "F"`
|
|
}
|
|
})
|
|
document.addEventListener('keyup', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closePopup()
|
|
}
|
|
})
|
|
document.addEventListener('copy', () => {
|
|
notify('copied', 'success')
|
|
})
|
|
document.addEventListener("pointerdown", (e) => {
|
|
if (e.target.closest("button:not([disabled]), .interactive")) {
|
|
createRipple(e, e.target.closest("button, .interactive"));
|
|
}
|
|
});
|
|
document.querySelectorAll('.popup__header__close, .close-popup-on-click').forEach(elem => {
|
|
elem.addEventListener('click', () => {
|
|
closePopup()
|
|
})
|
|
})
|
|
getRef('accent_color_selector').colors = selectedColors
|
|
if (localStorage.getItem(`accent-color`)) {
|
|
const color = localStorage.getItem(`accent-color`)
|
|
getRef('accent_color_selector').selectedColor = color
|
|
document.body.style.setProperty('--accent-color', `var(${color})`);
|
|
}
|
|
else {
|
|
getRef('accent_color_selector').selectedColor = '--nice-blue'
|
|
}
|
|
document.addEventListener('colorselected', e => {
|
|
const color = e.detail.value
|
|
localStorage.setItem(`accent-color`, color);
|
|
document.body.style.setProperty('--accent-color', `var(${color})`);
|
|
})
|
|
});
|
|
function createRipple(event, target) {
|
|
const circle = document.createElement("span");
|
|
const diameter = Math.max(target.clientWidth, target.clientHeight);
|
|
const radius = diameter / 2;
|
|
const targetDimensions = target.getBoundingClientRect();
|
|
circle.style.width = circle.style.height = `${diameter}px`;
|
|
circle.style.left = `${event.clientX - (targetDimensions.left + radius)}px`;
|
|
circle.style.top = `${event.clientY - (targetDimensions.top + radius)}px`;
|
|
circle.classList.add("ripple");
|
|
const rippleAnimation = circle.animate(
|
|
[
|
|
{
|
|
transform: "scale(4)",
|
|
opacity: 0,
|
|
},
|
|
],
|
|
{
|
|
duration: 600,
|
|
fill: "forwards",
|
|
easing: "ease-out",
|
|
}
|
|
);
|
|
target.append(circle);
|
|
rippleAnimation.onfinish = () => {
|
|
circle.remove();
|
|
};
|
|
}
|
|
|
|
const appState = {
|
|
params: {},
|
|
openedPages: new Set(),
|
|
}
|
|
const generalPages = ['sign_up', 'sign_in', 'loading', 'landing']
|
|
async function routeTo(targetPage, options = {}) {
|
|
const { firstLoad } = options
|
|
let pageId
|
|
let subPageId1
|
|
let subPageId2
|
|
let searchParams
|
|
let params
|
|
if (targetPage === '') {
|
|
try {
|
|
if (floDapps.user.id)
|
|
pageId = 'chat_page'
|
|
} catch (e) {
|
|
pageId = 'landing'
|
|
}
|
|
} else {
|
|
if (targetPage.includes('/')) {
|
|
let path;
|
|
[path, searchParams] = targetPage.split('?');
|
|
[, pageId, subPageId1, subPageId2] = path.split('/')
|
|
} else {
|
|
pageId = targetPage
|
|
}
|
|
}
|
|
|
|
if (!document.querySelector(`#${pageId}`)?.classList.contains('inner-page')) return
|
|
try {
|
|
if (floDapps.user.id && (generalPages.includes(pageId))) {
|
|
history.replaceState(null, null, '#/chat_page');
|
|
pageId = 'chat_page'
|
|
}
|
|
} catch (e) {
|
|
if (!(generalPages.includes(pageId))) return
|
|
}
|
|
appState.currentPage = pageId
|
|
|
|
if (searchParams) {
|
|
const urlSearchParams = new URLSearchParams('?' + searchParams);
|
|
params = Object.fromEntries(urlSearchParams.entries());
|
|
}
|
|
if (params)
|
|
appState.params = params
|
|
switch (pageId) {
|
|
case 'sign_in':
|
|
setTimeout(() => {
|
|
getRef('private_key_field').focusIn()
|
|
}, 0);
|
|
break;
|
|
case 'sign_up':
|
|
getRef('keys_generator').generateKeys()
|
|
break;
|
|
case 'chat_page':
|
|
if (subPageId1 && params.address) {
|
|
getRef('chats_list').querySelectorAll('.active').forEach(child => child.classList.remove('active'))
|
|
const targetChatCard = getChatCard(params.address)
|
|
if (targetChatCard) {
|
|
targetChatCard.classList.remove('unread')
|
|
document.title = `RanchiMall Messenger`
|
|
targetChatCard.classList.add('active')
|
|
}
|
|
await viewConversation(params.address)
|
|
setTimeout(() => {
|
|
if (!chatScrollInfo.isScrolledUp) {
|
|
scrollToBottom()
|
|
}
|
|
}, 0);
|
|
getRef('messages_container').animate([
|
|
{ opacity: 0 },
|
|
{ opacity: 1 },
|
|
], {
|
|
duration: 150,
|
|
fill: 'forwards',
|
|
easing: 'ease-out',
|
|
})
|
|
getRef('chat_view').classList.remove('hidden')
|
|
getRef('chat_view').classList.remove('hide-on-mobile')
|
|
getRef('chat_view').nextElementSibling.classList.add('hidden')
|
|
getRef('contacts').classList.add('hide-on-mobile')
|
|
getRef('main_navbar').classList.add('hide-on-mobile')
|
|
} else {
|
|
if (activeChat.address && !floGlobals.isMobileView) {
|
|
history.replaceState(null, null, `#/chat_page/messages?address=${activeChat.address}`);
|
|
} else {
|
|
history.replaceState(null, null, '#/chat_page');
|
|
getRef('chat_view').classList.add('hidden')
|
|
getRef('chat_view').nextElementSibling.classList.remove('hidden')
|
|
getRef('contacts').classList.remove('hide-on-mobile')
|
|
getRef('chats_list').querySelector('.active')?.classList.remove('active')
|
|
activeChat = {}
|
|
getRef('main_navbar').classList.remove('hide-on-mobile')
|
|
}
|
|
}
|
|
messenger.list_request_received({ completed: false }).then(requests => addNotificationBadge('#notification_panel_button', Object.keys(requests).length, { replace: true }))
|
|
removeNotificationBadge('#chat_page_button')
|
|
if (floGlobals.idInterval)
|
|
clearInterval(floGlobals.idInterval)
|
|
let showingFloID = true
|
|
// alternating between floID and btcID every 10 seconds
|
|
floGlobals.idInterval = setInterval(() => {
|
|
document.querySelectorAll('.user-profile-id').forEach(el => el.textContent = showingFloID ? floGlobals.myFloID : floGlobals.myBtcID)
|
|
showingFloID = !showingFloID
|
|
}, 10000)
|
|
break;
|
|
case 'mail_page':
|
|
if (subPageId1) {
|
|
let childIndex
|
|
switch (subPageId1) {
|
|
case 'inbox':
|
|
childIndex = 0
|
|
break;
|
|
case 'sent':
|
|
childIndex = 1
|
|
break;
|
|
}
|
|
showChildElement('mail_sections', childIndex)
|
|
getRef("mail_type_selector").value = subPageId1
|
|
if (subPageId2 && activeMail) {
|
|
getRef('mail').classList.remove('hidden')
|
|
getRef('mail').classList.remove('hide-on-mobile')
|
|
getRef('mails').classList.add('hide-on-mobile')
|
|
getRef('main_navbar').classList.add('hide-on-mobile')
|
|
} else {
|
|
history.replaceState(null, null, `#/mail_page/${subPageId1}`);
|
|
getRef('mail').classList.add('hide-on-mobile')
|
|
getRef('mails').classList.remove('hide-on-mobile')
|
|
activeMail = ''
|
|
getRef('main_navbar').classList.remove('hide-on-mobile')
|
|
}
|
|
}
|
|
removeNotificationBadge('#mail_page_button')
|
|
break;
|
|
case 'settings':
|
|
if (subPageId1) {
|
|
showPanel(subPageId1)
|
|
} else {
|
|
if (!floGlobals.isMobileView) {
|
|
history.replaceState(null, null, '#/settings/personalize');
|
|
showPanel('personalize')
|
|
} else
|
|
hidePanel()
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (appState.lastPage !== pageId) {
|
|
const animOptions = {
|
|
duration: 100,
|
|
fill: 'forwards',
|
|
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
|
|
}
|
|
let previousActiveElement = getRef('main_navbar').querySelector('.nav-item--active')
|
|
const currentActiveElement = document.querySelector(`.nav-item[href="#/${pageId}"]`)
|
|
if (currentActiveElement) {
|
|
getRef('main_page').classList.remove('nav-hidden')
|
|
if (getRef('main_navbar').classList.contains('hidden')) {
|
|
getRef('main_navbar').classList.remove('hide-away', 'hidden')
|
|
getRef('main_navbar').animate([
|
|
{
|
|
transform: floGlobals.isMobileView ? `translateY(100%)` : `translateX(-100%)`,
|
|
opacity: 0,
|
|
},
|
|
{
|
|
transform: `none`,
|
|
opacity: 1,
|
|
},
|
|
], { ...animOptions, easing: 'ease-in' })
|
|
}
|
|
const previousActiveElementIndex = [...getRef('main_navbar').querySelectorAll('.nav-item')].indexOf(previousActiveElement)
|
|
const currentActiveElementIndex = [...getRef('main_navbar').querySelectorAll('.nav-item')].indexOf(currentActiveElement)
|
|
const isOnTop = previousActiveElementIndex < currentActiveElementIndex
|
|
const currentIndicator = createElement('div', { className: 'nav-item__indicator' });
|
|
let previousIndicator = getRef('main_navbar').querySelector('.nav-item__indicator')
|
|
if (!previousIndicator) {
|
|
previousIndicator = currentIndicator.cloneNode(true)
|
|
previousActiveElement = currentActiveElement
|
|
previousActiveElement.append(previousIndicator)
|
|
} else if (currentActiveElementIndex !== previousActiveElementIndex) {
|
|
const indicatorDimensions = previousIndicator.getBoundingClientRect()
|
|
const currentActiveElementDimensions = currentActiveElement.getBoundingClientRect()
|
|
let moveBy
|
|
if (floGlobals.isMobileView) {
|
|
moveBy = ((currentActiveElementDimensions.width - indicatorDimensions.width) / 2) + indicatorDimensions.width
|
|
} else {
|
|
moveBy = ((currentActiveElementDimensions.height - indicatorDimensions.height) / 2) + indicatorDimensions.height
|
|
}
|
|
indicatorObserver.observe(previousIndicator)
|
|
previousIndicator.animate([
|
|
{
|
|
transform: 'none',
|
|
opacity: 1,
|
|
},
|
|
{
|
|
transform: `translate${floGlobals.isMobileView ? 'X' : 'Y'}(${isOnTop ? `${moveBy}px` : `-${moveBy}px`})`,
|
|
opacity: 0,
|
|
},
|
|
], { ...animOptions, easing: 'ease-in' }).onfinish = () => {
|
|
previousIndicator.remove()
|
|
}
|
|
tempData = {
|
|
currentActiveElement,
|
|
currentIndicator,
|
|
isOnTop,
|
|
animOptions,
|
|
moveBy
|
|
}
|
|
}
|
|
previousActiveElement.classList.remove('nav-item--active');
|
|
currentActiveElement.classList.add('nav-item--active')
|
|
} else {
|
|
getRef('main_page').classList.add('nav-hidden')
|
|
if (!getRef('main_navbar').classList.contains('hidden')) {
|
|
getRef('main_navbar').classList.add('hide-away')
|
|
getRef('main_navbar').animate([
|
|
{
|
|
transform: `none`,
|
|
opacity: 1,
|
|
},
|
|
{
|
|
transform: floGlobals.isMobileView ? `translateY(100%)` : `translateX(-100%)`,
|
|
opacity: 0,
|
|
},
|
|
], {
|
|
duration: 200,
|
|
fill: 'forwards',
|
|
easing: 'ease'
|
|
}).onfinish = () => {
|
|
getRef('main_navbar').classList.add('hidden')
|
|
}
|
|
}
|
|
}
|
|
document.querySelectorAll('.page').forEach(page => page.classList.add('hidden'))
|
|
getRef(pageId).closest('.page').classList.remove('hidden')
|
|
document.querySelectorAll('.inner-page').forEach(page => page.classList.add('hidden'))
|
|
getRef(pageId).classList.remove('hidden')
|
|
getRef(pageId).animate([
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateY(1rem)'
|
|
},
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateY(0)'
|
|
},
|
|
],
|
|
{
|
|
duration: 300,
|
|
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
|
|
}).onfinish = () => {
|
|
}
|
|
appState.lastPage = pageId
|
|
}
|
|
if (params)
|
|
appState.params = params
|
|
appState.openedPages.add(pageId)
|
|
|
|
}
|
|
const indicatorObserver = new IntersectionObserver(entries => {
|
|
entries.forEach(entry => {
|
|
if (!entry.isIntersecting) {
|
|
const { currentActiveElement, currentIndicator, isOnTop, animOptions, moveBy } = tempData
|
|
currentActiveElement.append(currentIndicator)
|
|
currentIndicator.animate([
|
|
{
|
|
transform: `translate${floGlobals.isMobileView ? 'X' : 'Y'}(${isOnTop ? `-${moveBy}px` : `${moveBy}px`})`,
|
|
opacity: 0,
|
|
},
|
|
{
|
|
transform: 'none',
|
|
opacity: 1
|
|
},
|
|
], { ...animOptions, easing: 'ease-out' })
|
|
}
|
|
})
|
|
}, {
|
|
threshold: 1
|
|
})
|
|
|
|
|
|
// class based lazy loading
|
|
class LazyLoader {
|
|
constructor(container, elementsToRender, renderFn, options = {}) {
|
|
const { batchSize = 10, freshRender, bottomFirst = false, onEnd } = options
|
|
|
|
this.elementsToRender = elementsToRender
|
|
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
|
|
this.renderFn = renderFn
|
|
this.intersectionObserver
|
|
|
|
this.batchSize = batchSize
|
|
this.freshRender = freshRender
|
|
this.bottomFirst = bottomFirst
|
|
this.onEnd = onEnd
|
|
|
|
this.shouldLazyLoad = false
|
|
this.lastScrollTop = 0
|
|
this.lastScrollHeight = 0
|
|
|
|
this.lazyContainer = document.querySelector(container)
|
|
|
|
this.update = this.update.bind(this)
|
|
this.render = this.render.bind(this)
|
|
this.init = this.init.bind(this)
|
|
this.clear = this.clear.bind(this)
|
|
}
|
|
get elements() {
|
|
return this.arrayOfElements
|
|
}
|
|
init() {
|
|
if (this.mutationObserver)
|
|
this.mutationObserver.disconnect()
|
|
if (this.intersectionObserver)
|
|
this.intersectionObserver.disconnect()
|
|
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
observer.disconnect()
|
|
this.render({ lazyLoad: true })
|
|
}
|
|
})
|
|
}, {
|
|
root: this.lazyContainer
|
|
})
|
|
this.mutationObserver = new MutationObserver(mutationList => {
|
|
mutationList.forEach(mutation => {
|
|
if (mutation.type === 'childList') {
|
|
if (mutation.addedNodes.length) {
|
|
if (this.bottomFirst) {
|
|
if (this.lazyContainer.firstElementChild)
|
|
this.intersectionObserver.observe(this.lazyContainer.firstElementChild)
|
|
} else {
|
|
if (this.lazyContainer.lastElementChild)
|
|
this.intersectionObserver.observe(this.lazyContainer.lastElementChild)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
this.mutationObserver.observe(this.lazyContainer, {
|
|
childList: true,
|
|
})
|
|
this.render()
|
|
}
|
|
update(elementsToRender) {
|
|
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
|
|
}
|
|
render(options = {}) {
|
|
let { lazyLoad = false } = options
|
|
this.shouldLazyLoad = lazyLoad
|
|
const frag = document.createDocumentFragment();
|
|
if (lazyLoad) {
|
|
if (this.bottomFirst) {
|
|
this.updateEndIndex = this.updateStartIndex
|
|
this.updateStartIndex = this.updateEndIndex - this.batchSize
|
|
} else {
|
|
this.updateStartIndex = this.updateEndIndex
|
|
this.updateEndIndex = this.updateEndIndex + this.batchSize
|
|
}
|
|
} else {
|
|
this.intersectionObserver.disconnect()
|
|
if (this.bottomFirst) {
|
|
this.updateEndIndex = this.arrayOfElements.length
|
|
this.updateStartIndex = this.updateEndIndex - this.batchSize - 1
|
|
} else {
|
|
this.updateStartIndex = 0
|
|
this.updateEndIndex = this.batchSize
|
|
}
|
|
this.lazyContainer.innerHTML = ``;
|
|
}
|
|
this.updateStartIndex = Math.max(this.updateStartIndex, 0)
|
|
this.lastScrollHeight = this.lazyContainer.scrollHeight
|
|
this.lastScrollTop = this.lazyContainer.scrollTop
|
|
this.arrayOfElements.slice(this.updateStartIndex, this.updateEndIndex).forEach((element, index) => {
|
|
frag.append(this.renderFn(element))
|
|
})
|
|
if (this.bottomFirst) {
|
|
this.lazyContainer.prepend(frag)
|
|
// scroll anchoring for reverse scrolling
|
|
this.lastScrollTop += this.lazyContainer.scrollHeight - this.lastScrollHeight
|
|
this.lazyContainer.scrollTo({ top: this.lastScrollTop })
|
|
this.lastScrollHeight = this.lazyContainer.scrollHeight
|
|
if (this.updateStartIndex <= 0 && this.onEnd) {
|
|
this.mutationObserver.disconnect()
|
|
this.intersectionObserver.disconnect()
|
|
this.onEnd()
|
|
}
|
|
} else {
|
|
this.lazyContainer.append(frag)
|
|
if (this.updateEndIndex >= this.arrayOfElements.length && this.onEnd) {
|
|
this.mutationObserver.disconnect()
|
|
this.intersectionObserver.disconnect()
|
|
this.onEnd()
|
|
}
|
|
}
|
|
if (!lazyLoad && this.bottomFirst) {
|
|
this.lazyContainer.scrollTop = this.lazyContainer.scrollHeight
|
|
}
|
|
// Callback to be called if elements are updated or rendered for first time
|
|
if (!lazyLoad && this.freshRender)
|
|
this.freshRender()
|
|
}
|
|
clear() {
|
|
this.intersectionObserver.disconnect()
|
|
this.mutationObserver.disconnect()
|
|
this.lazyContainer.innerHTML = ``;
|
|
}
|
|
reset() {
|
|
this.arrayOfElements = (typeof this.elementsToRender === 'function') ? this.elementsToRender() : this.elementsToRender || []
|
|
this.render()
|
|
}
|
|
}
|
|
|
|
function buttonLoader(id, show) {
|
|
const button = typeof id === 'string' ? getRef(id) : id;
|
|
button.disabled = show;
|
|
const animOptions = {
|
|
duration: 200,
|
|
fill: 'forwards',
|
|
easing: 'ease'
|
|
}
|
|
if (show) {
|
|
button.animate([
|
|
{
|
|
clipPath: 'circle(100%)',
|
|
},
|
|
{
|
|
clipPath: 'circle(0)',
|
|
},
|
|
], animOptions)
|
|
button.parentNode.append(createElement('sm-spinner'))
|
|
} else {
|
|
button.animate([
|
|
{
|
|
clipPath: 'circle(0)',
|
|
},
|
|
{
|
|
clipPath: 'circle(100%)',
|
|
},
|
|
], animOptions).onfinish = () => {
|
|
const potentialTarget = button.parentNode.querySelector('sm-spinner')
|
|
if (potentialTarget) potentialTarget.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
const mobileQuery = window.matchMedia('(max-width: 40rem)')
|
|
function handleMobileChange(e) {
|
|
floGlobals.isMobileView = e.matches
|
|
if (floGlobals.isMobileView) {
|
|
} else {
|
|
getRef('settings_sidebar').style = ''
|
|
getRef('settings_panel').style = ''
|
|
}
|
|
}
|
|
mobileQuery.addEventListener('change', handleMobileChange)
|
|
handleMobileChange(mobileQuery)
|
|
|
|
document.addEventListener("visibilitychange", handleVisibilityChange, false);
|
|
function handleVisibilityChange() {
|
|
if (document.visibilityState === "hidden") {
|
|
// code if page is hidden
|
|
} else {
|
|
if (activeChat.address) {
|
|
if (!chatScrollInfo.isScrolledUp) {
|
|
scrollToBottom()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
const slideInLeft = [
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateX(1.5rem)'
|
|
},
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateX(0)'
|
|
}
|
|
]
|
|
const slideOutLeft = [
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateX(0)'
|
|
},
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateX(-1.5rem)'
|
|
},
|
|
]
|
|
const slideInRight = [
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateX(-1.5rem)'
|
|
},
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateX(0)'
|
|
}
|
|
]
|
|
const slideOutRight = [
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateX(0)'
|
|
},
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateX(1.5rem)'
|
|
},
|
|
]
|
|
const slideInDown = [
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateY(-1.5rem)'
|
|
},
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateY(0)'
|
|
},
|
|
]
|
|
const slideOutUp = [
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateY(0)'
|
|
},
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateY(-1.5rem)'
|
|
},
|
|
]
|
|
const fadeIn = [
|
|
{
|
|
opacity: 0
|
|
},
|
|
{
|
|
opacity: 1
|
|
}
|
|
]
|
|
const fadeOut = [
|
|
{
|
|
opacity: 1
|
|
},
|
|
{
|
|
opacity: 0
|
|
}
|
|
]
|
|
|
|
function showChildElement(id, index, options = {}) {
|
|
return new Promise((resolve) => {
|
|
const { mobileView = false, entry, exit } = options
|
|
const animOptions = {
|
|
duration: 150,
|
|
easing: 'ease',
|
|
fill: 'forwards'
|
|
}
|
|
const parent = typeof id === 'string' ? document.getElementById(id) : id;
|
|
const visibleElement = [...parent.children].find(elem => !elem.classList.contains(mobileView ? 'hide-on-mobile' : 'hidden'));
|
|
if (visibleElement === parent.children[index]) return;
|
|
visibleElement.getAnimations().forEach(anim => anim.cancel())
|
|
parent.children[index].getAnimations().forEach(anim => anim.cancel())
|
|
if (visibleElement) {
|
|
if (exit) {
|
|
visibleElement.animate(exit, animOptions).onfinish = () => {
|
|
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden')
|
|
parent.children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
|
|
if (entry)
|
|
parent.children[index].animate(entry, animOptions).onfinish = () => resolve()
|
|
}
|
|
} else {
|
|
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden')
|
|
parent.children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
|
|
resolve()
|
|
}
|
|
} else {
|
|
parent.children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
|
|
parent.children[index].animate(entry, animOptions).onfinish = () => resolve()
|
|
}
|
|
})
|
|
}
|
|
function togglePrivateKeyVisibility(input) {
|
|
const target = input.closest('sm-input')
|
|
target.type = target.type === 'password' ? 'text' : 'password';
|
|
target.focusIn()
|
|
}
|
|
function getSignedIn(passwordType) {
|
|
return new Promise((resolve, reject) => {
|
|
routeTo(window.location.hash)
|
|
if (floGlobals.loaded) {
|
|
getPromptInput('Enter password', '', {
|
|
isPassword: true,
|
|
}).then(password => {
|
|
if (password) {
|
|
resolve(password)
|
|
}
|
|
})
|
|
} else {
|
|
if (passwordType === 'PIN/Password') {
|
|
floGlobals.isPrivKeySecured = true;
|
|
getRef('private_key_field').removeAttribute('data-private-key');
|
|
getRef('private_key_field').setAttribute('placeholder', 'Password');
|
|
getRef('private_key_field').customValidation = null
|
|
} else {
|
|
floGlobals.isPrivKeySecured = false;
|
|
getRef('private_key_field').dataset.privateKey = ''
|
|
getRef('private_key_field').setAttribute('placeholder', 'FLO/BTC private key');
|
|
getRef('private_key_field').customValidation = (value) => {
|
|
if (!value) return { isValid: false, errorText: 'Please enter a private key' }
|
|
return {
|
|
isValid: floCrypto.getPubKeyHex(value),
|
|
errorText: `Invalid private key.<br> It's a long string of random characters usually starting with 'R'.`
|
|
}
|
|
};
|
|
}
|
|
if (!generalPages.find(page => window.location.hash.includes(page))) {
|
|
location.hash = floGlobals.isPrivKeySecured ? '#/sign_in' : `#/landing`;
|
|
}
|
|
getRef('sign_in_button').onclick = () => {
|
|
resolve(getRef('private_key_field').value.trim());
|
|
getRef('private_key_field').value = '';
|
|
routeTo('loading');
|
|
};
|
|
getRef('sign_up_button').onclick = () => {
|
|
resolve(getRef('keys_generator').keys.privKey);
|
|
getRef('keys_generator').clearKeys();
|
|
routeTo('loading');
|
|
};
|
|
}
|
|
});
|
|
}
|
|
function setSecurePassword() {
|
|
if (!floGlobals.isPrivKeySecured) {
|
|
const password = getRef('secure_pwd_input').value.trim();
|
|
floDapps.securePrivKey(password).then(() => {
|
|
floGlobals.isPrivKeySecured = true;
|
|
notify('Password set successfully', 'success');
|
|
closePopup();
|
|
}).catch(err => {
|
|
notify(err, 'error');
|
|
})
|
|
}
|
|
}
|
|
async function clearCredentials() {
|
|
await floDapps.clearCredentials();
|
|
location.reload();
|
|
}
|
|
function signOut() {
|
|
getConfirmation('Sign out?', { message: 'You are about to sign out of the app, continue?', confirmText: 'Leave', cancelText: 'Stay', danger: true })
|
|
.then(async (res) => {
|
|
if (res) {
|
|
clearCredentials()
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
let activeChat = {};
|
|
let activeMail;
|
|
let renderedDates = {}
|
|
let lastSender = ''
|
|
|
|
// render elements
|
|
const render = {
|
|
mailCard(floID, ref, subject, timestamp, content, markUnread) {
|
|
let mailSummery = content.split(' ').splice(16).join(' ')
|
|
let contact = floGlobals.contacts[floID] || floID
|
|
if (Array.isArray(floID)) {
|
|
// find known floID name
|
|
contact = floID.find(id => floGlobals.contacts[id])
|
|
contact = floGlobals.contacts[contact] || `${floID[0].substring(12)}...`;
|
|
if (floID.length > 1)
|
|
contact = `${contact} & ${floID.length - 1} others(s)`
|
|
floID = floID.join(', ')
|
|
}
|
|
return html.node`
|
|
<li class="${`mail-card interactive ${markUnread ? 'unread' : ''}`}" data-ref="${ref}" style=${`--contact-color: var(${contactColor(floID)})`}>
|
|
<div class="initial flex align-center">${contact.charAt(0)}</div>
|
|
<h5 class="sender">${contact}</h5>
|
|
<time class="date">${getFormattedTime(timestamp, 'relative')}</time>
|
|
<h4 class="subject text-overflow">${subject}</h4>
|
|
<p class="description">${mailSummery}</p>
|
|
</li>
|
|
`
|
|
},
|
|
mail(from, to, subject, timestamp, content) {
|
|
let senderName, floID
|
|
if (from === floDapps.user.id) {
|
|
let count = 0, list = [];
|
|
to.forEach(f => floGlobals.contacts[f] ? list.push(getContactName(f)) : count++)
|
|
senderName = `To : ${list.join(', ')}`
|
|
if (count) {
|
|
if (list.length)
|
|
senderName = `${senderName} & ${count} other(s)`
|
|
else
|
|
senderName = `${senderName} ${count} Unknown people`
|
|
}
|
|
floID = to.join(', ')
|
|
} else {
|
|
senderName = getContactName(from);
|
|
floID = from
|
|
}
|
|
const backURL = `#/mail_page/${getRef('mail_type_selector').value}`
|
|
return html.node`
|
|
<div class="mail">
|
|
<header class="mail-header flex flex-direction-column">
|
|
<div class="flex space-between align-center">
|
|
<div class="flex align-center">
|
|
<a href="${backURL}" class="button icon-only back-button hide-on-desktop">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none"></path>
|
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"></path>
|
|
</svg>
|
|
</a>
|
|
<div class="initial flex align-center" style=${`--contact-color: var(${contactColor(floID)})`}>${senderName.charAt(0)}</div>
|
|
</div>
|
|
<time class="date justify-right">${getFormattedTime(timestamp)}</time>
|
|
</div>
|
|
<div class="mail-details flex flex-direction-column">
|
|
<h4 class="sender-name">${senderName}</h4>
|
|
<h5 class="flo-id text-overflow">${floID}</h5>
|
|
</div>
|
|
</header>
|
|
<h4 class="mail-subject">${subject}</h4>
|
|
<p class="mail-content">${content}</p>
|
|
</div>
|
|
`
|
|
},
|
|
contactCard(contactAddress, options = {}) {
|
|
let { type, prepend = false, markUnread = false, ref } = options
|
|
let name = getContactName(contactAddress)
|
|
let initial
|
|
if (type === 'group') {
|
|
initial = html`<svg class="icon group-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M13.61,28.09c-1.63,0-4.72-2.35-5.33-3.58a21.65,21.65,0,0,1-1.35-7.32s-.26-6.07,6.68-6.07a6.38,6.38,0,0,1,6.69,6.07A21.65,21.65,0,0,1,19,24.51c-.62,1.23-3.7,3.58-5.34,3.58"/><path d="M50.39,28.09c-1.64,0-4.72-2.35-5.34-3.58a21.9,21.9,0,0,1-1.35-7.32s-.26-6.07,6.69-6.07a6.37,6.37,0,0,1,6.68,6.07,21.65,21.65,0,0,1-1.35,7.32c-.61,1.23-3.7,3.58-5.33,3.58"/><path d="M32,31.74c-2.21,0-6.37-3.17-7.2-4.83A29.3,29.3,0,0,1,23,17s-.35-8.21,9-8.21c8.68,0,9,8.21,9,8.21a29.3,29.3,0,0,1-1.83,9.88c-.82,1.66-5,4.83-7.2,4.83"/><path d="M48.29,38.58c-4.16-1.83-8.57-3.08-10.34-6.4a12,12,0,0,1-6,3.73,12,12,0,0,1-5.95-3.73c-1.77,3.32-6.18,4.57-10.34,6.4-1.7.71-3.11,9.88-1.13,9.88A33.06,33.06,0,0,0,31.23,53h1.54a33.06,33.06,0,0,0,16.65-4.53C51.4,48.46,50,39.29,48.29,38.58Z"/><path d="M14.82,36.57c.76-.33,1.54-.65,2.3-1,2.49-1,4.85-2,6.22-3.44C21.07,31.23,19,30.25,18,28.41a8.83,8.83,0,0,1-4.41,2.76,8.83,8.83,0,0,1-4.4-2.76c-1.31,2.46-4.58,3.38-7.66,4.74-1.26.52-2.3,7.31-.84,7.31a24.55,24.55,0,0,0,10.86,3.31C11.89,40.81,12.86,37.39,14.82,36.57Z"/><path d="M62.45,33.15c-3.08-1.36-6.35-2.28-7.66-4.74a8.83,8.83,0,0,1-4.4,2.76A8.83,8.83,0,0,1,46,28.41c-1,1.84-3,2.82-5.32,3.76,1.37,1.43,3.73,2.41,6.22,3.44.76.31,1.54.63,2.26,1,2,.83,3,4.25,3.29,7.21a24.55,24.55,0,0,0,10.86-3.31C64.75,40.46,63.71,33.67,62.45,33.15Z"/></svg> `
|
|
name = messenger.groups[contactAddress].name
|
|
} else if (type === 'pipeline') {
|
|
initial = html`<svg class="icon group-icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M6,15c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S4.9,15,6,15 M6,13c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4S8.2,13,6,13z M12,5 c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S10.9,5,12,5 M12,3C9.8,3,8,4.8,8,7s1.8,4,4,4s4-1.8,4-4S14.2,3,12,3z M18,15 c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S16.9,15,18,15 M18,13c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4S20.2,13,18,13z"/></g></g></svg>`
|
|
} else {
|
|
initial = name.charAt(0)
|
|
}
|
|
if (type !== 'contact') {
|
|
//render chat card for newly added contact
|
|
getLastMessage(contactAddress).then(lastMessage => {
|
|
const { lastText, time } = lastMessage
|
|
const chatCard = getChatCard(contactAddress)
|
|
if (chatCard && !chatCard.querySelector('.last-message')) {
|
|
const timeAndOptions = html`
|
|
<time class="time">${time ? getFormattedTime(time, 'relative') : ''}</time>
|
|
<p class="last-message">${lastText}</p>
|
|
<button class="menu">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
|
</button>
|
|
`;
|
|
chatCard.append(html.node`${timeAndOptions}`)
|
|
if (time)
|
|
chatCard.classList.remove('collapsed')
|
|
}
|
|
}).catch(error => console.error(error))
|
|
if (prepend) {
|
|
activeChat.address = contactAddress
|
|
getRef('chats_list').querySelectorAll('.active').forEach(child => child.classList.remove('active'))
|
|
}
|
|
}
|
|
let tag = 'BTC'
|
|
if (type === 'pipeline' && messenger.pipeline[contactAddress].model === 'flo_multisig') {
|
|
tag = 'FLO'
|
|
}
|
|
const className = `contact ${type !== 'contact' ? type : ''} ${markUnread ? 'unread' : ''} interactive collapsed`
|
|
return html.for(ref, contactAddress)`
|
|
<div class="${className}" .dataset=${{ address: contactAddress }} style=${`--contact-color: var(${contactColor(contactAddress)})`}>
|
|
<div class="initial flex align-center">
|
|
${initial}
|
|
</div>
|
|
${type === 'pipeline' ? html`<div class="tag">${tag} Multisig ${messenger.pipeline[contactAddress].disabled ? ' - Completed' : ''}</div>` : ''}
|
|
<h4 class="name">${name}</h4>
|
|
${type === 'contact' ? html`<p class="contact__flo-address wrap-around">${contactAddress}</p>` : ''}
|
|
</div>
|
|
`
|
|
},
|
|
selectableContact(floID) {
|
|
const name = getContactName(floID)
|
|
const initial = name.charAt(0)
|
|
return html`
|
|
<label class="flex align-center selectable-contact interactive" data-address=${floID} style=${`--contact-color: var(${contactColor(floID)})`}>
|
|
<div class="initial flex align-center"> ${initial} </div>
|
|
<div class="grid gap-0-3">
|
|
<h4 class="name">${name}</h4>
|
|
<p class="contact__flo-address wrap-around">${floID}</p>
|
|
</div>
|
|
<input type="checkbox" autocomplete="off" value=${floID}/>
|
|
</label>
|
|
`
|
|
},
|
|
actionableContact(floID, hasSentRequest) {
|
|
const name = getContactName(floID)
|
|
const initial = name.charAt(0)
|
|
return html`
|
|
<div class="flex align-center selectable-contact" data-address=${floID} style=${`--contact-color: var(${contactColor(floID)})`}>
|
|
<div class="initial flex align-center"> ${initial} </div>
|
|
<div class="grid gap-0-3">
|
|
<h4 class="name">${name}</h4>
|
|
<p class="contact__flo-address wrap-around">${floID}</p>
|
|
</div>
|
|
<button class="button button--small request-pubkey" ?disabled=${hasSentRequest}>${hasSentRequest ? 'Request sent' : 'Request'}</button>
|
|
</div>
|
|
`
|
|
},
|
|
messageBubble(msg) {
|
|
let { admin = false, newMembers = [], rmMembers = [], groupID, name, description, sender, floID, message, time: timestamp, category, unconfirmed = false,
|
|
pipeID, type, tx_hex } = msg
|
|
if (activeChat.type === 'group') {
|
|
floID = groupID
|
|
category = sender === floDapps.user.id ? 'sent' : 'received'
|
|
} else if (activeChat.type === 'pipeline') {
|
|
floID = pipeID
|
|
category = sender === floDapps.user.id ? 'sent' : 'received'
|
|
}
|
|
if (message) {
|
|
let senderName = null
|
|
if (sender) {
|
|
if (sender !== floDapps.user.id && lastSender !== sender)
|
|
senderName = html.node`<a href="${`#/chat_page/messages?address=${sender}`}" class="sender-name" style=${`color: var(${contactColor(sender)})`}>${getContactName(sender)}</a>`
|
|
lastSender = sender
|
|
}
|
|
let messageContent = document.createDocumentFragment()
|
|
let isBigEmoji = false
|
|
if (hasURL(message)) {
|
|
const chunks = message.split(' ')
|
|
const chunksLength = chunks.length - 1
|
|
chunks.forEach((chunk, index) => {
|
|
if (hasURL(chunk)) {
|
|
const href = /^https?:\/\//i.test(chunk) ? chunk : `http://${chunk}`
|
|
const text = chunksLength !== index ? `${chunk} ` : chunk
|
|
const anchorTag = html.node`<a href="${href}" target="_blank" rel="noopener">${text}</a>`
|
|
messageContent.append(anchorTag)
|
|
} else {
|
|
const text = chunksLength !== index ? `${chunk} ` : chunk
|
|
messageContent.append(document.createTextNode(text))
|
|
}
|
|
})
|
|
} else {
|
|
messageContent.append(isEmoji(message))
|
|
}
|
|
const messageDate = getFormattedTime(timestamp, 'date-only')
|
|
// rendering Date card when date changes after a message
|
|
let dateCard
|
|
if (!renderedDates.hasOwnProperty(messageDate) || renderedDates[messageDate] > timestamp) {
|
|
getRef('messages_container').querySelectorAll(`.date-card[data-date="${messageDate}"]`).forEach(card => card.remove())
|
|
dateCard = html.node`<time class="date-card event-card" data-date="${messageDate}">${messageDate}</time>`
|
|
renderedDates[messageDate] = timestamp
|
|
}
|
|
const className = `message ${category} ${unconfirmed ? 'unconfirmed' : ''} ${senderName ? 'distinct-sender' : ''}`
|
|
return html.node`
|
|
${dateCard}
|
|
<div class="${className}" id="${`${floID}_${timestamp}`}" data-date="${messageDate}">
|
|
${senderName}
|
|
<p class="message-body">${messageContent}</p>
|
|
<time class="time">${getFormattedTime(timestamp, 'time-only')}</time>
|
|
</div>
|
|
`
|
|
} else if (admin) {
|
|
if (newMembers.length) {
|
|
const cards = document.createDocumentFragment()
|
|
const { admin } = messenger.groups[groupID]
|
|
newMembers.forEach(member => {
|
|
let eventCard = html.node`<p class="group-event-card event-card">${getContactName(admin)} added ${member === floDapps.user.id ? 'you' : getContactName(member)}</p>`
|
|
cards.append(eventCard)
|
|
})
|
|
return cards
|
|
} else if (rmMembers.length) {
|
|
const cards = document.createDocumentFragment()
|
|
const { admin } = messenger.groups[groupID]
|
|
rmMembers.forEach(member => {
|
|
let eventCard = html.node`<p class="group-event-card event-card">${getContactName(admin)} removed ${member === floDapps.user.id ? 'you' : getContactName(member)}</p>`
|
|
cards.append(eventCard)
|
|
})
|
|
return cards
|
|
} else if (name) {
|
|
return html.node`<p class="group-event-card event-card">Changed group name to '${name}'</p>`
|
|
} else if (description) {
|
|
return html.node`<p class="group-event-card event-card">Changed group description to '${description}'</p>`
|
|
}
|
|
} else if (type) {
|
|
switch (type) {
|
|
case 'TRANSACTION':
|
|
return html.node`<div class="pipeline-event pipeline-event--signed event-card flex align-center wrap-around">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M23,12l-2.44-2.79l0.34-3.69l-3.61-0.82L15.4,1.5L12,2.96L8.6,1.5L6.71,4.69L3.1,5.5L3.44,9.2L1,12l2.44,2.79l-0.34,3.7 l3.61,0.82L8.6,22.5l3.4-1.47l3.4,1.46l1.89-3.19l3.61-0.82l-0.34-3.69L23,12z M10.09,16.72l-3.8-3.81l1.48-1.48l2.32,2.33 l5.85-5.87l1.48,1.48L10.09,16.72z"/></g></svg>
|
|
<div class="grid gap-0-3">
|
|
<time class="time">${getFormattedTime(timestamp)}</time>
|
|
${getContactName(sender)} signed the transaction
|
|
</div>
|
|
</div>`
|
|
break;
|
|
case 'BROADCAST':
|
|
// render appropriate UI when transaction is broadcasted
|
|
if (getRef('transaction_details'))
|
|
getRef('transaction_details').remove()
|
|
// disable chatting
|
|
getRef('chat_footer').classList.add('hidden')
|
|
let redirectTo = `https://ranchimall.github.io/btcwallet/#/check_details?query=`
|
|
if (messenger.pipeline[activeChat.address].model === 'flo_multisig')
|
|
redirectTo = `https://blockbook.ranchimall.net/tx/`
|
|
return html.node`<div class="pipeline-event pipeline-event--signed event-card flex align-center wrap-around">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M23,12l-2.44-2.79l0.34-3.69l-3.61-0.82L15.4,1.5L12,2.96L8.6,1.5L6.71,4.69L3.1,5.5L3.44,9.2L1,12l2.44,2.79l-0.34,3.7 l3.61,0.82L8.6,22.5l3.4-1.47l3.4,1.46l1.89-3.19l3.61-0.82l-0.34-3.69L23,12z M10.09,16.72l-3.8-3.81l1.48-1.48l2.32,2.33 l5.85-5.87l1.48,1.48L10.09,16.72z"/></g></svg>
|
|
<div class="grid">
|
|
<time class="time">${getFormattedTime(timestamp)}</time>
|
|
<div class="flex align-center">
|
|
<h4>Transaction approved</h4>
|
|
<a class="button--small margin-left-0-5" href="${`${redirectTo}${msg.txid}`}" target="_blank">View on blockchain</a>
|
|
</div>
|
|
</div>
|
|
</div>`
|
|
break;
|
|
}
|
|
}
|
|
return html.node``
|
|
},
|
|
groupMembers(groupID, areSelectable = false) {
|
|
const groupMembersCards = [];
|
|
messenger.groups[groupID].members.forEach(floID => {
|
|
let isAdmin = messenger.groups[groupID].admin === floID ? true : false
|
|
let name = getContactName(floID)
|
|
let initial = name.charAt(0)
|
|
if (areSelectable) {
|
|
if (!isAdmin)
|
|
groupMembersCards.push(render.selectableContact(floID))
|
|
} else {
|
|
groupMembersCards.push(html`
|
|
<div class="group-member" .dataset=${{ address: floID }} style=${`--contact-color: var(${contactColor(floID)})`}>
|
|
<div class="initial flex align-center">
|
|
${initial}
|
|
</div>
|
|
<h4 class="name wrap-around">${name}</h4>
|
|
${isAdmin ? html`<p class="admin-tag">Admin</p>` : ''}
|
|
</div>
|
|
|
|
`)
|
|
}
|
|
})
|
|
renderElem(getRef('group_members_list'), html`${groupMembersCards}`)
|
|
},
|
|
pipelineMembers(pipeID) {
|
|
const groupMembersCards = [];
|
|
messenger.pipeline[pipeID].members.forEach(floID => {
|
|
let name = getContactName(floID)
|
|
let initial = name.charAt(0)
|
|
groupMembersCards.push(html`
|
|
<div class="group-member" .dataset=${{ address: floID }} style=${`--contact-color: var(${contactColor(floID)})`}>
|
|
<div class="initial flex align-center"> ${initial} </div>
|
|
<h4 class="name wrap-around">${name}</h4>
|
|
</div>
|
|
`)
|
|
})
|
|
renderElem(getRef('group_members_list'), html`${groupMembersCards}`)
|
|
},
|
|
blockedList() {
|
|
const blockedListCards = [...messenger.blocked].map(floID => {
|
|
const name = getContactName(floID)
|
|
const initial = name.charAt(0)
|
|
return html`
|
|
<li class="flex align-center interactive blocked-id" data-address=${floID} style=${`--contact-color: var(${contactColor(floID)})`}>
|
|
<div class="initial flex align-center"> ${initial} </div>
|
|
<h4 class="name">${name}</h4>
|
|
<button class="button margin-left-auto unblock">Unblock</button>
|
|
</li>
|
|
`
|
|
})
|
|
renderElem(getRef('blocked_list'), html`${blockedListCards}`)
|
|
},
|
|
contactOnly(floID) {
|
|
const name = getContactName(floID)
|
|
const initial = name.charAt(0)
|
|
return html`
|
|
<button class="contact-list__item interactive" .dataset=${{ address: floID }} style=${`--contact-color: var(${contactColor(floID)})`}>
|
|
<div class="initial flex align-center"> ${initial} </div>
|
|
<h4 class="name">${name}</h4>
|
|
</button>
|
|
`
|
|
},
|
|
multisigMemberCard(pubKey) {
|
|
const floID = floCrypto.getFloID(pubKey)
|
|
const name = getContactName(floID)
|
|
return html`
|
|
<li class="group-member" style=${`--contact-color: var(${contactColor(floID)})`}>
|
|
<div class="initial flex align-center">
|
|
${name[0].toUpperCase()}
|
|
</div>
|
|
${getContactName(floID) !== floID ?
|
|
html`<h4 class="name wrap-around">${name}</h4>` :
|
|
html`<sm-copy value=${floID} clip-text></sm-copy>`
|
|
}
|
|
</li>`
|
|
},
|
|
multisigAddressCard(address, details, label, mode = 'btc') {
|
|
const floMultisig = floCrypto.toMultisigFloID(address)
|
|
return html.for(getRef('select_multisig_list'), mode === 'btc' ? address : floMultisig)`
|
|
<li class="grid gap-0-5 align-center multisig-option" data-address=${address}>
|
|
<div class="flex align-center space-between gap-0-5">
|
|
<text-field class="multisig-option__label" placeholder="Label" value=${label || 'Add a label'}></text-field>
|
|
<button onclick=${showMultisigDetails}>
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>
|
|
</button>
|
|
</div>
|
|
<sm-copy clip-text value=${mode !== 'btc' ? floMultisig : address}></sm-copy>
|
|
<div class="flex align-center space-between gap-1">
|
|
<div class="multisig-option__balance flex align-center gap-0-5">
|
|
<button class="button button--small" onclick=${checkBalance}>Check balance</button>
|
|
</div>
|
|
<button class="button button--small button--colored" onclick=${startMultisigProcess}>Init transaction</button>
|
|
</div>
|
|
</li>
|
|
`
|
|
},
|
|
multisigAddresses() {
|
|
const multisigMode = getRef('multisig_mode_selector').value || 'btc'
|
|
return new Promise((resolve, reject) => {
|
|
Promise.all([messenger.multisig.listAddress(), compactIDB.readAllData('multisigLabels')])
|
|
.then(([addresses, labels]) => {
|
|
renderElem(getRef('select_multisig_list'), html``)
|
|
const list = Object.keys(addresses).map(address => render.multisigAddressCard(address, addresses[address], labels[address], multisigMode)) || []
|
|
renderElem(getRef('select_multisig_list'), html`${list}`)
|
|
resolve()
|
|
}).catch(err => {
|
|
notify(err, 'error')
|
|
reject(err)
|
|
})
|
|
})
|
|
},
|
|
notification(id, details) {
|
|
let { floID, message, time, type } = details
|
|
if (message === '')
|
|
message = `${getContactName(floID)} wants to connect with you`
|
|
return html`
|
|
<li class="notification grid align-center" .dataset=${{ id }}>
|
|
<div class="flex align-center space-between gap-0-5">
|
|
<h4>Connection request</h4>
|
|
<time class="notification__time">${getFormattedTime(time, 'relative')}</time>
|
|
</div>
|
|
<p class="notification__message wrap-around">${message}</p>
|
|
<div class="flex align-center gap-0-3 margin-left-auto">
|
|
<button class="button button--small accept">Accept</button>
|
|
</div>
|
|
</li>
|
|
`
|
|
},
|
|
async notifications() {
|
|
try {
|
|
const notifications = await messenger.list_request_received({ completed: false })
|
|
let receivedRequests = []
|
|
for (const key in notifications) {
|
|
receivedRequests.unshift(render.notification(key, notifications[key]))
|
|
}
|
|
renderElem(getRef('notifications_list'), html`${receivedRequests}`)
|
|
} catch (err) {
|
|
notify(err, 'error')
|
|
}
|
|
},
|
|
profile() {
|
|
return html`
|
|
<div class="grid gap-1-5">
|
|
<div class="grid gap-0-5">
|
|
<h4>
|
|
BTC integrated with FLO
|
|
</h4>
|
|
<p>
|
|
You can use your FLO private key to perform transactions on the BTC network within our
|
|
app
|
|
ecosystem. The private key is the same for both.
|
|
</p>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<b>My FLO address</b>
|
|
<sm-copy class="user-flo-id" clip-text value=${floGlobals.myFloID}></sm-copy>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<b>My Bitcoin address</b>
|
|
<sm-copy class="user-btc-id" clip-text value=${floGlobals.myBtcID}></sm-copy>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<b>My Ethereum address</b>
|
|
<sm-copy class="user-eth-id" clip-text value=${floGlobals.myEthID}></sm-copy>
|
|
</div>
|
|
<button class="button button--danger justify-self-start" onclick="signOut()">Sign out</button>
|
|
</div>
|
|
<div class="grid gap-1">
|
|
<div class="grid gap-0-5">
|
|
<h4>Secure private key</h4>
|
|
<p>
|
|
You can set a password to secure your private key and use the password instead of private key. This is applied to this browser only.
|
|
</p>
|
|
</div>
|
|
<button id="secure_pwd_button" class=${`button button--primary justify-self-start secure-priv-key ${floGlobals.isPrivKeySecured ? 'hidden' : ''}`} onclick="openPopup('secure_pwd_popup')">Set password</button>
|
|
</div>
|
|
<div class="grid gap-1">
|
|
<h4>Clear data</h4>
|
|
<p><strong></strong>This can't be undone.</strong> Make sure you have stored the PRIVATE KEY and
|
|
backed up the data.</p>
|
|
<button id="clear_data" class="button justify-self-start" onclick=${clearUserData}>
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24" /> </g> <g> <g> <path d="M16,11h-1V3c0-1.1-0.9-2-2-2h-2C9.9,1,9,1.9,9,3v8H8c-2.76,0-5,2.24-5,5v7h18v-7C21,13.24,18.76,11,16,11z M11,3h2v8h-2V3 z M19,21h-2v-3c0-0.55-0.45-1-1-1s-1,0.45-1,1v3h-2v-3c0-0.55-0.45-1-1-1s-1,0.45-1,1v3H9v-3c0-0.55-0.45-1-1-1s-1,0.45-1,1v3H5 v-5c0-1.65,1.35-3,3-3h8c1.65,0,3,1.35,3,3V21z" /> </g> </g> </svg>
|
|
Clear Data
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function getLastMessage(floID) {
|
|
return new Promise((resolve, reject) => {
|
|
let chatGetter
|
|
let type
|
|
if (messenger.chats[floID]) {
|
|
type = 'chat'
|
|
chatGetter = getMergedChat
|
|
} else {
|
|
if (messenger.groups[floID])
|
|
type = 'group'
|
|
if (messenger.pipeline[floID])
|
|
type = 'pipeline'
|
|
chatGetter = messenger.getChat
|
|
}
|
|
chatGetter(floID).then(chat => {
|
|
let lastMessage = Object.values(chat).reverse().find(({ message }) => message) || { message: '', time: 0 }
|
|
let { message, time, sender, category } = lastMessage
|
|
if (type === 'group' && time === 0)
|
|
lastMessage.time = messenger.groups[floID].created
|
|
const amISender = type === 'chat' && category === 'sent' || type === 'group' && sender === floDapps.user.id
|
|
lastMessage.lastMessage = ''
|
|
if (messenger.blocked.has(floID)) {
|
|
lastMessage.lastText = 'Blocked conversation'
|
|
} else {
|
|
if (amISender) {
|
|
lastMessage.lastText = `You: ${lastMessage.message}`
|
|
} else {
|
|
if (type === 'group') {
|
|
if (sender)
|
|
lastMessage.lastText = `${getContactName(sender)}: ${message}`
|
|
else
|
|
lastMessage.lastText = 'Group created'
|
|
} else {
|
|
lastMessage.lastText = message
|
|
}
|
|
}
|
|
}
|
|
resolve(lastMessage)
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
const hasURL = text => /[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/.test(text)
|
|
|
|
const isEmoji = (txt) => {
|
|
const rx = /([\uD800-\uDBFF][\uDC00-\uDFFF](?:[\u200D\uFE0F][\uD800-\uDBFF][\uDC00-\uDFFF]){2,}|\uD83D\uDC69(?:\u200D(?:(?:\uD83D\uDC69\u200D)?\uD83D\uDC67|(?:\uD83D\uDC69\u200D)?\uD83D\uDC66)|\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC69\u200D(?:\uD83D\uDC69\u200D)?\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D(?:\uD83D\uDC69\u200D)?\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]\uFE0F|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92])|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC6F\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3C-\uDD3E\uDDD6-\uDDDF])\u200D[\u2640\u2642]\uFE0F|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF6\uD83C\uDDE6|\uD83C\uDDF4\uD83C\uDDF2|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F\u200D[\u2640\u2642]|(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642])\uFE0F|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2695\u2696\u2708]|\uD83D\uDC69\u200D[\u2695\u2696\u2708]|\uD83D\uDC68(?:(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708]))\uFE0F|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83D\uDC69\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69]))|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|[#\*0-9]\uFE0F\u20E3|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67)\uDB40\uDC7F|\uD83D\uDC68(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:(?:\uD83D[\uDC68\uDC69])\u200D)?\uD83D\uDC66\u200D\uD83D\uDC66|(?:(?:\uD83D[\uDC68\uDC69])\u200D)?\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92])|(?:\uD83C[\uDFFB-\uDFFF])\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270A-\u270D]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC70\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDCAA\uDD74\uDD7A\uDD90\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD36\uDDD1-\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\u200D(?:(?:(?:\uD83D[\uDC68\uDC69])\u200D)?\uD83D\uDC67|(?:(?:\uD83D[\uDC68\uDC69])\u200D)?\uD83D\uDC66)|\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC69\uDC6E\uDC70-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD26\uDD30-\uDD39\uDD3D\uDD3E\uDDD1-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])?|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDEEB\uDEEC\uDEF4-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267B\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])\uFE0F)/;
|
|
const res = txt.split(rx).filter(Boolean)
|
|
const messageBody = document.createDocumentFragment()
|
|
res.forEach(section => {
|
|
if (section.match(rx)) {
|
|
messageBody.append(html.node`<span class="text-emoji">${section}</span>`)
|
|
} else {
|
|
messageBody.append(section)
|
|
}
|
|
})
|
|
return messageBody
|
|
}
|
|
const isEthereumAddress = new Set()
|
|
function validateEthAddress(address) {
|
|
if (isEthereumAddress.has(address)) return true
|
|
if (address.startsWith('0x') || address.length === 40) {
|
|
isEthereumAddress.add(address)
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
const ethPubKeyLookup = new Map()
|
|
function getEthPubKey(ethAddress) {
|
|
if (ethPubKeyLookup.has(ethAddress)) return ethPubKeyLookup.get(ethAddress)
|
|
for (const address in floGlobals.pubKeys) {
|
|
if (floEthereum.ethAddressFromCompressedPublicKey(floGlobals.pubKeys[address]) === ethAddress) {
|
|
ethPubKeyLookup.set(address, floGlobals.pubKeys[address])
|
|
return floGlobals.pubKeys[address]
|
|
}
|
|
}
|
|
}
|
|
function addNotificationBadge(elem, text, { replace = false } = {}) {
|
|
const animOptions = {
|
|
duration: 200,
|
|
fill: 'forwards',
|
|
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
|
|
}
|
|
const target = typeof elem === 'string' ? document.querySelector(elem) : elem
|
|
if (!target.querySelector('.badge')) {
|
|
if (parseInt(text) === 0) return
|
|
const badge = html.node`<span class="badge">${text}</span>`
|
|
target.append(badge)
|
|
badge.animate([
|
|
{
|
|
transform: 'scale(0) translateY(0.5rem)'
|
|
},
|
|
{
|
|
transform: 'scale(1) translateY(0)'
|
|
},
|
|
], animOptions)
|
|
} else {
|
|
const badge = target.querySelector('.badge');
|
|
if (parseInt(text) === 0) return badge.remove()
|
|
const oldValue = parseInt(badge.textContent)
|
|
const newValue = parseInt(text)
|
|
if (oldValue === newValue) return
|
|
badge.textContent = replace ? newValue : oldValue + newValue;
|
|
badge.animate([
|
|
{ transform: 'scale(1)' },
|
|
{ transform: `scale(1.5)` },
|
|
{ transform: 'scale(1)' }
|
|
], animOptions)
|
|
}
|
|
}
|
|
|
|
function removeNotificationBadge(elem) {
|
|
const animOptions = {
|
|
duration: 200,
|
|
fill: 'forwards',
|
|
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
|
|
}
|
|
const target = typeof elem === 'string' ? document.querySelector(elem) : elem
|
|
if (target.querySelector('.badge')) {
|
|
const badge = target.querySelector('.badge')
|
|
badge.animate([
|
|
{
|
|
transform: 'scale(1) translateY(0)'
|
|
},
|
|
{
|
|
transform: 'scale(0) translateY(0.5rem)'
|
|
},
|
|
], animOptions).onfinish = () => {
|
|
badge.remove()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function renderDirectUI(data) {
|
|
if (Object.keys(data.messages).length && appState.lastPage !== 'chat_page') {
|
|
document.title = `New message(s)`
|
|
addNotificationBadge('#chat_page_button', Object.keys(data.messages).length)
|
|
}
|
|
if (Object.keys(data.mails).length && appState.lastPage !== 'mail_page') {
|
|
document.title = `New mail(s)`
|
|
addNotificationBadge('#mail_page_button', Object.keys(data.mails).length)
|
|
}
|
|
messenger.list_request_received({ completed: false }).then(requests => addNotificationBadge('#notification_panel_button', Object.keys(requests).length, { replace: true }))
|
|
updateMessageUI(data.messages)
|
|
renderMailList(data.mails, true)
|
|
}
|
|
// merge sorted arrays of objects by a key
|
|
function mergeSortedArrays(arr1 = [], arr2 = [], key) {
|
|
const merged = []
|
|
let i = 0, j = 0
|
|
while (i < arr1.length && j < arr2.length) {
|
|
if (arr1[i][key] < arr2[j][key]) {
|
|
merged.push(arr1[i])
|
|
i++
|
|
} else {
|
|
merged.push(arr2[j])
|
|
j++
|
|
}
|
|
}
|
|
while (i < arr1.length) {
|
|
merged.push(arr1[i])
|
|
i++
|
|
}
|
|
while (j < arr2.length) {
|
|
merged.push(arr2[j])
|
|
j++
|
|
}
|
|
return merged
|
|
}
|
|
const memoizedTxHex = {}
|
|
function getTxHex(chatID) {
|
|
return new Promise((resolve, reject) => {
|
|
if (memoizedTxHex[chatID]) {
|
|
resolve(memoizedTxHex[chatID])
|
|
}
|
|
let floChatID = floCrypto.toFloID(chatID);
|
|
let btcChatID = btcOperator.convert.legacy2bech(floChatID);
|
|
Promise.all([messenger.getChat(floChatID), messenger.getChat(btcChatID)])
|
|
.then(async ([floChat, btcChat]) => {
|
|
const transaction = mergeSortedArrays(Object.values(floChat), Object.values(btcChat), 'time')
|
|
.reverse()
|
|
.find(message => message.tx_hex)
|
|
if (transaction) {
|
|
memoizedTxHex[chatID] = transaction.tx_hex
|
|
resolve(transaction.tx_hex)
|
|
} else {
|
|
reject('No transaction found')
|
|
}
|
|
}).catch(error => {
|
|
console.error(error)
|
|
reject(error)
|
|
})
|
|
})
|
|
}
|
|
|
|
function renderGroupUI(data) {
|
|
if (Object.keys(data.messages).length && appState.lastPage !== 'chat_page') {
|
|
document.title = `New message(s)`
|
|
addNotificationBadge('#chat_page_button', Object.keys(data.messages).length)
|
|
}
|
|
updateMessageUI(data.messages)
|
|
}
|
|
|
|
function renderPipelineUI(model, data) {
|
|
if (Object.keys(data.messages).length && appState.lastPage !== 'chat_page') {
|
|
document.title = `New message(s)`
|
|
addNotificationBadge('#chat_page_button', Object.keys(data.messages).length)
|
|
}
|
|
updateMessageUI(data.messages)
|
|
}
|
|
|
|
async function updateMessageUI(messagesData, sentByMe = false) {
|
|
const animOptions = {
|
|
duration: 300,
|
|
easing: 'ease',
|
|
fill: 'forwards'
|
|
}
|
|
for (let messageId in messagesData) {
|
|
const { category, floID, time, message, sender, groupID, admin, name, pipeID, unconfirmed, type } = messagesData[messageId]
|
|
const chatAddress = floID || groupID || pipeID
|
|
// code to run if a chat is opened
|
|
if (activeChat && floCrypto.isSameAddr(activeChat.address, chatAddress) || floCrypto.isSameAddr(floCrypto.getFloID(getEthPubKey(activeChat.address)), chatAddress)) {
|
|
if (sentByMe || type === 'TRANSACTION' || !sender || !floCrypto.isSameAddr(sender, floDapps.user.id)) {
|
|
const messageBody = render.messageBubble(messagesData[messageId]);
|
|
getRef('messages_container').append(messageBody);
|
|
}
|
|
if (!chatScrollInfo['isScrolledUp']) {
|
|
scrollToBottom()
|
|
}
|
|
// remove encryption badge if it exists
|
|
if (!groupID && (floDapps.user.get_pubKey(activeChat.address) || getEthPubKey(activeChat.address)) && floID !== floDapps.user.id) {
|
|
if (getRef('warn_no_encryption')) {
|
|
getRef('warn_no_encryption').after(
|
|
html.node`
|
|
<strong class="event-card flex align-center">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g fill="none"><path d="M0 0h24v24H0V0z"/><path d="M0 0h24v24H0V0z" opacity=".87"/></g><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>
|
|
Conversation is encrypted
|
|
</strong>
|
|
`
|
|
)
|
|
getRef('warn_no_encryption').remove()
|
|
}
|
|
}
|
|
}
|
|
let chatCard = getChatCard(chatAddress)
|
|
if (chatCard) {
|
|
if (admin) {
|
|
if (name)
|
|
chatCard.querySelector('.name').textContent = name
|
|
}
|
|
// move chat card to top if it is not already there
|
|
const topChatCard = getRef('chats_list').children[0]
|
|
if (!floCrypto.isSameAddr(chatAddress, topChatCard.dataset.address) && !floCrypto.isSameAddr(floCrypto.getFloID(getEthPubKey(chatAddress)), topChatCard.dataset.address)) {
|
|
const cloneContact = chatCard.cloneNode(true)
|
|
chatCard.remove()
|
|
getRef('chats_list').prepend(cloneContact)
|
|
await cloneContact.animate([
|
|
{
|
|
transform: 'scale(0.8)',
|
|
opacity: 0
|
|
},
|
|
{
|
|
transform: 'none',
|
|
opacity: 1
|
|
},
|
|
], animOptions).finished
|
|
getRef('chats_list').scroll({ top: 0, behavior: 'smooth' })
|
|
}
|
|
if (type && type === 'BROADCAST') {
|
|
chatCard.querySelector('.tag').textContent += ' - Completed'
|
|
chatCard.classList.add('collapsed')
|
|
}
|
|
} else {
|
|
const chatType = floID ? 'chat' : groupID ? 'group' : 'pipeline'
|
|
if (floID) {
|
|
if (!(floID in messenger.chats) || !(floCrypto.toFloID(floID) in messenger.chats)) {
|
|
await messenger.addChat(floID).catch(err => console.error(err))
|
|
}
|
|
}
|
|
getRef('chats_list').prepend(html.node`${render.contactCard(chatAddress, { type: chatType, prepend: true, markUnread: true, ref: getRef('chats_list') })}`)
|
|
}
|
|
chatCard = getRef('chats_list').firstElementChild
|
|
if (sentByMe) {
|
|
chatCard.classList.add('active')
|
|
}
|
|
let finalMessage
|
|
if (messenger.groups && messenger.groups[groupID]) {
|
|
finalMessage = `${getContactName(sender)}: ${message}`
|
|
}
|
|
else
|
|
finalMessage = message
|
|
if (chatCard.querySelector('.last-message')) {
|
|
chatCard.querySelector('.last-message').textContent = finalMessage
|
|
chatCard.classList.remove('collapsed')
|
|
}
|
|
if (chatCard.querySelector('.time'))
|
|
chatCard.querySelector('.time').textContent = getFormattedTime(time, 'relative')
|
|
|
|
if (floCrypto.isSameAddr(activeChat.address, chatAddress) || floCrypto.isSameAddr(floCrypto.getFloID(getEthPubKey(activeChat.address)), chatAddress)) {
|
|
if (chatScrollInfo.isScrolledUp)
|
|
getRef('scroll_to_bottom').classList.add('new-message')
|
|
else {
|
|
if (document.hasFocus()) {
|
|
messenger.removeMark(chatAddress, 'unread')
|
|
setTimeout(() => {
|
|
document.title = 'RanchiMall Messenger'
|
|
getChatCard(chatAddress).classList.remove('unread')
|
|
}, 1000);
|
|
}
|
|
}
|
|
} else {
|
|
getChatCard(chatAddress).classList.add('unread')
|
|
}
|
|
}
|
|
|
|
}
|
|
getRef('feature_mode').addEventListener('change', e => {
|
|
const sections = ['dm', 'multisig', 'requests'];
|
|
const lastIndex = [...getRef('chat_sections').children].findIndex(el => !el.classList.contains('hidden'))
|
|
const currentIndex = sections.indexOf(e.target.value)
|
|
const entry = lastIndex < currentIndex ? slideInLeft : slideInRight
|
|
const exit = lastIndex < currentIndex ? slideOutLeft : slideOutRight
|
|
showChildElement(getRef('chat_sections'), currentIndex, { entry, exit })
|
|
switch (e.target.value) {
|
|
case 'dm':
|
|
break;
|
|
case 'multisig':
|
|
render.multisigAddresses()
|
|
break;
|
|
case 'requests':
|
|
render.notifications()
|
|
break;
|
|
}
|
|
})
|
|
getRef('search_chats').addEventListener('keyup', e => {
|
|
if (e.code === 'ArrowDown') {
|
|
for (child of getRef('chats_list').children) {
|
|
if (!child.classList.contains('hidden')) {
|
|
child.focus()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if (e.code === 'Enter' && getRef('contacts_container').firstElementChild) {
|
|
for (child of getRef('contacts_container').children) {
|
|
if (!child.classList.contains('hidden')) {
|
|
child.click()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
})
|
|
getRef('send_mail_to').addEventListener('keyup', e => {
|
|
if (e.code === 'ArrowDown' && getRef('mail_contact_list').firstElementChild) {
|
|
getRef('mail_contact_list').firstElementChild.focus()
|
|
}
|
|
if (e.code === 'Enter' && getRef('mail_contact_list').firstElementChild) {
|
|
getRef('mail_contact_list').firstElementChild.click()
|
|
|
|
}
|
|
})
|
|
getRef('auto_complete_contact').addEventListener('keyup', e => {
|
|
if (e.target.closest('.contact-list__item')) {
|
|
if (e.code === 'ArrowDown' || e.code === 'ArrowRight') {
|
|
if (document.activeElement.nextElementSibling)
|
|
document.activeElement.nextElementSibling.focus()
|
|
else
|
|
getRef('mail_contact_list').firstElementChild.focus()
|
|
}
|
|
if (e.code === 'ArrowUp' || e.code === 'ArrowLeft') {
|
|
if (document.activeElement.previousElementSibling)
|
|
document.activeElement.previousElementSibling.focus()
|
|
else
|
|
getRef('mail_contact_list').lastElementChild.focus()
|
|
}
|
|
if (e.code === 'Enter' || e.code === 'Space') {
|
|
getRef('send_mail_to').value = document.activeElement.dataset.address
|
|
getRef('mail_contact_list').classList.add('hidden')
|
|
}
|
|
}
|
|
})
|
|
getRef('send_mail_to').addEventListener('input', function () {
|
|
getRef('mail_contact_list').classList.remove('hidden')
|
|
if (this.value.trim !== '') {
|
|
[...getRef('mail_contact_list').children].forEach(child => {
|
|
if (getContactName(child.dataset.address).includes(this.value.trim())) {
|
|
child.classList.remove('hidden')
|
|
} else {
|
|
child.classList.add('hidden')
|
|
}
|
|
})
|
|
}
|
|
})
|
|
getRef('compose_mail_popup').addEventListener('click', e => {
|
|
if (e.target.closest('#send_mail_to') || e.target.closest('#mail_contact_list')) {
|
|
getRef('mail_contact_list').classList.remove('hidden')
|
|
} else {
|
|
getRef('mail_contact_list').classList.add('hidden')
|
|
}
|
|
})
|
|
|
|
const chatScrollInfo = {};
|
|
getRef('messages_container').addEventListener('scroll', debounce((e) => {
|
|
if ((e.target.scrollHeight > e.target.clientHeight) && (e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop >= 100)) {
|
|
chatScrollInfo['isScrolledUp'] = true
|
|
getRef('scroll_to_bottom').classList.add('no-transformations')
|
|
} else {
|
|
chatScrollInfo['isScrolledUp'] = false
|
|
getRef('scroll_to_bottom').classList.remove('no-transformations', 'new-message')
|
|
}
|
|
}, 100), { passive: true })
|
|
|
|
getRef('search_chats').addEventListener('input', function (e) {
|
|
const contacts = getRef('chats_list').querySelectorAll('.contact')
|
|
contacts.forEach(child => {
|
|
if (`${getContactName(child.dataset.address)}${child.dataset.address}`.toLowerCase().includes(this.value.toLowerCase())) {
|
|
child.classList.remove('hidden')
|
|
} else {
|
|
child.classList.add('hidden')
|
|
}
|
|
})
|
|
})
|
|
|
|
getRef('search_contacts').addEventListener('input', function () {
|
|
const contacts = {}
|
|
for (contact in floGlobals.contacts) {
|
|
if (contact.toLowerCase().includes(this.value.toLowerCase()) || floGlobals.contacts[contact].toLowerCase().includes(this.value.toLowerCase())) {
|
|
contacts[contact] = floGlobals.contacts[contact]
|
|
}
|
|
}
|
|
renderContactList(contacts)
|
|
})
|
|
document.addEventListener('click', e => {
|
|
// detect click outside emoji panel and emoji button
|
|
if (isEmojiPickerOpen && (!e.target.closest('#emoji_picker') && !e.target.closest('#emoji_toggle') && !e.target.closest('#type_message'))) {
|
|
toggleEmoji('hide')
|
|
}
|
|
})
|
|
|
|
let holdTimeout
|
|
let holdThreshold = 500
|
|
|
|
getRef('chats_list').addEventListener('touchstart', e => {
|
|
if (e.target.closest('.contact')) {
|
|
holdTimeout = setTimeout(() => {
|
|
let contact = e.target.closest(".contact")
|
|
clickedContact['chatCard'] = contact
|
|
clickedContact.address = contact.dataset.address
|
|
clickedContact['isGroup'] = messenger.groups.hasOwnProperty(clickedContact.address)
|
|
floGlobals.viewingDetailsOfAddress = clickedContact.address
|
|
openPopup('contact_details_popup')
|
|
}, 500)
|
|
getRef('chats_list').addEventListener('touchmove', handleTouchMove)
|
|
}
|
|
})
|
|
|
|
function handleTouchMove(e) {
|
|
if (e.target.closest('.contact')) {
|
|
clearTimeout(holdTimeout)
|
|
}
|
|
}
|
|
|
|
getRef('chats_list').addEventListener('touchend', e => {
|
|
if (e.target.closest('.contact')) {
|
|
clearTimeout(holdTimeout)
|
|
getRef('chats_list').removeEventListener('touchmove', handleTouchMove)
|
|
}
|
|
})
|
|
floGlobals.uiState = {}
|
|
getRef('chat_details_button').addEventListener('click', e => {
|
|
floGlobals.viewingDetailsOfAddress = activeChat.address
|
|
openPopup('contact_details_popup')
|
|
})
|
|
|
|
getRef('mail_contact_list').addEventListener('click', e => {
|
|
if (e.target.closest('.contact-list__item')) {
|
|
getRef('send_mail_to').value = e.target.closest('.contact-list__item').dataset.address
|
|
getRef('mail_contact_list').classList.add('hidden')
|
|
}
|
|
})
|
|
|
|
|
|
let clickedContact = {}
|
|
// process click on chat card
|
|
delegate(getRef('chats_list'), 'click', '.contact', e => {
|
|
clickedContact = {
|
|
chatCard: e.delegateTarget,
|
|
address: e.delegateTarget.dataset.address,
|
|
isGroup: messenger.groups.hasOwnProperty(e.delegateTarget.dataset.address)
|
|
}
|
|
if (clickedContact.address === floGlobals.myFloID) return
|
|
if (e.target.closest(".initial") || e.target.closest(".menu")) {
|
|
floGlobals.viewingDetailsOfAddress = clickedContact.address
|
|
openPopup('contact_details_popup')
|
|
} else {
|
|
location.hash = `#/chat_page/messages?address=${clickedContact.address}`
|
|
}
|
|
})
|
|
delegate(getRef('contacts_container'), 'click', '.contact', e => {
|
|
const floID = e.delegateTarget.dataset.address
|
|
let chatCard = getChatCard(floID)
|
|
if (!chatCard) {
|
|
getRef('chats_list').prepend(html.node`${render.contactCard(floID, { type: 'chat', ref: getRef('chats_list') })}`);
|
|
chatCard = getRef('chats_list').firstElementChild
|
|
}
|
|
chatCard.click()
|
|
closePopup()
|
|
})
|
|
|
|
function transformScroll(event) {
|
|
if (!event.deltaY) {
|
|
return;
|
|
}
|
|
|
|
event.currentTarget.scrollLeft += event.deltaY + event.deltaX;
|
|
event.preventDefault();
|
|
}
|
|
|
|
function openCreationPopup(type) {
|
|
let popupTitle = ''
|
|
switch (type) {
|
|
case 'group':
|
|
popupTitle = 'Create group'
|
|
getRef('skip_members_button').classList.remove('hidden')
|
|
getRef('add_self_check').parentNode.classList.add('hidden')
|
|
break;
|
|
case 'multisig':
|
|
getRef('add_self_check').parentNode.classList.remove('hidden')
|
|
getRef('skip_members_button').classList.add('hidden')
|
|
popupTitle = 'Create multisig address'
|
|
getRef('multisig_creation__warning').classList.add('hide')
|
|
floBlockchainAPI.getBalance(floGlobals.myFloID).then(balance => {
|
|
let warning = `Creation of multisig address consumes FLO, you have ${balance} FLO.`;
|
|
getRef('multisig_creation__warning').classList.add('info--warning')
|
|
if (balance < 0.01) {
|
|
warning = `Creation of multisig address consumes FLO, you don't have enough FLO`;
|
|
getRef('multisig_creation__warning').classList.remove('info--warning')
|
|
getRef('multisig_creation__warning').classList.add('info--error')
|
|
}
|
|
getRef('multisig_creation__warning').textContent = warning
|
|
getRef('multisig_creation__warning').classList.remove('hidden')
|
|
}).catch(err => notify(err, 'error'))
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
getRef('creation_popup__title').textContent = popupTitle
|
|
getRef('creation_popup').dataset.type = type
|
|
openPopup('creation_popup')
|
|
}
|
|
|
|
getRef('selected_contacts_container').addEventListener('wheel', transformScroll);
|
|
|
|
getRef('select_contacts_container').addEventListener('change', e => {
|
|
if (e.target.checked)
|
|
selectContact(e.target.value)
|
|
else
|
|
removeSelectedContact(e.target.value)
|
|
})
|
|
delegate(getRef('select_contacts_container'), 'click', '.request-pubkey', e => {
|
|
const floID = e.delegateTarget.closest('.selectable-contact').dataset.address
|
|
messenger.request_pubKey(floID)
|
|
e.delegateTarget.disabled = true;
|
|
e.delegateTarget.textContent = 'Request sent'
|
|
})
|
|
delegate(getRef('selected_contacts_container'), 'click', '.remove-selected', e => {
|
|
removeSelectedContact(e.target.closest('.contact-preview').dataset.address)
|
|
})
|
|
const selectedMembers = new Set();
|
|
function checkSelectedMembers() {
|
|
if (selectedMembers.size) {
|
|
getRef('skip_members_button').textContent = 'Next'
|
|
if (getRef('creation_popup').dataset.type === 'multisig') {
|
|
getRef('skip_members_button').classList.remove('hidden')
|
|
getRef('min_sign_required').setAttribute('max', selectedMembers.size + 1)
|
|
}
|
|
} else {
|
|
getRef('skip_members_button').textContent = 'Skip'
|
|
if (getRef('creation_popup').dataset.type === 'multisig')
|
|
getRef('skip_members_button').classList.add('hidden')
|
|
}
|
|
}
|
|
function selectContact(floID) {
|
|
if (!selectedMembers.has(floID)) {
|
|
selectedMembers.add(floID)
|
|
const name = getContactName(floID)
|
|
const preview = html.node`<div class="contact-preview" .dataset="${{ address: floID }}">
|
|
<div class="initial flex align-center" style=${`--contact-color: var(${contactColor(floID)})`}>
|
|
${name.charAt(0)}
|
|
</div>
|
|
<h4 class="name">${name}</h4>
|
|
<button class="remove-selected icon-only">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0V0z" fill="none" /> <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" /></svg>
|
|
</button>
|
|
</div>`
|
|
getRef('selected_contacts_container').append(preview)
|
|
preview.scrollIntoView({ behavior: "smooth", inline: "end" });
|
|
preview.animate(
|
|
slideInRight,
|
|
{
|
|
duration: 150,
|
|
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
|
fill: 'forwards'
|
|
}
|
|
)
|
|
checkSelectedMembers()
|
|
}
|
|
}
|
|
|
|
function removeSelectedContact(floID) {
|
|
selectedMembers.delete(floID)
|
|
const relatedContact = getRef('select_contacts_container').querySelector(`[data-address="${floID}"]`)
|
|
const relatedPreview = getRef('selected_contacts_container').querySelector(`[data-address="${floID}"]`)
|
|
relatedPreview.animate(
|
|
slideOutLeft,
|
|
{
|
|
duration: 150,
|
|
easing: 'ease',
|
|
fill: 'forwards'
|
|
}
|
|
).onfinish = () => {
|
|
relatedPreview.remove()
|
|
}
|
|
relatedContact.querySelector('input').checked = false
|
|
checkSelectedMembers()
|
|
}
|
|
|
|
function clearAllMembers() {
|
|
getRef('selected_contacts_container').innerHTML = ''
|
|
selectedMembers.clear()
|
|
checkSelectedMembers()
|
|
}
|
|
getRef('skip_members_button').addEventListener('click', e => {
|
|
if (getRef('creation_popup').dataset.type === 'multisig') {
|
|
showChildElement('creation_process', 2, { entry: slideInLeft, exit: slideOutLeft }).then(() => {
|
|
getRef('multisig_label').focusIn()
|
|
})
|
|
} else {
|
|
showChildElement('creation_process', 1, { entry: slideInLeft, exit: slideOutLeft }).then(() => {
|
|
getRef('group_name_field').focusIn()
|
|
})
|
|
}
|
|
})
|
|
|
|
getRef('min_sign_required').addEventListener('input', e => {
|
|
const { rangeOverflow, rangeUnderflow } = e.target.validity;
|
|
if (rangeUnderflow)
|
|
e.target.setAttribute('error-text', 'At least 2 members are required ')
|
|
if (rangeOverflow)
|
|
e.target.setAttribute('error-text', `Maximum ${selectedMembers.size + 1} allowed`)
|
|
})
|
|
getRef('create_multisig_button').addEventListener('click', () => {
|
|
if (getRef('add_self_check').checked)
|
|
selectedMembers.add(floDapps.user.id)
|
|
const selctedPubKeys = [...selectedMembers].map(id => {
|
|
if (id === floDapps.user.id)
|
|
return floDapps.user.public
|
|
return floDapps.user.get_pubKey(id)
|
|
});
|
|
const minRequired = parseInt(getRef('min_sign_required').value.trim());
|
|
const label = getRef('multisig_label').value.trim();
|
|
buttonLoader('create_multisig_button', true)
|
|
console.log('creating multisig address', selctedPubKeys, minRequired)
|
|
messenger.multisig.createAddress(selctedPubKeys, minRequired).then(multisigAddress => {
|
|
console.log('created multisig address', multisigAddress)
|
|
compactIDB.writeData('multisigLabels', label, multisigAddress).then(() => {
|
|
notify('Created multisig address', 'success');
|
|
closePopup();
|
|
clearAllMembers();
|
|
render.multisigAddresses().then(() => {
|
|
const newlyCreatedAddress = getRef('select_multisig_list').querySelector(`[data-address="${multisigAddress}"]`);
|
|
if (newlyCreatedAddress) {
|
|
newlyCreatedAddress.scrollIntoView({ behavior: "smooth", inline: "center" });
|
|
// highlight the newly created address
|
|
newlyCreatedAddress.classList.add('highlight')
|
|
setTimeout(() => {
|
|
newlyCreatedAddress.classList.remove('highlight')
|
|
}, 2000);
|
|
}
|
|
})
|
|
}).catch(error => notify(error, 'error'))
|
|
}).catch(error => notify(error, 'error'))
|
|
.finally(() => {
|
|
buttonLoader('create_multisig_button', false)
|
|
})
|
|
})
|
|
|
|
getRef('select_multisig_list').addEventListener('change', e => {
|
|
const multisigAddress = e.target.closest('.multisig-option').dataset.address;
|
|
let label = e.target.value.trim();
|
|
if (label === '')
|
|
label = 'Unnamed'
|
|
compactIDB.writeData('multisigLabels', label, multisigAddress).then(() => {
|
|
notify('Updated label', 'success');
|
|
}).catch(error => notify(error, 'error'))
|
|
})
|
|
|
|
|
|
getRef('create_group_button').addEventListener('click', () => {
|
|
const groupName = getRef('group_name_field').value.trim()
|
|
const groupDescription = getRef('group_description_field').value.trim()
|
|
buttonLoader('create_group_button', true)
|
|
messenger.createGroup(groupName, groupDescription)
|
|
.then(groupInfo => {
|
|
getRef('chats_list').prepend(html.node`${render.contactCard(groupInfo.groupID, { type: 'group', ref: getRef('chats_list') })}`);
|
|
getRef('chats_list').children[0].click()
|
|
closePopup()
|
|
notify('Group created', 'success')
|
|
if (selectedMembers.size) {
|
|
messenger.addGroupMembers(groupInfo.groupID, [...selectedMembers])
|
|
.then(res => {
|
|
clearAllMembers()
|
|
})
|
|
.catch(err => console.error(err))
|
|
}
|
|
})
|
|
.catch(err => console.error(err))
|
|
.finally(() => buttonLoader('create_group_button', false))
|
|
})
|
|
|
|
let isEmojiPickerOpen = false
|
|
function toggleEmoji(mode) {
|
|
switch (mode) {
|
|
case 'toggle':
|
|
isEmojiPickerOpen = true
|
|
getRef('emoji_toggle').classList.toggle('active')
|
|
getRef('emoji_picker').classList.toggle('hidden')
|
|
break;
|
|
case 'hide':
|
|
isEmojiPickerOpen = false
|
|
getRef('emoji_toggle').classList.remove('active')
|
|
getRef('emoji_picker').classList.add('hidden')
|
|
break;
|
|
}
|
|
getRef('scroll_to_bottom').setAttribute('style', `bottom: calc(${window.innerHeight - getRef('chat_footer').getBoundingClientRect().top}px - .5rem)`)
|
|
if (!chatScrollInfo.isScrolledUp)
|
|
scrollToBottom()
|
|
}
|
|
|
|
getRef('emoji_picker').addEventListener('emoji-click', e => {
|
|
const clickedEmoji = e.detail.unicode
|
|
getRef('type_message').value += clickedEmoji
|
|
if (!floGlobals.isMobileView) {
|
|
setTimeout(() => {
|
|
getRef('type_message').focusIn()
|
|
}, 0);
|
|
}
|
|
})
|
|
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.pad-top{
|
|
background-color: transparent;
|
|
}
|
|
.emoji-menu{
|
|
border-top: solid rgba(var(--text-color), 0.2) 1px;
|
|
background: rgba(var(--foreground-color), 0.6);
|
|
}
|
|
@media (hover: hover){
|
|
::-webkit-scrollbar{
|
|
width: 0.5rem;
|
|
height: 0.5rem;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb{
|
|
background: rgba(var(--text-color), 0.3);
|
|
border-radius: 1rem;
|
|
&:hover{
|
|
background: rgba(var(--text-color), 0.5);
|
|
}
|
|
}
|
|
}`
|
|
|
|
let isEnterSend = true
|
|
if (localStorage.getItem('isEnterSend') === null)
|
|
localStorage.setItem('isEnterSend', 'true')
|
|
|
|
if (localStorage.isEnterSend === 'true') {
|
|
isEnterSend = true
|
|
getRef('is_enter_send_toggle').checked = true;
|
|
} else {
|
|
isEnterSend = false
|
|
getRef('is_enter_send_toggle').checked = false;
|
|
}
|
|
|
|
getRef('is_enter_send_toggle').addEventListener('change', function () {
|
|
if (this.checked) {
|
|
isEnterSend = true
|
|
localStorage.setItem("isEnterSend", 'true');
|
|
} else {
|
|
isEnterSend = false
|
|
localStorage.setItem("isEnterSend", 'false');
|
|
}
|
|
})
|
|
|
|
getRef("mail_type_selector").addEventListener('change', function (e) {
|
|
removeNotificationBadge(getRef("mail_type_selector").querySelector(`[value="${e.target.value}"]`))
|
|
location.hash = `#/mail_page/${e.target.value}`
|
|
})
|
|
getRef("mail_sections").addEventListener('click', function (e) {
|
|
if (e.target.closest(".mail-card")) {
|
|
e.target.closest(".mail-card").classList.remove('unread')
|
|
viewMail(e.target.closest(".mail-card").dataset.ref);
|
|
getRef("mail_sections").querySelectorAll(".mail-card").forEach(card => {
|
|
card.classList.remove('active')
|
|
})
|
|
e.target.closest(".mail-card").classList.add('active')
|
|
activeMail = e.target.closest(".mail-card")
|
|
}
|
|
})
|
|
|
|
getRef("prev_mail").addEventListener('click', function (e) {
|
|
viewMail(this.dataset["value"], false);
|
|
})
|
|
|
|
getRef('send_message_button').addEventListener('click', sendMessage)
|
|
|
|
getRef('type_message').addEventListener('input', e => {
|
|
getRef('send_message_button').disabled = getRef('type_message').value.trim() === ''
|
|
})
|
|
getRef('type_message').addEventListener('keydown', e => {
|
|
if (getRef('type_message').value.trim() === '' && e.code === "Enter") {
|
|
e.preventDefault()
|
|
return
|
|
}
|
|
else {
|
|
if (e.code === "Enter" && e.shiftKey) {
|
|
e.preventDefault()
|
|
getRef('type_message').value += '\r\n';
|
|
} else if (!e.shiftKey && e.code === "Enter" && isEnterSend) {
|
|
e.preventDefault()
|
|
sendMessage()
|
|
}
|
|
}
|
|
})
|
|
|
|
function clearUserData() {
|
|
getConfirmation('Clear Data', { message: 'Are you sure you want to clear all data?', confirmText: 'Yes', cancelText: 'No' }).then(confirmed => {
|
|
if (confirmed) {
|
|
messenger.clearUserData().then(result => {
|
|
notify("Successfully Cleared local data", 'success')
|
|
onLoadStartUp()
|
|
}).catch(error => notify(error, "error"))
|
|
}
|
|
})
|
|
}
|
|
|
|
getRef('add_contact_button').addEventListener("click", addContact)
|
|
|
|
getRef('show_reply_popup').addEventListener("click", () => {
|
|
openPopup('reply_mail_popup')
|
|
})
|
|
|
|
getRef('reply_mail_button').addEventListener("click", replyMail)
|
|
|
|
getRef("backup_data").addEventListener("click", function (e) {
|
|
renderElem(getRef("backup_info"), html`Generating the backup file! Please wait...`)
|
|
messenger.backupData().then(blob => {
|
|
renderElem(getRef("backup_info"), html`
|
|
Backup file generated! If the download didn't start automatically,
|
|
<a href="${URL.createObjectURL(blob)}" download="${`BackupFor_${floDapps.user.id}_${Date.now()}.json`}">click here</a>
|
|
`)
|
|
getRef("backup_info").querySelector('a').click();
|
|
}).catch(error => {
|
|
renderElem(getRef("backup_info"), html`Unable to generate backup file! Try again later...`)
|
|
getRef("backup_info").classList.add("error")
|
|
notify("Backup data Unsuccessful!", "error");
|
|
console.error(error)
|
|
})
|
|
})
|
|
|
|
getRef("restore_data").addEventListener("change", async function (e) {
|
|
let file = this.files[0]
|
|
notify(`Retrieving backup data! Please wait.`);
|
|
if (!file) {
|
|
notify(`No files selected!`);
|
|
return;
|
|
}
|
|
messenger.parseBackup(file).then(async data => {
|
|
getConfirmation('Restore Data?', { message: `Found: ${Object.keys(data.contacts).length} Contacts,\n ${Object.keys(data.messages).length} Messages, ${Object.keys(data.mails).length} Mails.`, confirmText: 'Restore', cancelText: 'Cancel' }).then(confirmed => {
|
|
if (confirmed) {
|
|
notify(`Restoring data! Please wait.`);
|
|
messenger.restoreData(data).then(result => {
|
|
notify("Data restore completed successful! Initiating reload, Please wait", 'success')
|
|
setTimeout(() => {
|
|
location.reload()
|
|
}, 1000)
|
|
}).catch(error => {
|
|
notify("Failed to restore data! Try again later", "error", error);
|
|
});
|
|
}
|
|
})
|
|
}).catch(error => {
|
|
notify("Retrive data Unsuccessful!", "error", error);
|
|
})
|
|
})
|
|
async function sendMessage() {
|
|
if (!floGlobals.isMobileView)
|
|
getRef('type_message').focusIn()
|
|
let receiver = activeChat.address
|
|
let message = getRef('type_message').value.trim();
|
|
getRef('type_message').value = ''
|
|
if (message === '') return
|
|
let time = Date.now()
|
|
let msgObj = { message, time, unconfirmed: true }
|
|
switch (activeChat.type) {
|
|
case 'group':
|
|
msgObj['groupID'] = receiver
|
|
msgObj['sender'] = floDapps.user.id
|
|
break;
|
|
case 'pipeline':
|
|
msgObj['pipeID'] = receiver
|
|
msgObj['sender'] = floDapps.user.id
|
|
break;
|
|
case 'plain':
|
|
msgObj['floID'] = receiver
|
|
msgObj['category'] = 'sent'
|
|
break;
|
|
}
|
|
updateMessageUI({ msgObj }, true)
|
|
try {
|
|
switch (activeChat.type) {
|
|
case 'group':
|
|
await messenger.sendGroupMessage(message, receiver)
|
|
break;
|
|
case 'pipeline':
|
|
await messenger.sendPipelineMessage(message, receiver)
|
|
break;
|
|
case 'plain':
|
|
await messenger.sendMessage(message, receiver)
|
|
break;
|
|
}
|
|
if (getRef('messages_container').querySelector(`.unconfirmed`))
|
|
getRef('messages_container').querySelector(`.unconfirmed`).classList.remove('unconfirmed')
|
|
} catch (err) {
|
|
notify(err, "error")
|
|
}
|
|
}
|
|
|
|
function addContact() {
|
|
let addressToSave = getRef('add_contact_floID').value.trim();
|
|
let name = getRef('add_contact_name').value.trim();
|
|
if (floCrypto.isSameAddr(addressToSave, floDapps.user.id) || floCrypto.myEthID === addressToSave) {
|
|
notify(`you can't add your own FLO/BTC/ETH address as contact`, 'error')
|
|
return
|
|
}
|
|
if (floGlobals.contacts.hasOwnProperty(addressToSave)) {
|
|
notify(`Contact already saved`, 'error')
|
|
return
|
|
}
|
|
// check whether an equivalent BTC/FLO address is already saved
|
|
const addrInFlo = floCrypto.toFloID(addressToSave);
|
|
const addrInBtc = btcOperator.convert.legacy2bech(addrInFlo);
|
|
const addrInEth = floGlobals.pubKeys[addressToSave] ? floEthereum.ethAddressFromCompressedPublicKey(floGlobals.pubKeys[addressToSave]) : null;
|
|
if (floGlobals.contacts.hasOwnProperty(addrInFlo) || floGlobals.contacts.hasOwnProperty(addrInBtc) || floGlobals.contacts.hasOwnProperty(addrInEth)) {
|
|
notify(`Equivalent Address is already saved as ${getContactName(addrInFlo)}`, 'error');
|
|
return;
|
|
}
|
|
messenger.storeContact(addressToSave, name).then(result => {
|
|
closePopup()
|
|
notify(`Added Contact: ${addressToSave}`)
|
|
const chatCard = getChatCard(addressToSave)
|
|
if (chatCard)
|
|
chatCard.querySelector('.name').textContent = name
|
|
if (popupStack.peek().popup.id === 'contact_details_popup') {
|
|
getRef('contact_name').value = name
|
|
getRef('add_as_contact_option').remove()
|
|
}
|
|
if (popupStack.items.find(elem => elem.popup.id === 'creation_popup')) {
|
|
renderCreationList()
|
|
}
|
|
if (popupStack.items.find(elem => elem.popup.id === 'new_message_popup')) {
|
|
renderContactList()
|
|
}
|
|
if (floCrypto.isSameAddr(activeChat.address, addressToSave))
|
|
updateChatHeaderName(name)
|
|
}).catch(error => notify(error, "error"));
|
|
}
|
|
|
|
function renderContactList(contactList = floGlobals.contacts) {
|
|
const contacts = Object.keys(contactList)
|
|
.sort((a, b) => getContactName(a).localeCompare(getContactName(b)))
|
|
.map(floID => {
|
|
const isSelected = selectedMembers.has(floID)
|
|
return render.contactCard(floID, { type: 'contact', isSelected, ref: getRef('contacts_container') })
|
|
})
|
|
renderElem(getRef('contacts_container'), html`${contacts}`)
|
|
}
|
|
async function renderCreationList() {
|
|
const contacts = []
|
|
const sentRequests = await messenger.list_request_sent();
|
|
const skipSendingRequest = new Set();
|
|
for (const key in sentRequests) {
|
|
skipSendingRequest.add(sentRequests[key].floID)
|
|
}
|
|
for (const floID in floGlobals.contacts) {
|
|
if (getAddressType(floID) !== 'plain') continue;
|
|
if (floDapps.user.get_pubKey(floID)) {
|
|
contacts.push(render.selectableContact(floID))
|
|
} else {
|
|
const hasSentRequest = skipSendingRequest.has(floID)
|
|
contacts.push(render.actionableContact(floID, hasSentRequest))
|
|
}
|
|
}
|
|
renderElem(getRef('select_contacts_container'), html`${contacts}`)
|
|
}
|
|
|
|
async function renderChatList(chatOrder = messenger.getChatOrder()) {
|
|
const chatOrderLookup = new Set()
|
|
const mergedChatOrder = []
|
|
// merge equivalent addresses
|
|
chatOrder.forEach(address => {
|
|
if (messenger.groups.hasOwnProperty(address))
|
|
return mergedChatOrder.push(address)
|
|
if (messenger.pipeline.hasOwnProperty(address))
|
|
return mergedChatOrder.push(address)
|
|
let priorityAddress;
|
|
let equivalentFloAddress;
|
|
let equivalentBtcAddress;
|
|
let equivalentEthAddress;
|
|
const addressPubKey = floGlobals.pubKeys[address] || getEthPubKey(address);
|
|
//if address id ethereuem address get equivalent flo address from pubKey if available
|
|
equivalentFloAddress = validateEthAddress(address) ? floCrypto.getFloID(addressPubKey) : floCrypto.toFloID(address); //
|
|
if (equivalentFloAddress)
|
|
equivalentBtcAddress = btcOperator.convert.legacy2bech(equivalentFloAddress);
|
|
equivalentEthAddress = addressPubKey ? floEthereum.ethAddressFromCompressedPublicKey(addressPubKey) : null;
|
|
if (equivalentFloAddress && floGlobals.contacts.hasOwnProperty(equivalentFloAddress)) {
|
|
priorityAddress = equivalentFloAddress;
|
|
} else if (equivalentBtcAddress && floGlobals.contacts.hasOwnProperty(equivalentBtcAddress)) {
|
|
priorityAddress = equivalentBtcAddress;
|
|
} else if (equivalentEthAddress && floGlobals.contacts.hasOwnProperty(equivalentEthAddress)) {
|
|
priorityAddress = equivalentEthAddress;
|
|
} else {
|
|
priorityAddress = address;
|
|
}
|
|
if (!chatOrderLookup.has(equivalentFloAddress) && !chatOrderLookup.has(equivalentBtcAddress) && !chatOrderLookup.has(equivalentEthAddress)) {
|
|
mergedChatOrder.push(priorityAddress)
|
|
chatOrderLookup.add(priorityAddress)
|
|
}
|
|
});
|
|
const chats = mergedChatOrder.map(address => {
|
|
const markUnread = messenger.marked[address] ? messenger.marked[address].includes('unread') : false;
|
|
let type
|
|
if (messenger.chats[address])
|
|
type = 'chat'
|
|
else if (messenger.groups[address])
|
|
type = 'group'
|
|
else if (messenger.pipeline[address])
|
|
type = 'pipeline'
|
|
return render.contactCard(address, { type, markUnread, ref: getRef('chats_list') })
|
|
})
|
|
renderElem(getRef('chats_list'), html`${chats}`)
|
|
}
|
|
|
|
function renderMarked(data) {
|
|
for (let d in data) {
|
|
let element = document.getElementsByName(d)[0]
|
|
if (element)
|
|
data[d].forEach(mark => element.classList.add(mark))
|
|
}
|
|
}
|
|
|
|
function scrollToBottom(smooth = false) {
|
|
if (activeChat.address) {
|
|
messenger.removeMark(activeChat.address, 'unread')
|
|
if (getChatCard(activeChat.address))
|
|
getChatCard(activeChat.address).classList.remove('unread')
|
|
}
|
|
getRef('scroll_to_bottom').classList.remove('new-message')
|
|
getRef('messages_container').scrollTo({ top: getRef('messages_container').scrollHeight, behavior: smooth ? 'smooth' : undefined })
|
|
}
|
|
|
|
function broadcastTx(pipeID, e) {
|
|
return new Promise(async (resolve, reject) => {
|
|
const button = e.target.closest('button')
|
|
try {
|
|
buttonLoader(button, true)
|
|
const tx_hex_signed = floGlobals.pipelineTxHex || getTxHex(pipeID)
|
|
if (!tx_hex_signed) reject('No transaction found')
|
|
const pipeline = messenger.pipeline[pipeID]
|
|
const txid = await floBlockchainAPI.broadcastTx(tx_hex_signed)
|
|
console.debug(txid);
|
|
const result = await messenger.sendRaw(messenger.encrypt(txid, pipeline.eKey), pipeline.id, "BROADCAST", false)
|
|
renderElem(button.closest('.grid'), html``)
|
|
resolve({
|
|
tx_hex: tx_hex_signed,
|
|
txid: txid
|
|
})
|
|
} catch (error) {
|
|
setTimeout(() => {
|
|
buttonLoader(button, false)
|
|
}, 400);
|
|
reject(error)
|
|
}
|
|
})
|
|
}
|
|
async function initFeeIncrease(parsedTx, currentPipeID, e) {
|
|
if (!floGlobals.pipelineTxID) return notify('No transaction found', 'error')
|
|
const button = e.target.closest('button')
|
|
const { fee } = parsedTx;
|
|
try {
|
|
let newFee = await getPromptInput('Increase transaction fee', `Current fee: ${fee} BTC.\n *This will create a new group and disable current one.`, {
|
|
confirmText: 'Increase',
|
|
placeholder: 'New BTC transaction fee',
|
|
attributes: {
|
|
type: 'number',
|
|
min: fee,
|
|
step: '0.00000001',
|
|
'error-text': `New fee should be greater than ${fee} BTC`,
|
|
}
|
|
})
|
|
newFee = parseFloat(newFee)
|
|
if (newFee <= fee)
|
|
return notify('New fee should be greater than current fee', 'error')
|
|
buttonLoader(button, true)
|
|
const newPipeID = await messenger.editFee(floGlobals.pipelineTxID, newFee, await floDapps.user.private)
|
|
highlightNewGroup(newPipeID)
|
|
notify('Created a new multisig group. Please collect required signs again.', 'success')
|
|
} catch (error) {
|
|
notify(error, 'error')
|
|
buttonLoader(button, false)
|
|
}
|
|
}
|
|
|
|
function getMergedChat(address) {
|
|
return new Promise((resolve, reject) => {
|
|
let floChatAddress
|
|
let btcChatAddress
|
|
const promises = []
|
|
floChatAddress = validateEthAddress(address) ? floCrypto.getFloID(floGlobals.pubKeys[address] || getEthPubKey(address)) : floCrypto.toFloID(address);
|
|
if (floChatAddress) {
|
|
btcChatAddress = btcOperator.convert.legacy2bech(floChatAddress);
|
|
promises.push(messenger.getChat(floChatAddress), messenger.getChat(btcChatAddress))
|
|
}
|
|
if (validateEthAddress(address))
|
|
promises.push(messenger.getChat(address))
|
|
Promise.all(promises)
|
|
.then((chats) => {
|
|
// recursively merge chats using mergeSortedArrays
|
|
const mergedChat = chats.reduce((acc, chat) => mergeSortedArrays(acc, Object.values(chat), 'time'), [])
|
|
resolve(mergedChat)
|
|
}).catch(error => {
|
|
reject(error)
|
|
})
|
|
})
|
|
}
|
|
|
|
let chatLazyLoader
|
|
function renderMessages(address) {
|
|
return new Promise(async (resolve, reject) => {
|
|
(getAddressType(address) === 'plain' ? getMergedChat(address) : messenger.getChat(address)).then(chat => {
|
|
chat = Object.values(chat)
|
|
if (chatLazyLoader) {
|
|
chatLazyLoader.update(chat)
|
|
} else {
|
|
chatLazyLoader = new LazyLoader('#messages_container', chat, render.messageBubble, {
|
|
bottomFirst: true,
|
|
batchSize: 20,
|
|
onEnd: () => {
|
|
if (activeChat.type === 'plain') {
|
|
if (floDapps.user.get_pubKey(activeChat.address) || getEthPubKey(activeChat.address)) {
|
|
getRef('messages_container').prepend(html.node`<strong class="event-card flex align-center">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g fill="none"><path d="M0 0h24v24H0V0z"/><path d="M0 0h24v24H0V0z" opacity=".87"/></g><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>
|
|
Conversation is encrypted
|
|
</strong>`)
|
|
} else {
|
|
getRef('messages_container').prepend(html.node`<strong id="warn_no_encryption" class="event-card">Conversation is not encrypted until receiver replies</strong>`)
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
chatLazyLoader.init()
|
|
if (getAddressType(address) === 'pipeline') {
|
|
if (!floGlobals.pipeSigns[address])
|
|
floGlobals.pipeSigns[address] = new Set()
|
|
for (const key in chat) {
|
|
const { type, sender, tx_hex, txid } = chat[key]
|
|
switch (type) {
|
|
case 'TRANSACTION':
|
|
floGlobals.pipeSigns[address].add(sender)
|
|
if (tx_hex)
|
|
floGlobals.pipelineTxHex = tx_hex
|
|
break;
|
|
case 'BROADCAST':
|
|
if (txid) {
|
|
floGlobals.pipelineTxID = txid
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
resolve()
|
|
}).catch(error => {
|
|
reject(error)
|
|
})
|
|
})
|
|
}
|
|
|
|
floGlobals.typedMessages = {}
|
|
floGlobals.pipeSigns = {}
|
|
function viewConversation(floID) {
|
|
return new Promise((resolve, reject) => {
|
|
// clear rendered date cards if any
|
|
renderedDates = {}
|
|
// save typed message from previous chat
|
|
if (getRef('type_message').value.trim() !== '') {
|
|
floGlobals.typedMessages[activeChat.address] = getRef('type_message').value
|
|
} else {
|
|
delete floGlobals.typedMessages[activeChat.address]
|
|
}
|
|
// restore typed message if any
|
|
getRef('type_message').value = floGlobals.typedMessages[floID] || ''
|
|
activeChat.address = floID
|
|
activeChat.type = getAddressType(floID)
|
|
updateChatHeaderName(getContactName(floID));
|
|
const chatCard = getChatCard(floID);
|
|
if (chatCard) {
|
|
getRef("receiver_initial").innerHTML = chatCard.querySelector('.initial').innerHTML;
|
|
} else {
|
|
if (activeChat.type === 'group') {
|
|
getRef("receiver_initial").innerHTML = ` <svg class="icon group-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M13.61,28.09c-1.63,0-4.72-2.35-5.33-3.58a21.65,21.65,0,0,1-1.35-7.32s-.26-6.07,6.68-6.07a6.38,6.38,0,0,1,6.69,6.07A21.65,21.65,0,0,1,19,24.51c-.62,1.23-3.7,3.58-5.34,3.58"/><path d="M50.39,28.09c-1.64,0-4.72-2.35-5.34-3.58a21.9,21.9,0,0,1-1.35-7.32s-.26-6.07,6.69-6.07a6.37,6.37,0,0,1,6.68,6.07,21.65,21.65,0,0,1-1.35,7.32c-.61,1.23-3.7,3.58-5.33,3.58"/><path d="M32,31.74c-2.21,0-6.37-3.17-7.2-4.83A29.3,29.3,0,0,1,23,17s-.35-8.21,9-8.21c8.68,0,9,8.21,9,8.21a29.3,29.3,0,0,1-1.83,9.88c-.82,1.66-5,4.83-7.2,4.83"/><path d="M48.29,38.58c-4.16-1.83-8.57-3.08-10.34-6.4a12,12,0,0,1-6,3.73,12,12,0,0,1-5.95-3.73c-1.77,3.32-6.18,4.57-10.34,6.4-1.7.71-3.11,9.88-1.13,9.88A33.06,33.06,0,0,0,31.23,53h1.54a33.06,33.06,0,0,0,16.65-4.53C51.4,48.46,50,39.29,48.29,38.58Z"/><path d="M14.82,36.57c.76-.33,1.54-.65,2.3-1,2.49-1,4.85-2,6.22-3.44C21.07,31.23,19,30.25,18,28.41a8.83,8.83,0,0,1-4.41,2.76,8.83,8.83,0,0,1-4.4-2.76c-1.31,2.46-4.58,3.38-7.66,4.74-1.26.52-2.3,7.31-.84,7.31a24.55,24.55,0,0,0,10.86,3.31C11.89,40.81,12.86,37.39,14.82,36.57Z"/><path d="M62.45,33.15c-3.08-1.36-6.35-2.28-7.66-4.74a8.83,8.83,0,0,1-4.4,2.76A8.83,8.83,0,0,1,46,28.41c-1,1.84-3,2.82-5.32,3.76,1.37,1.43,3.73,2.41,6.22,3.44.76.31,1.54.63,2.26,1,2,.83,3,4.25,3.29,7.21a24.55,24.55,0,0,0,10.86-3.31C64.75,40.46,63.71,33.67,62.45,33.15Z"/></svg> `
|
|
} else if (activeChat.type === 'pipeline') {
|
|
getRef("receiver_initial").innerHTML = `<svg class="icon group-icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M6,15c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S4.9,15,6,15 M6,13c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4S8.2,13,6,13z M12,5 c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S10.9,5,12,5 M12,3C9.8,3,8,4.8,8,7s1.8,4,4,4s4-1.8,4-4S14.2,3,12,3z M18,15 c1.1,0,2,0.9,2,2s-0.9,2-2,2s-2-0.9-2-2S16.9,15,18,15 M18,13c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4S20.2,13,18,13z"/></g></g></svg>`
|
|
} else {
|
|
getRef("receiver_initial").textContent = getContactName(floID).charAt(0);
|
|
}
|
|
}
|
|
setTimeout(() => {
|
|
getChatCard(floID)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
}, 100);
|
|
getRef("receiver_initial").setAttribute('style', `--contact-color: var(${contactColor(floID)})`)
|
|
messenger.removeMark(floID, "unread");
|
|
if (this.scrollHeight <= this.clientHeight) {
|
|
chatScrollInfo['isScrolledUp'] = false
|
|
getRef('scroll_to_bottom').classList.remove('no-transformations')
|
|
}
|
|
const floIdType = getAddressType(floID)
|
|
if (floIdType === 'pipeline' && messenger.pipeline[floID].disabled) {
|
|
getRef('chat_footer').classList.add('hidden')
|
|
} else {
|
|
getRef('chat_footer').classList.remove('hidden')
|
|
}
|
|
lastSender = ''
|
|
renderMessages(floID).then(async () => {
|
|
if (activeChat.type === 'pipeline') {
|
|
if (!messenger.pipeline[floID].disabled && floGlobals.pipeSigns[floID] && !floGlobals.pipeSigns[floID].has(floDapps.user.id)) {
|
|
getRef('messages_container').append(html.node`
|
|
<div class="grid gap-1 card signing-banner" style="margin: 1.5rem auto;">
|
|
<div class="grid gap-0-5">
|
|
<h4>You haven't signed the transaction</h4>
|
|
<p>Use "Sign now" button to approve the transaction</p>
|
|
</div>
|
|
<div class="multi-state-button">
|
|
<button class="button button--primary cta" onclick="${() => signTransaction(floID)}">Sign now</button>
|
|
</div>
|
|
</div>
|
|
`)
|
|
}
|
|
if (floGlobals.pipelineTxHex) {
|
|
try {
|
|
let parsedTx, currency
|
|
switch (messenger.pipeline[floID].model) {
|
|
case 'flo_multisig':
|
|
currency = 'FLO'
|
|
parsedTx = await floBlockchainAPI.parseTransaction(floGlobals.pipelineTxHex)
|
|
break;
|
|
case 'btc_multisig':
|
|
currency = 'BTC'
|
|
parsedTx = await btcOperator.parseTransaction(floGlobals.pipelineTxHex)
|
|
break;
|
|
}
|
|
const { inputs, outputs, fee, floData } = parsedTx
|
|
const { s: signsDone, r: minSignsRequired, t: totalMembers } = inputs[0]?.signed || {}
|
|
const pendingSigns = minSignsRequired - signsDone || 0;
|
|
if (getRef('transaction_details')) getRef('transaction_details').remove()
|
|
let retrySection = ''
|
|
if (pendingSigns === 0 && !messenger.pipeline[floID].disabled) {
|
|
switch (messenger.pipeline[floID].model) {
|
|
case 'flo_multisig':
|
|
retrySection = html`
|
|
<div class="grid gap-0-5 margin-top-1">
|
|
<strong> The transaction was not completed. Retry the transaction by clicking the button below.</strong>
|
|
<div class="multi-state-button margin-right-auto">
|
|
<button class="button button--primary cta" onclick="${(e) => broadcastTx(floID, e)}">Retry</button>
|
|
</div>
|
|
</div>
|
|
`
|
|
break;
|
|
}
|
|
}
|
|
if (messenger.pipeline[floID].disabled && floGlobals.pipelineTxID) {
|
|
switch (messenger.pipeline[floID].model) {
|
|
case 'btc_multisig':
|
|
const { block, confirmations } = await btcOperator.getTx(floGlobals.pipelineTxID)
|
|
if (!block || confirmations === 0)
|
|
retrySection = html`
|
|
<div class="grid gap-1 margin-top-1">
|
|
<div class="grid">
|
|
<h5>Transaction ID</h5>
|
|
<sm-copy value=${floGlobals.pipelineTxID}>
|
|
<a href=${`https://ranchimall.github.io/btcwallet/#/check_details?query=${floGlobals.pipelineTxID}`} target="_blank">
|
|
${floGlobals.pipelineTxID}
|
|
</a>
|
|
</sm-copy>
|
|
</div>
|
|
<strong> If transaction is taking too long to confirm, increase transaction fee.</strong>
|
|
<div class="multi-state-button margin-right-auto">
|
|
<button class="button button--primary cta" onclick="${(e) => initFeeIncrease(parsedTx, floID, e)}">Increase fee</button>
|
|
</div>
|
|
</div>`
|
|
break;
|
|
}
|
|
}
|
|
getRef('messages_container').prepend(html.node`
|
|
<details id="transaction_details" class="grid gap-1 card">
|
|
<summary>
|
|
${pendingSigns === 0 ? html`
|
|
<h4>Required signatures are done</h4>
|
|
`: html`
|
|
<h4>Transaction is waiting for ${pendingSigns} more signature${pendingSigns > 1 ? 's' : ''}</h4>
|
|
`}
|
|
<svg class="icon down-arrow" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M24 24H0V0h24v24z" fill="none" opacity=".87"/><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z"/></svg>
|
|
</summary>
|
|
<div class="grid margin-bottom-1">
|
|
<h5>Multisig address</h5>
|
|
<sm-copy value=${inputs[0].address} clip-text></sm-copy>
|
|
</div>
|
|
<div class="grid margin-bottom-1">
|
|
<h5>Receiver</h5>
|
|
<sm-copy value=${outputs[0].address} clip-text></sm-copy>
|
|
</div>
|
|
<div class="flex align-center gap-1-5 margin-bottom-1">
|
|
<div class="grid">
|
|
<h5>Amount</h5>
|
|
<p>${outputs[0].value} ${currency}</p>
|
|
</div>
|
|
<div class="grid">
|
|
<h5>Fee</h5>
|
|
<p>${fee} ${currency}</p>
|
|
</div>
|
|
</div>
|
|
${floData ? html`
|
|
<div class="grid">
|
|
<h5>FLO data</h5>
|
|
<p>${floData}</p>
|
|
</div>
|
|
`: ''}
|
|
${retrySection}
|
|
</details>
|
|
`)
|
|
} catch (err) {
|
|
notify(err, 'error')
|
|
}
|
|
}
|
|
}
|
|
resolve()
|
|
})
|
|
})
|
|
}
|
|
|
|
function renderMailList(mails, markUnread = true) {
|
|
const inboxMails = document.createDocumentFragment()
|
|
const sentMails = document.createDocumentFragment()
|
|
let inboxCount = 0, sentCount = 0
|
|
for (let m in mails) {
|
|
let { from, to, prev, ref, subject, time, content } = mails[m]
|
|
if (from === floDapps.user.id) {
|
|
sentMails.prepend(render.mailCard(to, ref, subject, time, content, markUnread))
|
|
if (markUnread) {
|
|
sentCount++
|
|
}
|
|
if (sentMails.querySelector(`[data-ref="${prev}"]`))
|
|
sentMails.querySelector(`[data-ref="${prev}"]`).remove()
|
|
} else if (to.includes(floDapps.user.id)) {
|
|
inboxMails.prepend(render.mailCard(from, ref, subject, time, content, markUnread))
|
|
if (markUnread) {
|
|
inboxCount++
|
|
}
|
|
if (inboxMails.querySelector(`[data-ref="${prev}"]`))
|
|
inboxMails.querySelector(`[data-ref="${prev}"]`).remove()
|
|
}
|
|
}
|
|
if (getRef('mail_type_selector').value === 'inbox' && sentCount) {
|
|
addNotificationBadge(getRef('mail_type_selector').children[1], sentCount)
|
|
} else if (getRef('mail_type_selector').value === 'sent' && inboxCount) {
|
|
addNotificationBadge(getRef('mail_type_selector').children[0], inboxCount)
|
|
}
|
|
getRef('inbox_mail_container').prepend(inboxMails)
|
|
getRef('sent_mail_container').prepend(sentMails)
|
|
}
|
|
|
|
function viewMail(mailRef, newView = true) {
|
|
//stop rerendering if same mail is already open
|
|
if (mailRef === (activeMail ? activeMail.dataset.ref : '')) return
|
|
messenger.getMail(mailRef).then(result => {
|
|
let { from, to, prev, ref, subject, time, content } = result
|
|
if (newView)
|
|
getRef('mail_container').innerHTML = ''
|
|
//append the contents to mail container
|
|
getRef("mail_container").append(render.mail(from, to, subject, time, content));
|
|
getRef("mail_container").lastElementChild.scrollIntoView();
|
|
//add prop for previous mail (if available)
|
|
getRef("prev_mail").dataset["value"] = prev;
|
|
messenger.getMail(prev).then(result => {
|
|
getRef("prev_mail").classList.remove("hidden");
|
|
}).catch(err => {
|
|
getRef("prev_mail").classList.add("hidden");
|
|
});
|
|
if (!prev)
|
|
getRef("prev_mail").classList.add("hidden")
|
|
//set values for reply mail form if new view
|
|
if (newView) {
|
|
getRef('reply_mail_popup').dataset["to"] = (from === floDapps.user.id ? to.join(',') : from)
|
|
getRef('reply_mail_popup').dataset["prev"] = mailRef;
|
|
getRef('subject_of_reply_mail').value = subject.startsWith("Re: ") ? subject : `Re: ${subject}`;
|
|
getRef("show_reply_popup").classList.remove("hidden");
|
|
}
|
|
messenger.removeMark(mailRef, "unread");
|
|
location.hash = `#/mail_page/${getRef("mail_type_selector").value}/mail`
|
|
}).catch(error => notify("Unable to read mail", "error", error))
|
|
}
|
|
|
|
getRef('send_mail_button').addEventListener('click', () => {
|
|
let to = getRef('send_mail_to').value.split(",");
|
|
let subject = getRef('subject_of_mail').value;
|
|
let content = getRef('mail_content').value
|
|
let recipients = [];
|
|
try {
|
|
to.forEach(id => {
|
|
let tmp = id.trim();
|
|
if (!floCrypto.validateAddr(tmp))
|
|
throw "Invalid Address: " + tmp
|
|
if (!recipients.includes(tmp))
|
|
recipients.push(tmp);
|
|
})
|
|
buttonLoader('send_mail_button', true)
|
|
messenger.sendMail(subject, content, recipients).then(result => {
|
|
notify(`Mail sent!`, 'success')
|
|
renderMailList(result)
|
|
closePopup()
|
|
})
|
|
.catch(error => notify("Failed to send mail!", "error", error))
|
|
.finally(() => buttonLoader('send_mail_button', false))
|
|
} catch (error) {
|
|
notify(error, "error")
|
|
}
|
|
})
|
|
|
|
function replyMail() {
|
|
let recipient = getRef('reply_mail_popup').dataset.to;
|
|
if (recipient.includes(','))
|
|
recipient = recipient.split(',')
|
|
let subject = getRef('subject_of_reply_mail').value;
|
|
let content = getRef('reply_mail_content').value;
|
|
let prev = getRef('reply_mail_popup').dataset.prev;
|
|
messenger.sendMail(subject, content, recipient, prev).then(result => {
|
|
notify(`Mail replied!`);
|
|
renderMailList(result)
|
|
closePopup()
|
|
}).catch(error => notify("Failed to reply mail!", "error", error))
|
|
}
|
|
|
|
const flyOutLeft = [
|
|
{
|
|
transform: 'translateX(0)',
|
|
opacity: 1
|
|
},
|
|
{
|
|
transform: 'translateX(-1rem)',
|
|
opacity: 0
|
|
},
|
|
]
|
|
|
|
const flyInLeft = [
|
|
{
|
|
transform: 'translateX(1rem)',
|
|
opacity: 0
|
|
},
|
|
{
|
|
transform: 'translateX(0)',
|
|
opacity: 1
|
|
},
|
|
]
|
|
|
|
const flyOutRight = [
|
|
{
|
|
transform: 'translateX(0)',
|
|
opacity: 1
|
|
},
|
|
{
|
|
transform: 'translateX(1rem)',
|
|
opacity: 0
|
|
},
|
|
]
|
|
|
|
const flyInRight = [
|
|
{
|
|
transform: 'translateX(-1rem)',
|
|
opacity: 0
|
|
},
|
|
{
|
|
transform: 'translateX(0)',
|
|
opacity: 1
|
|
},
|
|
]
|
|
|
|
const animOptions = {
|
|
duration: 150,
|
|
easing: 'ease'
|
|
}
|
|
|
|
function animateTo(element, animation, options) {
|
|
const anime = element.animate(animation, { ...options, fill: 'both' })
|
|
anime.addEventListener('finish', () => {
|
|
anime.commitStyles()
|
|
anime.cancel()
|
|
})
|
|
return anime
|
|
}
|
|
|
|
function showPanel(subPageId) {
|
|
getRef('settings_title').textContent = subPageId
|
|
if (window.innerWidth < 720) {
|
|
animateTo(getRef('settings_sidebar'), flyOutLeft, animOptions).onfinish = () => {
|
|
animateTo(getRef('settings_panel'), flyInLeft, animOptions)
|
|
getRef('settings_sidebar').style = ''
|
|
getRef('settings_sidebar').classList.add('hide-on-mobile')
|
|
getRef('settings_panel').classList.remove('hide-on-mobile')
|
|
}
|
|
}
|
|
|
|
switch (subPageId) {
|
|
case 'blocked':
|
|
render.blockedList()
|
|
break;
|
|
}
|
|
|
|
document.querySelectorAll('.sidebar-item').forEach(item => item.classList.remove('active'))
|
|
document.querySelector(`.sidebar-item[href="#/settings/${subPageId}"]`).classList.add('active')
|
|
document.querySelectorAll('.panel').forEach(panel => panel.classList.add('hidden'))
|
|
getRef(subPageId).classList.remove('hidden')
|
|
}
|
|
|
|
function hidePanel() {
|
|
if (floGlobals.isMobileView && !getRef('settings_panel').classList.contains('hide-on-mobile')) {
|
|
animateTo(getRef('settings_panel'), flyOutRight, animOptions).onfinish = () => {
|
|
getRef('settings_title').textContent = ''
|
|
getRef('settings_panel').style = ''
|
|
animateTo(getRef('settings_sidebar'), flyInRight, animOptions)
|
|
getRef('settings_panel').classList.add('hide-on-mobile')
|
|
getRef('settings_sidebar').classList.remove('hide-on-mobile')
|
|
}
|
|
}
|
|
}
|
|
function handleGroupDescriptionChange(e) {
|
|
messenger.changeGroupDescription(floGlobals.viewingDetailsOfAddress, e.detail.value.trim())
|
|
.then(res => {
|
|
notify('Changed group description', 'success')
|
|
})
|
|
.catch(error => notify(error, "error"));
|
|
}
|
|
|
|
async function changeContactName(name) {
|
|
const floID = floGlobals.viewingDetailsOfAddress
|
|
const type = getAddressType(floID)
|
|
if (type === 'group') {
|
|
messenger.changeGroupName(floID, name).then(res => {
|
|
updateChatCards({ name, floID })
|
|
notify('Changed group name', 'success')
|
|
})
|
|
.catch(error => notify(error, "error"));
|
|
} else {
|
|
messenger.storeContact(floID, name).then(result => {
|
|
updateChatCards({ name, floID })
|
|
notify('Changed contact name', 'success')
|
|
})
|
|
.catch(error => notify(error, "error"));
|
|
}
|
|
}
|
|
|
|
function updateChatCards({ name, floID }) {
|
|
const type = getAddressType(floID)
|
|
if (activeChat.address && floCrypto.isSameAddr(activeChat.address, clickedContact.address)) {
|
|
updateChatHeaderName(name)
|
|
}
|
|
if (type === 'plain') {
|
|
getRef('contact_initial').textContent = name.charAt(0)
|
|
if (activeChat.address && floCrypto.isSameAddr(activeChat.address, clickedContact.address)) {
|
|
getRef('receiver_initial').textContent = name.charAt(0)
|
|
}
|
|
document.querySelectorAll(`.contact[data-address="${floID}"]`).forEach(contact => {
|
|
contact.querySelector('.initial').textContent = name.charAt(0)
|
|
contact.querySelector('.name').textContent = name
|
|
})
|
|
}
|
|
}
|
|
|
|
function getChatCard(address) {
|
|
let floAddress
|
|
let btcAddress
|
|
let ethAddress
|
|
const addressPubKey = floGlobals.pubKeys[address] || getEthPubKey(address);
|
|
floAddress = validateEthAddress(address) ? floCrypto.getFloID(addressPubKey) : floCrypto.toFloID(address)
|
|
if (floAddress)
|
|
btcAddress = btcOperator.convert.legacy2bech(floAddress)
|
|
ethAddress = addressPubKey ? floEthereum.ethAddressFromCompressedPublicKey(addressPubKey) : null;
|
|
return getRef('chats_list').querySelector(`[data-address="${floAddress}"], [data-address="${btcAddress}"], [data-address="${ethAddress}"], [data-address="${address}"]`)
|
|
}
|
|
|
|
function addAsContact() {
|
|
openPopup('add_contact_popup')
|
|
getRef('add_contact_floID').value = clickedContact.address
|
|
}
|
|
|
|
function markAsUnread() {
|
|
getChatCard(floGlobals.viewingDetailsOfAddress).classList.add('unread')
|
|
messenger.addMark(floGlobals.viewingDetailsOfAddress, 'unread')
|
|
closePopup()
|
|
}
|
|
|
|
function markAsRead() {
|
|
getChatCard(floGlobals.viewingDetailsOfAddress).classList.remove('unread')
|
|
messenger.removeMark(floGlobals.viewingDetailsOfAddress, 'unread')
|
|
closePopup()
|
|
}
|
|
|
|
function blockUser() {
|
|
getConfirmation('Block this address?', { message: `Are you sure to block this address?`, confirmText: 'Block', cancelText: 'Cancel' }).then(confirmed => {
|
|
if (confirmed) {
|
|
messenger.blockUser(floGlobals.viewingDetailsOfAddress).then(result => {
|
|
getChatCard(floGlobals.viewingDetailsOfAddress).querySelector('.last-message').textContent = 'This user is blocked'
|
|
closePopup()
|
|
notify('Address blocked', 'success')
|
|
})
|
|
}
|
|
})
|
|
}
|
|
function unblockUser(floID) {
|
|
getConfirmation('Unblock this address?', { message: `Are you sure to unblock this address?`, confirmText: 'Unblock', cancelText: 'Cancel' }).then(confirmed => {
|
|
if (confirmed) {
|
|
messenger.unblockUser(floID || floGlobals.viewingDetailsOfAddress).then(result => {
|
|
const chatCard = getChatCard(floID || floGlobals.viewingDetailsOfAddress);
|
|
getLastMessage(floGlobals.viewingDetailsOfAddress).then(({ lastText }) => {
|
|
chatCard.querySelector('.last-message').textContent = lastText
|
|
}).catch(error => {
|
|
console.error(error)
|
|
})
|
|
chatCard.querySelector('.last-message').textContent = 'This user is unblocked'
|
|
notify('Address unblocked', 'success')
|
|
closePopup()
|
|
render.blockedList()
|
|
renderChatList()
|
|
})
|
|
}
|
|
})
|
|
}
|
|
getRef('blocked_list').addEventListener('click', e => {
|
|
if (e.target.closest('.unblock')) {
|
|
unblockUser(e.target.closest('.blocked-id').dataset.address)
|
|
}
|
|
})
|
|
function clearChat() {
|
|
getConfirmation('Clear chat?', { message: `Are you sure to clear this chat?`, confirmText: 'Clear', cancelText: 'Cancel', danger: true }).then(confirmed => {
|
|
if (confirmed) {
|
|
messenger.clearChat(floGlobals.viewingDetailsOfAddress).then(result => {
|
|
let chatCard = getChatCard(floGlobals.viewingDetailsOfAddress);
|
|
if (chatCard) {
|
|
chatCard.querySelector('.last-message').textContent = ''
|
|
chatCard.querySelector('.time').textContent = ''
|
|
}
|
|
renderElem(getRef('messages_container'), html``)
|
|
closePopup()
|
|
notify('Chat cleared', 'success')
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
function deleteChat() {
|
|
getConfirmation('Delete chat', { message: `Are you sure to delete this chat?`, confirmText: 'Delete', cancelText: 'Cancel', danger: true }).then(confirmed => {
|
|
if (confirmed) {
|
|
messenger.rmChat(floGlobals.viewingDetailsOfAddress).then(result => {
|
|
getChatCard(floGlobals.viewingDetailsOfAddress).remove()
|
|
closePopup()
|
|
activeChat = {}
|
|
location.hash = '#/chat_page'
|
|
notify('Chat deleted', 'success')
|
|
}).catch(error => {
|
|
console.error(error)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
let isGroupEditable = false
|
|
let isRemovingMember = false
|
|
function editGroupMembers(groupID) {
|
|
if (isGroupEditable) {
|
|
// to-do: make group members non editable
|
|
render.groupMembers(groupID)
|
|
membersToRemove.clear()
|
|
renderElem(getRef('edit_group_button'), 'Edit');
|
|
addClass(['#group_members_tip', '#member_options'], 'hidden')
|
|
} else {
|
|
// to-do: make group members selectable except for admin
|
|
render.groupMembers(groupID, true)
|
|
renderElem(getRef('edit_group_button'), 'Done');
|
|
removeClass(['#group_members_tip', '#member_options', '#init_add_members_button'], 'hidden')
|
|
getRef('remove_members_button').classList.add('hidden')
|
|
}
|
|
isGroupEditable = isRemovingMember = !isGroupEditable
|
|
}
|
|
const membersToRemove = new Set()
|
|
function selectMemberToRemove(floID) {
|
|
if (membersToRemove.has(floID)) {
|
|
membersToRemove.delete(floID)
|
|
} else {
|
|
membersToRemove.add(floID)
|
|
}
|
|
if (membersToRemove.size) {
|
|
addClass(['#group_members_tip', '#init_add_members_button'], 'hidden')
|
|
getRef('remove_members_button').classList.remove('hidden')
|
|
} else {
|
|
removeClass(['#group_members_tip', '#init_add_members_button'], 'hidden')
|
|
getRef('remove_members_button').classList.add('hidden')
|
|
}
|
|
}
|
|
getRef('popup_contacts_container').addEventListener('change', e => {
|
|
selectMemberToAdd(e.target.value)
|
|
})
|
|
|
|
const membersToAdd = new Set()
|
|
function selectMemberToAdd(floID) {
|
|
if (membersToAdd.has(floID)) {
|
|
membersToAdd.delete(floID)
|
|
} else {
|
|
membersToAdd.add(floID)
|
|
}
|
|
getRef('add_members_button').disabled = !Boolean(membersToAdd.size)
|
|
}
|
|
|
|
getRef('add_members_button').addEventListener('click', addGroupMembers)
|
|
|
|
function addGroupMembers() {
|
|
const groupID = getRef('edit_group_button').dataset.groupId
|
|
messenger.addGroupMembers(groupID, [...membersToAdd])
|
|
.then(res => {
|
|
render.groupMembers(groupID)
|
|
closePopup()
|
|
})
|
|
.catch(err => console.error(err))
|
|
}
|
|
|
|
function removeGroupMembers() {
|
|
getConfirmation('Remove group members', { message: `Are you sure to remove these members from this group?`, confirmText: 'Remove', cancelText: 'No' }).then(confirmed => {
|
|
if (confirmed) {
|
|
const groupID = getRef('edit_group_button').dataset.groupId
|
|
messenger.rmGroupMembers(groupID, [...membersToRemove])
|
|
.then(res => {
|
|
editGroupMembers(groupID)
|
|
})
|
|
.catch(err => console.error(err))
|
|
}
|
|
})
|
|
}
|
|
|
|
document.addEventListener('colorselected', e => {
|
|
const color = e.detail.value
|
|
localStorage.setItem(`accent-color${floDapps.user.id}`, color);
|
|
document.body.style.setProperty('--accent-color', color);
|
|
})
|
|
|
|
getRef('select_bg_image').addEventListener('change', async function (e) {
|
|
await compactIDB.writeData('userSettings', true, 'hasSelectedBg')
|
|
compactIDB.writeData('userSettings', this.files[0], 'bgImage')
|
|
.then(async res => {
|
|
setBgImage()
|
|
notify('Background applied', 'success')
|
|
})
|
|
.catch(err => console.error(err))
|
|
})
|
|
|
|
getRef('bg_preview_container').addEventListener('change', async e => {
|
|
await compactIDB.writeData('userSettings', e.target.value === 'img', 'hasSelectedBg');
|
|
[...getRef('bg_preview_container').children].forEach(child => {
|
|
if (child.firstElementChild.value === e.target.value)
|
|
child.classList.add('bg-preview--selected')
|
|
else
|
|
child.classList.remove('bg-preview--selected')
|
|
})
|
|
if (e.target.value === 'img') {
|
|
setBgImage()
|
|
} else {
|
|
setDefaultBg()
|
|
}
|
|
})
|
|
async function setBgImage() {
|
|
try {
|
|
const hasSelectedBg = await compactIDB.readData('userSettings', 'hasSelectedBg')
|
|
const image = await compactIDB.readData('userSettings', 'bgImage')
|
|
if (image) {
|
|
const url = URL.createObjectURL(image)
|
|
if (hasSelectedBg) {
|
|
getRef('background_image').src = url
|
|
addClass(['#chat_view', '#mail', '#chat_preview'], 'has-bg-image')
|
|
getRef('select_bg_button').textContent = 'Change background'
|
|
getRef('selected_bg_preview').classList.add('bg-preview--selected')
|
|
getRef('default_bg_preview').classList.remove('bg-preview--selected')
|
|
getRef('backdrop_options').classList.remove('hidden')
|
|
} else {
|
|
getRef('backdrop_options').classList.add('hidden')
|
|
}
|
|
getRef('selected_bg_preview').classList.remove('hidden')
|
|
getRef('selected_bg_preview').querySelector('img').src = url
|
|
}
|
|
const [bgOpacity, bgBlur] = await Promise.all([compactIDB.readData('userSettings', 'bgOpacity'), compactIDB.readData('userSettings', 'bgBlur')])
|
|
if (bgOpacity !== undefined) {
|
|
getRef('backdrop_opacity').value = bgOpacity * 100
|
|
getRef('backdrop_opacity_value').value = `${parseInt(bgOpacity * 100)}%`
|
|
getRef('background_overlay').style.setProperty('--opacity', bgOpacity)
|
|
}
|
|
if (bgBlur !== undefined) {
|
|
getRef('backdrop_blur').value = bgBlur * 100
|
|
getRef('backdrop_blur_value').value = `${parseInt(bgBlur * 100)}%`
|
|
getRef('background_image').style.setProperty('--blur', `${bgBlur}rem`)
|
|
getRef('background_image').style.setProperty('--scale', bgBlur)
|
|
|
|
}
|
|
}
|
|
catch (err) {
|
|
console.error(err)
|
|
}
|
|
}
|
|
|
|
function setDefaultBg() {
|
|
compactIDB.writeData('userSettings', false, 'hasSelectedBg')
|
|
.then(async res => {
|
|
getRef('background_image').src = ''
|
|
removeClass(['#chat_view', '#mail', '#chat_preview'], 'has-bg-image')
|
|
getRef('backdrop_options').classList.add('hidden')
|
|
})
|
|
.catch(err => console.error(err))
|
|
}
|
|
|
|
getRef('backdrop_opacity').addEventListener('input', e => {
|
|
const opacity = e.target.value
|
|
getRef('backdrop_opacity_value').value = `${opacity}%`
|
|
const validOpacity = parseFloat((opacity * 0.01).toFixed(2))
|
|
getRef('background_overlay').style.setProperty('--opacity', validOpacity)
|
|
compactIDB.writeData('userSettings', validOpacity, 'bgOpacity')
|
|
.catch(err => console.error(err))
|
|
})
|
|
getRef('backdrop_blur').addEventListener('input', e => {
|
|
const blur = e.target.value
|
|
getRef('backdrop_blur_value').value = `${blur}%`
|
|
const validBlur = parseFloat((blur * 0.01).toFixed(2))
|
|
getRef('background_image').style.setProperty('--blur', `${validBlur}rem`)
|
|
getRef('background_image').style.setProperty('--scale', validBlur)
|
|
compactIDB.writeData('userSettings', validBlur, 'bgBlur')
|
|
.catch(err => console.error(err))
|
|
})
|
|
|
|
|
|
|
|
function updateChatHeaderName(name) {
|
|
if (!floGlobals.isMobileView) {
|
|
const animOptions = {
|
|
duration: 200,
|
|
easing: 'ease',
|
|
fill: 'forwards'
|
|
};
|
|
if (document.startViewTransition) {
|
|
document.startViewTransition(() => {
|
|
getRef('receiver_name').textContent = name
|
|
})
|
|
} else {
|
|
let originalName = getRef('receiver_name').textContent
|
|
getRef('chat_details_button').style.overflow = 'initial'
|
|
const originalPillWidth = getRef('chat_details_button').getBoundingClientRect().width;
|
|
const originalNameWidth = getRef('receiver_name').getBoundingClientRect().width;
|
|
getRef('receiver_name').textContent = name
|
|
const changedWidth = getRef('chat_details_button').getBoundingClientRect().width;
|
|
const widthDelta = changedWidth - originalPillWidth
|
|
const leftMove = -widthDelta / 2;
|
|
const rightMove = widthDelta / 2;
|
|
const scale = (changedWidth / (originalPillWidth || 1)).toFixed(2) || 1;
|
|
getRef('receiver_name').textContent = originalName
|
|
getRef('receiver_name').style.width = `${originalNameWidth}px`
|
|
renderElem(getRef('receiver_name'), html`
|
|
<span>${name}</span>
|
|
<span>${originalName}</span>
|
|
`)
|
|
getRef('receiver_name').children[0].animate(fadeIn, animOptions)
|
|
getRef('receiver_name').children[1].animate(fadeOut, animOptions).onfinish = e => {
|
|
e.target.cancel()
|
|
getRef('receiver_name').style.width = 'auto'
|
|
renderElem(getRef('receiver_name'), html`${name}`)
|
|
}
|
|
getRef('pseudo_background').animate([
|
|
{
|
|
width: `${originalPillWidth}px`
|
|
}, {
|
|
width: `${changedWidth}px`
|
|
}
|
|
], animOptions).onfinish = (e) => {
|
|
getRef('chat_details_button').style.overflow = 'hidden'
|
|
e.target.cancel()
|
|
}
|
|
getRef('chat_details_button').querySelector('#receiver_initial').animate([
|
|
{
|
|
transform: `none`
|
|
},
|
|
{
|
|
transform: `translateX(${leftMove}px)`
|
|
},
|
|
], animOptions).onfinish = e => {
|
|
e.target.cancel()
|
|
}
|
|
getRef('chat_details_button').children[2].animate([
|
|
{
|
|
transform: `none`
|
|
},
|
|
{
|
|
transform: `translateX(${rightMove}px)`
|
|
},
|
|
], animOptions).onfinish = e => {
|
|
e.target.cancel()
|
|
}
|
|
}
|
|
} else {
|
|
getRef('receiver_name').textContent = name
|
|
}
|
|
}
|
|
|
|
function handleMultisigModeChange(e) {
|
|
render.multisigAddresses()
|
|
}
|
|
|
|
async function checkBalance(e) {
|
|
const multisig = e.target.closest('.multisig-option')
|
|
const multisigAddress = multisig.dataset.address;
|
|
renderElem(multisig.querySelector('.multisig-option__balance'), html`<sm-spinner></sm-spinner>`);
|
|
const multisigMode = getRef('multisig_mode_selector').value;
|
|
let balance = 0;
|
|
try {
|
|
if (multisigMode === 'btc') {
|
|
balance = await btcOperator.getBalance(multisigAddress)
|
|
} else if (multisigMode === 'flo') {
|
|
balance = await floBlockchainAPI.getBalance(floCrypto.toMultisigFloID(multisigAddress))
|
|
}
|
|
renderElem(multisig.querySelector('.multisig-option__balance'), html`
|
|
${balance} ${multisigMode.toUpperCase()}
|
|
<button class="button icon-only" onclick=${checkBalance} title="Refresh">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
|
</button>
|
|
`);
|
|
} catch (err) {
|
|
console.error(err)
|
|
notify('Failed to check balance', 'error')
|
|
renderElem(multisig.querySelector('.multisig-option__balance'), html` <button class="button button--small" onclick=${checkBalance}>Check balance</button> `)
|
|
}
|
|
}
|
|
function showMultisigDetails(e) {
|
|
const multisigAddress = e.target.closest('.multisig-option').dataset.address;
|
|
renderElem(getRef('multisig_details'), html`<sm-spinner></sm-spinner>`)
|
|
openPopup('multisig_detail_popup')
|
|
messenger.multisig.listAddress().then(addresses => {
|
|
const { minRequired, pubKeys, time } = addresses[multisigAddress]
|
|
renderElem(getRef('multisig_details'), html`
|
|
<div class="grid gap-0-3">
|
|
<div class="label">Address</div>
|
|
<sm-copy value=${multisigAddress} clip-text></sm-copy>
|
|
</div>
|
|
<div class="grid">
|
|
<div class="label">Created</div>
|
|
<p><time class="value">${getFormattedTime(time)}</time></p>
|
|
</div>
|
|
<div class="grid">
|
|
<div class="label">Min. signatures required</div>
|
|
<p class="value">${minRequired}</p>
|
|
</div>
|
|
<div class="grid">
|
|
<div class="label">Members</div>
|
|
<ul>
|
|
${pubKeys.map(pubKey => render.multisigMemberCard(pubKey))}
|
|
</ul>
|
|
</div>
|
|
`)
|
|
}).catch(err => {
|
|
console.error(err)
|
|
notify('Failed to fetch multisig addresses', 'error')
|
|
})
|
|
}
|
|
const debouncedFeeCalculation = debounce(calculateFees, 300)
|
|
function startMultisigProcess(e) {
|
|
const multisigAddress = e.target.closest('.multisig-option').dataset.address;
|
|
getRef('send_tx').dataset.multisigAddress = multisigAddress;
|
|
const multisigMode = getRef('multisig_mode_selector').value;
|
|
getRef('receiver_container').innerHTML = ''
|
|
addReceiver()
|
|
renderElem(getRef('send_tx__dynamic_content'), html``)
|
|
renderElem(getRef('selected_multisig__balance'), html`Fetching balance... <sm-spinner></sm-spinner>`)
|
|
if (multisigMode === 'btc') {
|
|
getRef('selected_multisig').textContent = multisigAddress;
|
|
btcOperator.getBalance(multisigAddress).then(balance => {
|
|
getRef('selected_multisig__balance').textContent = `Balance: ${balance} BTC`;
|
|
}).catch(err => notify(err, 'error'))
|
|
renderElem(getRef('send_tx__dynamic_content'), html`
|
|
<div id="fees_section" class="grid gap-0-5">
|
|
<div class="flex align-center space-between">
|
|
<h4>Fees</h4>
|
|
<sm-chips id="fees_selector" onchange="handleFeeSelector(event)">
|
|
<sm-chip value="suggested" selected>Suggested</sm-chip>
|
|
<sm-chip value="custom">Custom</sm-chip>
|
|
</sm-chips>
|
|
</div>
|
|
<p id="selected_fee_tip">*Fill out all fields for exact fee!</p>
|
|
<div id="send_fee_wrapper">
|
|
<sm-input type="number" id="send_fee" placeholder="Fee" min="0.00000001" step="0.00000001"
|
|
error-text="Please enter valid fees" readonly animate required>
|
|
<div class="currency-symbol flex" slot="icon">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24"></rect> </g> <g> <path d="M17.06,11.57C17.65,10.88,18,9.98,18,9c0-1.86-1.27-3.43-3-3.87L15,3h-2v2h-2V3H9v2H6v2h2v10H6v2h3v2h2v-2h2v2h2v-2 c2.21,0,4-1.79,4-4C19,13.55,18.22,12.27,17.06,11.57z M10,7h4c1.1,0,2,0.9,2,2s-0.9,2-2,2h-4V7z M15,17h-5v-4h5c1.1,0,2,0.9,2,2 S16.1,17,15,17z"> </path> </g> </svg>
|
|
</div>
|
|
</sm-input>
|
|
<div id="send_fee_loader" class="hidden flex align-center gap-0-5">
|
|
<sm-spinner></sm-spinner>
|
|
<span>Calculating fees...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="error_section" class="hidden"></div>
|
|
`)
|
|
calculateFees()
|
|
getRef('receiver_container').addEventListener('input', debouncedFeeCalculation)
|
|
} else if (multisigMode === 'flo') {
|
|
floBlockchainAPI.getBalance(floCrypto.toMultisigFloID(multisigAddress)).then(balance => {
|
|
getRef('selected_multisig__balance').textContent = `Balance: ${balance} FLO`;
|
|
}).catch(err => notify(err, 'error'))
|
|
getRef('selected_multisig').textContent = floCrypto.toMultisigFloID(multisigAddress);
|
|
renderElem(getRef('send_tx__dynamic_content'), html`
|
|
<sm-textarea id="send_tx__flo_data" placeholder="FLO data" rows="6"></sm-textarea>
|
|
`)
|
|
}
|
|
const { opened } = openPopup('multisig_tx_popup')
|
|
opened.then(() => {
|
|
getRef('send_tx').querySelector('.receiver-input').focusIn()
|
|
})
|
|
}
|
|
const txParticipantsObserver = new MutationObserver(mutations => {
|
|
mutations.forEach(mutation => {
|
|
if (mutation.type === 'childList') {
|
|
if (mutation.addedNodes.length > 0 && mutation.target.children.length > 1) {
|
|
const removeButton = mutation.target.firstElementChild.querySelector('.remove-card')
|
|
if (!removeButton) {
|
|
const newRemoveButton = html.node`
|
|
<button class="remove-card button--small">
|
|
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0V0z" fill="none"></path> <path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"></path> </svg>
|
|
Remove
|
|
</button>
|
|
`
|
|
mutation.target.firstElementChild.querySelector('.remove-card-wrapper').appendChild(newRemoveButton)
|
|
}
|
|
} else if (mutation.removedNodes.length > 0 && mutation.target.children.length === 1) {
|
|
const removeButton = mutation.target.firstElementChild.querySelector('.remove-card')
|
|
if (removeButton) {
|
|
removeButton.remove()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
txParticipantsObserver.observe(getRef('receiver_container'), {
|
|
childList: true
|
|
})
|
|
let globalExchangeRate = {}
|
|
async function getExchangeRate() {
|
|
return new Promise((resolve, reject) => {
|
|
Promise.all(['usd', 'inr'].map(cur => fetch(`https://bitpay.com/api/rates/btc/${cur}`))).then(responses => {
|
|
Promise.all(responses.map(res => res.json())).then(rates => {
|
|
rates.forEach(rate => {
|
|
globalExchangeRate[rate.code.toLowerCase()] = rate.rate
|
|
})
|
|
globalExchangeRate.btc = 1
|
|
resolve(globalExchangeRate)
|
|
}).catch(err => console.error(err))
|
|
}).catch(err => console.error(err))
|
|
})
|
|
}
|
|
function resetMultisigProcess() {
|
|
getRef('receiver_container').removeEventListener('input', debouncedFeeCalculation)
|
|
}
|
|
const icons = {
|
|
btc: `<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24"></rect> </g> <g> <path d="M17.06,11.57C17.65,10.88,18,9.98,18,9c0-1.86-1.27-3.43-3-3.87L15,3h-2v2h-2V3H9v2H6v2h2v10H6v2h3v2h2v-2h2v2h2v-2 c2.21,0,4-1.79,4-4C19,13.55,18.22,12.27,17.06,11.57z M10,7h4c1.1,0,2,0.9,2,2s-0.9,2-2,2h-4V7z M15,17h-5v-4h5c1.1,0,2,0.9,2,2 S16.1,17,15,17z"> </path> </g> </svg>`,
|
|
flo: `<svg class="icon" width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"> <mask id="path-1-outside-1_16_6" maskUnits="userSpaceOnUse" x="3" y="3" width="58" height="58"> <rect fill="white" x="3" y="3" width="58" height="58"></rect> <path fill-rule="evenodd" clip-rule="evenodd" d="M31.946 43.6019C28.8545 47.1308 28.7789 51.9719 31.9811 55.1957C35.0078 52.2797 35.0105 46.8446 31.946 43.6019ZM31.9487 10.6835C24.6452 19.1291 24.9206 29.1137 31.9433 37.4108C39.0929 28.9436 39.1118 19.0076 31.9487 10.6808V10.6835ZM37.1111 35.051C43.1861 28.841 50.5976 27.4208 59 28.7654C56.2919 34.9754 52.4714 39.9353 45.7214 42.53C47.2118 41.3609 48.5699 40.4051 49.7984 39.3089C51.8504 37.481 53.303 35.267 54.0293 32.6075C54.2588 31.7678 53.9618 31.5302 53.1896 31.406C50.3627 30.9524 47.7086 31.5464 45.1733 32.702C40.9073 34.646 37.4324 37.6403 34.1735 40.8938C34.1168 40.9532 34.0925 41.0396 33.9899 41.2259C40.3754 41.4689 45.2381 44.0177 48.119 49.9064C44.3768 50.738 40.9532 50.5625 37.778 48.5213C40.1702 49.2557 42.557 49.7903 45.176 48.5483C44.852 48.0083 44.663 47.3765 44.231 47.0228C43.0511 46.0589 41.8415 45.0842 40.505 44.3498C38.4395 43.2212 36.131 42.692 33.5768 42.1682C37.0247 48.2081 36.1607 53.6918 31.946 59C29.9858 56.5295 28.6142 53.9456 28.2389 50.8946C27.8609 47.8328 28.7222 45.0518 30.1991 42.3896C26.9321 41.8658 19.8824 45.6485 18.7916 48.5645C21.3215 49.8443 23.7893 49.2611 26.33 48.3161C24.5048 50.1953 19.2425 50.9648 15.8108 49.8335C18.6593 44.0285 23.4977 41.4716 30.0263 41.2286C29.5457 40.7426 29.1893 40.3673 28.8167 40.0082C26.1005 37.3892 23.2142 34.9862 19.823 33.2366C17.0501 31.8056 14.1422 30.9092 10.9481 31.3871C10.5701 31.4438 10.1948 31.5356 9.6656 31.6436C10.6376 36.6386 13.9856 39.7355 18.1274 42.3464C13.775 41.8199 6.431 34.214 5 28.7546C13.397 27.3938 20.849 28.8842 26.897 35.2319C21.8939 24.089 24.5561 14.2259 31.946 5C39.2765 14.153 41.9657 23.9459 37.1111 35.051"> </path> </mask> <path fill-rule="evenodd" clip-rule="evenodd" d="M31.946 43.6019C28.8545 47.1308 28.7789 51.9719 31.9811 55.1957C35.0078 52.2797 35.0105 46.8446 31.946 43.6019ZM31.9487 10.6835C24.6452 19.1291 24.9206 29.1137 31.9433 37.4108C39.0929 28.9436 39.1118 19.0076 31.9487 10.6808V10.6835ZM37.1111 35.051C43.1861 28.841 50.5976 27.4208 59 28.7654C56.2919 34.9754 52.4714 39.9353 45.7214 42.53C47.2118 41.3609 48.5699 40.4051 49.7984 39.3089C51.8504 37.481 53.303 35.267 54.0293 32.6075C54.2588 31.7678 53.9618 31.5302 53.1896 31.406C50.3627 30.9524 47.7086 31.5464 45.1733 32.702C40.9073 34.646 37.4324 37.6403 34.1735 40.8938C34.1168 40.9532 34.0925 41.0396 33.9899 41.2259C40.3754 41.4689 45.2381 44.0177 48.119 49.9064C44.3768 50.738 40.9532 50.5625 37.778 48.5213C40.1702 49.2557 42.557 49.7903 45.176 48.5483C44.852 48.0083 44.663 47.3765 44.231 47.0228C43.0511 46.0589 41.8415 45.0842 40.505 44.3498C38.4395 43.2212 36.131 42.692 33.5768 42.1682C37.0247 48.2081 36.1607 53.6918 31.946 59C29.9858 56.5295 28.6142 53.9456 28.2389 50.8946C27.8609 47.8328 28.7222 45.0518 30.1991 42.3896C26.9321 41.8658 19.8824 45.6485 18.7916 48.5645C21.3215 49.8443 23.7893 49.2611 26.33 48.3161C24.5048 50.1953 19.2425 50.9648 15.8108 49.8335C18.6593 44.0285 23.4977 41.4716 30.0263 41.2286C29.5457 40.7426 29.1893 40.3673 28.8167 40.0082C26.1005 37.3892 23.2142 34.9862 19.823 33.2366C17.0501 31.8056 14.1422 30.9092 10.9481 31.3871C10.5701 31.4438 10.1948 31.5356 9.6656 31.6436C10.6376 36.6386 13.9856 39.7355 18.1274 42.3464C13.775 41.8199 6.431 34.214 5 28.7546C13.397 27.3938 20.849 28.8842 26.897 35.2319C21.8939 24.089 24.5561 14.2259 31.946 5C39.2765 14.153 41.9657 23.9459 37.1111 35.051"> </path> <path d="M31.946 43.6019L32.6728 42.915L31.918 42.1163L31.1938 42.943L31.946 43.6019ZM31.9811 55.1957L31.2716 55.9004L31.9657 56.5992L32.6749 55.9159L31.9811 55.1957ZM31.9487 10.6835L32.7051 11.3376L32.9487 11.0559V10.6835H31.9487ZM31.9433 37.4108L31.18 38.0569L31.9442 38.9597L32.7074 38.056L31.9433 37.4108ZM31.9487 10.6808L32.7068 10.0287L30.9487 7.98495V10.6808H31.9487ZM59 28.7654L59.9166 29.1651L60.4326 27.9819L59.158 27.778L59 28.7654ZM45.7214 42.53L45.1042 41.7432L46.0802 43.4634L45.7214 42.53ZM49.7984 39.3089L49.1332 38.5622L49.1326 38.5628L49.7984 39.3089ZM54.0293 32.6075L53.0647 32.3439L53.0646 32.3441L54.0293 32.6075ZM53.1896 31.406L53.3484 30.4187L53.348 30.4186L53.1896 31.406ZM45.1733 32.702L45.588 33.612L45.5881 33.6119L45.1733 32.702ZM34.1735 40.8938L33.467 40.1861L33.4585 40.1946L33.4501 40.2033L34.1735 40.8938ZM33.9899 41.2259L33.114 40.7435L32.3319 42.1635L33.9519 42.2252L33.9899 41.2259ZM48.119 49.9064L48.3359 50.8826L49.5751 50.6072L49.0173 49.4669L48.119 49.9064ZM37.778 48.5213L38.0715 47.5653L37.2372 49.3625L37.778 48.5213ZM45.176 48.5483L45.6045 49.4518L46.6008 48.9794L46.0335 48.0338L45.176 48.5483ZM44.231 47.0228L44.8645 46.2491L44.8637 46.2484L44.231 47.0228ZM40.505 44.3498L40.9866 43.4734L40.9845 43.4723L40.505 44.3498ZM33.5768 42.1682L33.7777 41.1886L31.6127 40.7446L32.7083 42.664L33.5768 42.1682ZM31.946 59L31.1626 59.6216L31.9457 60.6085L32.7292 59.6218L31.946 59ZM28.2389 50.8946L29.2314 50.7725L29.2314 50.7721L28.2389 50.8946ZM30.1991 42.3896L31.0736 42.8747L31.7652 41.6279L30.3574 41.4022L30.1991 42.3896ZM18.7916 48.5645L17.855 48.2141L17.5413 49.0527L18.3402 49.4568L18.7916 48.5645ZM26.33 48.3161L27.0473 49.0128L25.9814 47.3788L26.33 48.3161ZM15.8108 49.8335L14.9131 49.393L14.4073 50.4237L15.4977 50.7832L15.8108 49.8335ZM30.0263 41.2286L30.0635 42.2279L32.3372 42.1433L30.7373 40.5255L30.0263 41.2286ZM28.8167 40.0082L28.1226 40.7281L28.1228 40.7282L28.8167 40.0082ZM19.823 33.2366L19.3644 34.1252L19.3645 34.1253L19.823 33.2366ZM10.9481 31.3871L10.8001 30.3981L10.7998 30.3982L10.9481 31.3871ZM9.6656 31.6436L9.46564 30.6638L8.49474 30.8619L8.68401 31.8346L9.6656 31.6436ZM18.1274 42.3464L18.0073 43.3392L18.6607 41.5005L18.1274 42.3464ZM5 28.7546L4.84003 27.7675L3.75363 27.9435L4.03268 29.0082L5 28.7546ZM26.897 35.2319L26.173 35.9217L27.8093 34.8223L26.897 35.2319ZM31.946 5L32.7265 4.37488L31.9461 3.40036L31.1655 4.37483L31.946 5ZM31.1938 42.943C27.7974 46.8199 27.6566 52.261 31.2716 55.9004L32.6906 54.491C29.9012 51.6828 29.9116 47.4417 32.6982 44.2609L31.1938 42.943ZM32.6749 55.9159C36.1304 52.5867 36.081 46.5214 32.6728 42.915L31.2192 44.2888C33.94 47.1678 33.8852 51.9727 31.2873 54.4755L32.6749 55.9159ZM31.1923 10.0294C27.4048 14.4092 25.5379 19.2455 25.5737 24.1102C25.6095 28.971 27.5438 33.7608 31.18 38.0569L32.7066 36.7647C29.3201 32.7637 27.6054 28.4127 27.5736 24.0955C27.5419 19.7822 29.1891 15.4034 32.7051 11.3376L31.1923 10.0294ZM32.7074 38.056C36.4083 33.6729 38.31 28.8504 38.3133 23.9938C38.3165 19.1359 36.42 14.3451 32.7068 10.0287L31.1906 11.3329C34.6405 15.3433 36.3161 19.6839 36.3133 23.9925C36.3104 28.3024 34.6279 32.6815 31.1792 36.7656L32.7074 38.056ZM30.9487 10.6808V10.6835H32.9487V10.6808H30.9487ZM37.8259 35.7503C43.6131 29.8345 50.6632 28.444 58.842 29.7528L59.158 27.778C50.532 26.3976 42.7591 27.8475 36.3963 34.3517L37.8259 35.7503ZM58.0834 28.3657C55.4402 34.4268 51.7791 39.1301 45.3626 41.5966L46.0802 43.4634C53.1637 40.7405 57.1436 35.524 59.9166 29.1651L58.0834 28.3657ZM46.3386 43.3168C47.7733 42.1914 49.2057 41.178 50.4642 40.055L49.1326 38.5628C47.9341 39.6322 46.6503 40.5304 45.1042 41.7432L46.3386 43.3168ZM50.4636 40.0556C52.6466 38.111 54.2121 35.7338 54.994 32.871L53.0646 32.3441C52.3939 34.8002 51.0542 36.851 49.1332 38.5622L50.4636 40.0556ZM54.9939 32.8711C55.1262 32.3872 55.2248 31.6946 54.7698 31.1186C54.3643 30.6053 53.7257 30.4794 53.3484 30.4187L53.0308 32.3933C53.1086 32.4058 53.17 32.4181 53.2184 32.43C53.2671 32.442 53.2955 32.4518 53.3091 32.4572C53.3227 32.4626 53.3139 32.4606 53.2926 32.4461C53.2699 32.4307 53.2353 32.4025 53.2004 32.3583C53.1832 32.3366 53.1677 32.3132 53.1543 32.2886C53.1409 32.264 53.1307 32.2402 53.1231 32.2182C53.1079 32.1742 53.1053 32.144 53.1049 32.1366C53.1046 32.1302 53.1059 32.1427 53.1007 32.1797C53.0955 32.2165 53.0849 32.27 53.0647 32.3439L54.9939 32.8711ZM53.348 30.4186C50.274 29.9254 47.4176 30.5801 44.7585 31.7921L45.5881 33.6119C47.9996 32.5127 50.4514 31.9794 53.0312 32.3934L53.348 30.4186ZM44.7586 31.792C40.3293 33.8104 36.7535 36.9051 33.467 40.1861L34.88 41.6015C38.1113 38.3755 41.4853 35.4816 45.588 33.612L44.7586 31.792ZM33.4501 40.2033C33.3063 40.354 33.2291 40.5156 33.2033 40.5674C33.1672 40.64 33.1517 40.6749 33.114 40.7435L34.8658 41.7083C34.9307 41.5906 34.9786 41.4891 34.9945 41.4572C35.0049 41.4363 35.0004 41.4467 34.9882 41.4667C34.9731 41.4914 34.9435 41.5355 34.8969 41.5843L33.4501 40.2033ZM33.9519 42.2252C37.0233 42.3421 39.6629 43.0109 41.8597 44.3098C44.0485 45.6039 45.857 47.5582 47.2207 50.3459L49.0173 49.4669C47.5001 46.3659 45.4368 44.1014 42.8776 42.5882C40.3265 41.0798 37.342 40.3527 34.0279 40.2266L33.9519 42.2252ZM47.9021 48.9302C44.3241 49.7253 41.1983 49.5313 38.3188 47.6801L37.2372 49.3625C40.7081 51.5937 44.4295 51.7507 48.3359 50.8826L47.9021 48.9302ZM37.4845 49.4773C39.9089 50.2215 42.6205 50.8669 45.6045 49.4518L44.7475 47.6448C42.4935 48.7137 40.4315 48.2899 38.0715 47.5653L37.4845 49.4773ZM46.0335 48.0338C45.8849 47.7862 45.8212 47.6158 45.6292 47.2504C45.4736 46.9544 45.2423 46.5584 44.8645 46.2491L43.5975 47.7965C43.6517 47.8409 43.7309 47.9377 43.8588 48.181C43.9503 48.3549 44.1431 48.7704 44.3185 49.0628L46.0335 48.0338ZM44.8637 46.2484C43.6913 45.2906 42.4149 44.2582 40.9866 43.4734L40.0234 45.2262C41.2681 45.9102 42.4109 46.8272 43.5983 47.7972L44.8637 46.2484ZM40.9845 43.4723C38.7711 42.2628 36.3236 41.7107 33.7777 41.1886L33.3759 43.1478C35.9384 43.6733 38.1079 44.1796 40.0255 45.2273L40.9845 43.4723ZM32.7083 42.664C34.346 45.5328 34.9255 48.203 34.6411 50.7474C34.3555 53.3015 33.1882 55.8274 31.1628 58.3782L32.7292 59.6218C34.9185 56.8644 36.2905 53.9944 36.6287 50.9696C36.9679 47.9351 36.2555 44.8435 34.4453 41.6724L32.7083 42.664ZM32.7294 58.3784C30.8397 55.9968 29.577 53.5818 29.2314 50.7725L27.2464 51.0167C27.6514 54.3094 29.1319 57.0622 31.1626 59.6216L32.7294 58.3784ZM29.2314 50.7721C28.8877 47.9881 29.6602 45.4224 31.0736 42.8747L29.3247 41.9045C27.7842 44.6812 26.8341 47.6775 27.2464 51.0171L29.2314 50.7721ZM30.3574 41.4022C29.3303 41.2375 28.1125 41.4187 26.9159 41.7622C25.6987 42.1117 24.4105 42.6564 23.2045 43.307C21.999 43.9574 20.8488 44.728 19.9166 45.543C19.005 46.3399 18.2137 47.2553 17.855 48.2141L19.7282 48.9149C19.9149 48.4157 20.4139 47.7647 21.233 47.0487C22.0314 46.3507 23.0524 45.6616 24.1541 45.0672C25.2552 44.4732 26.4102 43.9882 27.4678 43.6846C28.5458 43.3751 29.4344 43.2798 30.0408 43.377L30.3574 41.4022ZM18.3402 49.4568C21.2702 50.939 24.0958 50.214 26.6786 49.2534L25.9814 47.3788C23.4828 48.3082 21.3728 48.7496 19.243 47.6722L18.3402 49.4568ZM25.6127 47.6194C24.9334 48.3187 23.4333 48.9382 21.5294 49.2141C19.6667 49.484 17.6562 49.3889 16.1239 48.8838L15.4977 50.7832C17.3971 51.4094 19.7336 51.4952 21.8162 51.1934C23.8578 50.8976 25.9014 50.1927 27.0473 49.0128L25.6127 47.6194ZM16.7085 50.274C18.0554 47.5292 19.8486 45.5942 22.044 44.3081C24.2482 43.0169 26.918 42.345 30.0635 42.2279L29.9891 40.2293C26.606 40.3552 23.5923 41.0832 21.0331 42.5824C18.4649 44.0869 16.4147 46.3328 14.9131 49.393L16.7085 50.274ZM30.7373 40.5255C30.2757 40.0586 29.8942 39.6578 29.5106 39.2882L28.1228 40.7282C28.4844 41.0768 28.8157 41.4266 29.3153 41.9317L30.7373 40.5255ZM29.5108 39.2883C26.7637 36.6396 23.7967 34.1615 20.2815 32.3479L19.3645 34.1253C22.6317 35.8109 25.4373 38.1388 28.1226 40.7281L29.5108 39.2883ZM20.2816 32.348C17.4111 30.8666 14.2829 29.877 10.8001 30.3981L11.0961 32.3761C14.0015 31.9414 16.6891 32.7446 19.3644 34.1252L20.2816 32.348ZM10.7998 30.3982C10.388 30.4599 9.94468 30.566 9.46564 30.6638L9.86556 32.6234C10.4449 32.5052 10.7522 32.4277 11.0964 32.376L10.7998 30.3982ZM8.68401 31.8346C9.73479 37.2345 13.3673 40.5279 17.5941 43.1923L18.6607 41.5005C14.6039 38.9431 11.5404 36.0427 10.6472 31.4526L8.68401 31.8346ZM18.2475 41.3536C17.3819 41.2489 16.2594 40.7693 14.9918 39.9335C13.742 39.1093 12.427 37.9891 11.1875 36.7057C8.68437 34.1137 6.62095 30.9947 5.96732 28.5011L4.03268 29.0082C4.81005 31.9739 7.13413 35.3875 9.74885 38.095C11.0683 39.4613 12.4947 40.6825 13.8908 41.6031C15.2691 42.512 16.6967 43.1806 18.0073 43.3392L18.2475 41.3536ZM5.15997 29.7417C13.3246 28.4186 20.4108 29.874 26.173 35.9217L27.621 34.5421C21.2872 27.8944 13.4694 26.369 4.84003 27.7675L5.15997 29.7417ZM27.8093 34.8223C25.3851 29.4232 24.8341 24.3801 25.7561 19.5859C26.6809 14.7778 29.1009 10.1516 32.7265 5.62517L31.1655 4.37483C27.4012 9.07433 24.7952 13.9927 23.7921 19.2082C22.7863 24.4378 23.4058 29.8977 25.9847 35.6415L27.8093 34.8223ZM31.1655 5.62512C34.7619 10.1157 37.1747 14.7077 38.1168 19.4869C39.0562 24.2523 38.5469 29.2701 36.1948 34.6504L38.0274 35.4515C40.5299 29.7268 41.1033 24.2956 40.0791 19.1001C39.0576 13.9183 36.4606 9.03731 32.7265 4.37488L31.1655 5.62512Z" mask="url(#path-1-outside-1_16_6)"></path> </svg>`
|
|
}
|
|
function addReceiver() {
|
|
const multisigMode = getRef('multisig_mode_selector').value;
|
|
const receiverCard = getRef('receiver_template').content.cloneNode(true)
|
|
if (!getRef('receiver_container').children.length) {
|
|
receiverCard.querySelector('.remove-card').remove()
|
|
}
|
|
receiverCard.querySelectorAll('.currency-symbol').forEach(symbol => symbol.innerHTML = icons[multisigMode])
|
|
getRef('receiver_container').appendChild(receiverCard);
|
|
if (multisigMode === 'flo') {
|
|
getRef('receiver_container').querySelectorAll('.receiver-input').forEach(input => {
|
|
input.setAttribute('error-text', 'Invalid FLO address')
|
|
input.customValidation = (value) => {
|
|
if (!value) return { isValid: false, errorText: 'Please enter a FLO address' }
|
|
return {
|
|
isValid: floCrypto.validateFloID(value),
|
|
errorText: `Invalid FLO address.<br> It usually starts with "F"`
|
|
}
|
|
}
|
|
})
|
|
} else if (multisigMode === 'btc') {
|
|
getRef('receiver_container').querySelectorAll('.receiver-input').forEach(input => {
|
|
input.setAttribute('error-text', 'Invalid BTC address')
|
|
input.customValidation = (value) => {
|
|
if (!value) return { isValid: false, errorText: 'Please enter a BTC address' }
|
|
return {
|
|
isValid: btcOperator.validateAddr(value),
|
|
errorText: `Invalid BTC address.<br> It usually starts with '1', '3' or 'bc1'`
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
function handleFeeSelector(e) {
|
|
switch (e.target.value) {
|
|
case 'custom':
|
|
getRef('send_fee').readOnly = false;
|
|
getRef('send_fee').placeholder = 'Fee';
|
|
renderElem(getRef('selected_fee_tip'), html`Set custom fee`)
|
|
feeMemo.memoized = false;
|
|
break;
|
|
case 'suggested':
|
|
calculateFees();
|
|
getRef('send_fee').readOnly = true;
|
|
break;
|
|
}
|
|
}
|
|
function calculateApproxFee() {
|
|
return new Promise((resolve, reject) => {
|
|
fetch('https://bitcoiner.live/api/fees/estimates/latest')
|
|
.then(res => {
|
|
res.json()
|
|
.then(data => {
|
|
const satPerByte = data.estimates['60'].sat_per_vbyte;
|
|
const legacyBytes = 200;
|
|
const segwitBytes = 77;
|
|
resolve((legacyBytes * satPerByte + (0.25 * satPerByte) * segwitBytes) / Math.pow(10, 8));
|
|
}).catch(e => {
|
|
reject(e)
|
|
})
|
|
}).catch(e => {
|
|
reject(e)
|
|
})
|
|
})
|
|
}
|
|
async function calculateBtcFees() {
|
|
const [sender, receivers, amounts, redeemScript] = await getTransactionInputs().catch(e => {
|
|
console.error(e)
|
|
return
|
|
});
|
|
return btcOperator.createMultiSigTx(sender, redeemScript, receivers, amounts)
|
|
}
|
|
async function getTransactionInputs() {
|
|
try {
|
|
const sender = getRef('selected_multisig').textContent;
|
|
const receivers = [...getRef('receiver_container').querySelectorAll('.receiver-input')].filter(input => input.value.trim() !== '').map(input => input.value.trim());
|
|
const amounts = [...getRef('receiver_container').querySelectorAll('.amount-input')].filter(input => input.value.trim() !== '').map(input => {
|
|
return parseFloat(input.value.trim())
|
|
});
|
|
const multisigList = await messenger.multisig.listAddress()
|
|
const redeemScript = multisigList[sender].redeemScript;
|
|
return [sender, receivers, amounts, redeemScript]
|
|
} catch (err) {
|
|
console.error(err)
|
|
}
|
|
}
|
|
let feeMemo = {
|
|
memoized: false,
|
|
memoizedFee: 0
|
|
};
|
|
function calculateFees() {
|
|
if (getRef('fees_selector').value === 'custom') return;
|
|
if (getRef('receiver_container').children.length === 0) return;
|
|
const allValid = [...getRef('receiver_container').querySelectorAll('sm-input')].every(input => input.isValid)
|
|
if (!allValid && feeMemo.memoized) return;
|
|
getRef('fees_selector').children[0].click();
|
|
getRef('fees_selector').classList.remove('hidden')
|
|
getRef('initiate_transaction').disabled = true;
|
|
getRef('send_fee').value = '';
|
|
getRef('send_fee_loader').classList.remove('hidden')
|
|
const animOptions = {
|
|
duration: 200,
|
|
easing: 'ease',
|
|
fill: 'forwards'
|
|
}
|
|
getRef('send_fee_loader').animate(fadeIn, animOptions)
|
|
getRef('fees_section').classList.remove('hidden')
|
|
getRef('error_section').classList.add('hidden')
|
|
if (allValid) {
|
|
getRef('send_fee').placeholder = 'Fee'
|
|
calculateBtcFees().then(({ fee }) => {
|
|
getRef('send_fee').value = fee.toFixed(8);
|
|
renderElem(getRef('selected_fee_tip'), html``)
|
|
getRef('initiate_transaction').disabled = false;
|
|
}).catch(e => {
|
|
getRef('fees_section').classList.add('hidden')
|
|
getRef('error_section').classList.remove('hidden')
|
|
renderElem(getRef('error_section'), html`
|
|
<p class="error flex align-center gap-0-5">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M11 15h2v2h-2v-2zm0-8h2v6h-2V7zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>
|
|
${e}
|
|
</p>
|
|
`)
|
|
console.error(e)
|
|
}).finally(_ => {
|
|
getRef('send_fee_loader').animate(fadeOut, animOptions).onfinish = _ =>
|
|
getRef('send_fee_loader').classList.add('hidden')
|
|
|
|
})
|
|
feeMemo.memoized = false;
|
|
} else {
|
|
getRef('send_fee').placeholder = 'Approximate fee'
|
|
renderElem(getRef('selected_fee_tip'), html` <p style="opacity: 0.8;">*Fill out all fields for exact fee!</p> `)
|
|
if (feeMemo.memoized) {
|
|
getRef('send_fee').value = feeMemo.memoizedFee.toFixed(8);
|
|
return;
|
|
}
|
|
calculateApproxFee().then(fee => {
|
|
getRef('send_fee').value = fee.toFixed(8);
|
|
feeMemo.memoizedFee = fee;
|
|
}).catch(e => {
|
|
getRef('fees_selector').children[1].click();
|
|
getRef('fees_selector').classList.add('hidden')
|
|
}).finally(_ => {
|
|
getRef('send_fee_loader').animate(fadeOut, animOptions).onfinish = _ =>
|
|
getRef('send_fee_loader').classList.add('hidden')
|
|
})
|
|
feeMemo.memoized = true;
|
|
}
|
|
}
|
|
|
|
getRef('initiate_transaction').onclick = async _ => {
|
|
buttonLoader('initiate_transaction', true)
|
|
const selectedMultisigAddress = getRef('send_tx').dataset.multisigAddress;
|
|
const receivers = [...getRef('receiver_container').querySelectorAll('.receiver-input')].map(input => input.value.trim());
|
|
const amounts = [...getRef('receiver_container').querySelectorAll('.amount-input')].map(input => {
|
|
return parseFloat(input.value.trim())
|
|
});
|
|
const multisigMode = getRef('multisig_mode_selector').value;
|
|
try {
|
|
const allMultiSigs = await messenger.multisig.listAddress()
|
|
const { redeemScript } = allMultiSigs[selectedMultisigAddress]
|
|
let result
|
|
if (multisigMode === 'btc') {
|
|
const fee = parseFloat(getRef('send_fee').value.trim());
|
|
console.debug(selectedMultisigAddress, receivers, amounts, fee);
|
|
result = await messenger.multisig.createTx_BTC(selectedMultisigAddress, redeemScript, receivers, amounts, fee)
|
|
} else if (multisigMode === 'flo') {
|
|
const floData = getRef('send_tx__flo_data').value.trim();
|
|
result = await messenger.multisig.createTx_FLO(floCrypto.toMultisigFloID(selectedMultisigAddress), redeemScript, receivers, amounts, floData)
|
|
}
|
|
console.log(result);
|
|
notify('New multisig group created', 'success')
|
|
closePopup();
|
|
getRef('send_tx').reset()
|
|
highlightNewGroup(result)
|
|
} catch (err) {
|
|
notify(`Error intiating transaction \n ${err}`, 'error');
|
|
} finally {
|
|
buttonLoader('initiate_transaction', false)
|
|
}
|
|
}
|
|
function highlightNewGroup(address) {
|
|
getRef('feature_mode').firstElementChild.click();
|
|
const createdPipelineCard = getRef('chats_list').querySelector(`[data-address="${address}"]`)
|
|
if (createdPipelineCard) {
|
|
createdPipelineCard.scrollIntoView({ behavior: 'smooth' })
|
|
createdPipelineCard.classList.add('highlight')
|
|
setTimeout(_ => createdPipelineCard.classList.remove('highlight'), 2000)
|
|
}
|
|
}
|
|
|
|
function signTransaction(pipeID) {
|
|
getConfirmation('Sign transaction', { message: 'Are you sure you want to sign this transaction?', confirmText: 'Sign' }).then(async (res) => {
|
|
if (!res) return
|
|
const banner = getRef('messages_container').querySelector('.signing-banner')
|
|
const button = banner.querySelector('button')
|
|
buttonLoader(button, true)
|
|
console.log('Signing transaction', pipeID);
|
|
try {
|
|
switch (messenger.pipeline[pipeID].model) {
|
|
case 'flo_multisig':
|
|
await messenger.multisig.signTx_FLO(pipeID)
|
|
break;
|
|
case 'btc_multisig':
|
|
await messenger.multisig.signTx_BTC(pipeID)
|
|
break;
|
|
}
|
|
getRef('messages_container').querySelector('.signing-banner').remove()
|
|
notify('Transaction signed', 'success')
|
|
if (messenger.pipeline[pipeID].disabled)
|
|
getRef('transaction_details').querySelector('h4').textContent = 'Transaction signatures complete'
|
|
} catch (err) {
|
|
notify(err, 'error')
|
|
buttonLoader(button, false)
|
|
}
|
|
})
|
|
}
|
|
|
|
const messengerIllustration = `
|
|
<svg width="484" height="484" viewBox="0 0 484 484" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<style>
|
|
.circle {
|
|
fill: var(--accent-color);
|
|
opacity: 0.5;
|
|
}
|
|
.foreground-circle{
|
|
fill: rgba(var(--foreground-color), 1)
|
|
}
|
|
.shadow-overlay{
|
|
fill: rgba(0 0 0 /0.1)
|
|
}
|
|
</style>
|
|
<g clip-path="url(#clip0_102_2)">
|
|
<circle class="circle" cx="243" cy="242" r="242" fill="#DBE5FF" fill-opacity="0.1" />
|
|
<circle class="circle" cx="242.5" cy="241.5" r="162.5" fill="#A6BEFC" fill-opacity="0.1" />
|
|
<circle class="circle" cx="242.5" cy="241.5" r="109.5" fill="#7295EE" fill-opacity="0.1" />
|
|
<g filter="url(#filter0_d_102_2)"> <circle cx="127.5" cy="383.5" r="42.5" class="foreground-circle" /> </g>
|
|
<path d="M140.757 372.193H117.947C116.017 372.193 114.438 373.758 114.438 375.672V391.328C114.438 393.259 116.017 394.807 117.947 394.807H140.757C142.705 394.807 144.266 393.259 144.266 391.328V375.672C144.266 373.758 142.705 372.193 140.757 372.193ZM140.757 378.577L129.352 384.37L117.947 378.577V375.672L129.352 381.43L140.757 375.672V378.577ZM110.929 391.328C110.929 391.624 110.982 391.902 111.017 392.198H103.911C102.942 392.198 102.156 391.415 102.156 390.458C102.156 389.502 102.942 388.719 103.911 388.719H110.929V391.328ZM107.42 374.802H111.017C110.982 375.098 110.929 375.376 110.929 375.672V378.281H107.42C106.455 378.281 105.665 377.498 105.665 376.542C105.665 375.585 106.455 374.802 107.42 374.802ZM103.911 383.5C103.911 382.543 104.7 381.76 105.665 381.76H110.929V385.24H105.665C104.7 385.24 103.911 384.457 103.911 383.5Z" fill="#72EEC1" />
|
|
<g filter="url(#filter1_d_102_2)"> <circle cx="410.5" cy="378.5" r="12.5" fill="#7286EE" /> </g>
|
|
<g filter="url(#filter2_d_102_2)"> <circle cx="78.5" cy="286.5" r="6.5" fill="#EE7297" /> </g>
|
|
<g filter="url(#filter3_d_102_2)"> <circle cx="361" cy="131" r="6" fill="#72B2EE" /> </g>
|
|
<g filter="url(#filter4_d_102_2)"> <circle cx="332.5" cy="368.5" r="10.5" class="foreground-circle" /> </g>
|
|
<g filter="url(#filter5_d_102_2)"> <circle cx="46.5" cy="185.5" r="10.5" class="foreground-circle" /> </g>
|
|
<g filter="url(#filter6_d_102_2)"> <circle cx="356.5" cy="84.5" r="10.5" class="foreground-circle" /> </g>
|
|
<g filter="url(#filter7_d_102_2)"> <circle cx="158.5" cy="124.5" r="50.5" class="foreground-circle" /> </g>
|
|
<path d="M165.907 119.56C164.951 123.383 159 121.441 157.15 120.917L158.846 114.195C160.758 114.75 166.893 115.552 165.907 119.56ZM156.318 124.37L154.468 131.801C156.749 132.387 163.81 134.638 164.858 130.444C165.968 126.066 158.599 124.925 156.318 124.37ZM188.908 131.462C184.777 147.988 168.065 158.04 151.538 153.908C135.012 149.777 124.969 133.065 129.092 116.538C133.223 100.012 149.935 89.9723 166.462 94.0917C182.958 98.2233 193.009 114.935 188.908 131.462ZM165.814 111.821L167.202 106.271L163.81 105.5L162.453 110.834C161.559 110.618 160.665 110.403 159.74 110.218L161.097 104.76L157.736 103.958L156.348 109.478C155.608 109.293 154.868 109.138 154.19 108.953L149.534 107.782L148.609 111.389C148.609 111.389 151.138 111.975 151.076 112.006C152.463 112.345 152.71 113.208 152.648 113.979L148.856 129.18C148.702 129.612 148.208 130.167 147.314 130.013C147.345 130.043 144.848 129.396 144.848 129.396L143.183 133.25L147.561 134.36C148.393 134.576 149.195 134.792 149.997 134.977L148.578 140.588L151.97 141.452L153.358 135.871C154.283 136.118 155.177 136.333 156.04 136.58L154.653 142.099L158.044 142.963L159.463 137.351C165.167 138.43 169.545 137.998 171.333 132.788C172.875 128.625 171.333 126.158 168.25 124.586C170.47 124 172.135 122.613 172.598 119.591C173.214 115.49 170.069 113.301 165.814 111.821Z" fill="#FFBF5E" />
|
|
<path d="M288.653 268.267V279.265C288.653 282.228 287.476 285.069 285.381 287.164C283.285 289.259 280.444 290.437 277.481 290.437H207.957C204.994 290.437 202.153 289.259 200.058 287.164C197.963 285.069 196.786 282.228 196.786 279.265V228.682L182 198L277.481 198.507C278.949 198.507 280.403 198.796 281.759 199.358C283.115 199.921 284.347 200.744 285.384 201.783C286.422 202.822 287.245 204.054 287.805 205.411C288.366 206.768 288.654 208.222 288.653 209.69V237.639" fill= "white" />
|
|
<path d="M288.653 210.42V281.939C288.775 293.984 256.437 291.393 256.437 291.393C259.4 291.393 262.242 290.216 264.337 288.12C266.432 286.025 267.609 283.184 267.609 280.221V198.855L277.481 199.24C278.949 199.24 280.402 199.529 281.758 200.091C283.114 200.653 284.346 201.477 285.384 202.515C286.421 203.553 287.244 204.786 287.805 206.142C288.366 207.499 288.654 208.952 288.653 210.42V210.42Z" class="shadow-overlay" />
|
|
<path d="M288.653 248.031V259.516" style="stroke: rgba(var(--text-color),1)" stroke-width="6" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
|
|
<path d="M288.653 268.267V279.265C288.653 282.228 287.476 285.069 285.381 287.164C283.285 289.259 280.444 290.437 277.481 290.437H207.957C204.994 290.437 202.153 289.259 200.058 287.164C197.963 285.069 196.786 282.228 196.786 279.265V228.682L182 198L277.481 198.507C278.949 198.507 280.403 198.796 281.759 199.358C283.115 199.921 284.347 200.744 285.384 201.783C286.422 202.822 287.245 204.054 287.805 205.411C288.366 206.768 288.654 208.222 288.653 209.69V237.639" style="stroke: rgba(var(--text-color), 1)" stroke-width="6" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
|
|
<path d="M257.679 257.264C257.679 261.232 256.103 265.037 253.297 267.842C250.492 270.648 246.687 272.224 242.719 272.224C238.752 272.224 234.947 270.648 232.141 267.842C229.336 265.037 227.76 261.232 227.76 257.264C227.76 248.993 257.679 248.993 257.679 257.264Z" fill="#ED1C24" stroke="black" stroke-width="6" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
|
|
<path d="M212.598 236.302C213.112 234.868 214.058 233.629 215.305 232.755C216.551 231.88 218.038 231.413 219.561 231.418C221.084 231.423 222.568 231.899 223.81 232.781C225.051 233.663 225.989 234.908 226.494 236.345" stroke="black" stroke-width="6" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
|
|
<path d="M258.959 236.302C259.473 234.868 260.419 233.629 261.666 232.755C262.913 231.88 264.4 231.413 265.922 231.418C267.445 231.423 268.929 231.899 270.171 232.781C271.412 233.663 272.35 234.908 272.855 236.345" stroke="black" stroke-width="6" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
|
|
<path d="M249.766 251.706C254.278 252.613 257.683 254.46 257.683 257.249C257.684 260.134 256.851 262.959 255.285 265.382C253.719 267.805 251.486 269.724 248.855 270.909" fill="#D30D41" />
|
|
<path d="M249.766 251.706C254.278 252.613 257.683 254.46 257.683 257.249C257.684 260.134 256.851 262.959 255.285 265.382C253.719 267.805 251.486 269.724 248.855 270.909" stroke="black" stroke-width="6" stroke-miterlimit="10" />
|
|
<g filter="url(#filter8_d_102_2)"> <circle cx="378.5" cy="242.5" r="44.5" class="foreground-circle" /> </g>
|
|
<path d="M379 229C388.9 229 397 234.967 397 242.333C397 249.7 388.9 255.667 379 255.667C376.768 255.667 374.626 255.367 372.646 254.833C367.39 259 361 259 361 259C365.194 255.117 365.86 252.5 365.95 251.5C362.89 249.117 361 245.883 361 242.333C361 234.967 369.1 229 379 229Z" fill="#E9707F" />
|
|
</g>
|
|
<defs> <filter id="filter0_d_102_2" x="53" y="325" width="149" height="149" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix" /> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> <feOffset dy="16" /> <feGaussianBlur stdDeviation="16" /> <feComposite in2="hardAlpha" operator="out" /> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_102_2" /> <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_102_2" result="shape" /> </filter> <filter id="filter1_d_102_2" x="366" y="350" width="89" height="89" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix" /> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> <feOffset dy="16" /> <feGaussianBlur stdDeviation="16" /> <feComposite in2="hardAlpha" operator="out" /> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_102_2" /> <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_102_2" result="shape" /> </filter> <filter id="filter2_d_102_2" x="40" y="264" width="77" height="77" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix" /> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> <feOffset dy="16" /> <feGaussianBlur stdDeviation="16" /> <feComposite in2="hardAlpha" operator="out" /> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_102_2" /> <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_102_2" result="shape" /> </filter> <filter id="filter3_d_102_2" x="323" y="109" width="76" height="76" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix" /> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> <feOffset dy="16" /> <feGaussianBlur stdDeviation="16" /> <feComposite in2="hardAlpha" operator="out" /> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_102_2" /> <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_102_2" result="shape" /> </filter> <filter id="filter4_d_102_2" x="290" y="342" width="85" height="85" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix" /> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> <feOffset dy="16" /> <feGaussianBlur stdDeviation="16" /> <feComposite in2="hardAlpha" operator="out" /> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_102_2" /> <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_102_2" result="shape" /> </filter> <filter id="filter5_d_102_2" x="4" y="159" width="85" height="85" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix" /> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> <feOffset dy="16" /> <feGaussianBlur stdDeviation="16" /> <feComposite in2="hardAlpha" operator="out" /> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_102_2" /> <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_102_2" result="shape" /> </filter> <filter id="filter6_d_102_2" x="314" y="58" width="85" height="85" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix" /> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> <feOffset dy="16" /> <feGaussianBlur stdDeviation="16" /> <feComposite in2="hardAlpha" operator="out" /> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_102_2" /> <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_102_2" result="shape" /> </filter> <filter id="filter7_d_102_2" x="76" y="58" width="165" height="165" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix" /> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> <feOffset dy="16" /> <feGaussianBlur stdDeviation="16" /> <feComposite in2="hardAlpha" operator="out" /> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_102_2" /> <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_102_2" result="shape" /> </filter> <filter id="filter8_d_102_2" x="302" y="182" width="153" height="153" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix" /> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> <feOffset dy="16" /> <feGaussianBlur stdDeviation="16" /> <feComposite in2="hardAlpha" operator="out" /> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" /> <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_102_2" /> <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_102_2" result="shape" /> </filter> <clipPath id="clip0_102_2"> <rect width="484" height="484" class="foreground-circle" /> </clipPath> </defs>
|
|
</svg>`
|
|
document.querySelectorAll('.messenger-illustration').forEach(elem => {
|
|
elem.innerHTML = messengerIllustration
|
|
})
|
|
|
|
|
|
delegate(getRef('notifications_list'), 'click', '.accept', e => {
|
|
getConfirmation('Are you sure you want to accept this request?').then((res) => {
|
|
if (!res) return;
|
|
const vectorClock = e.target.closest('.notification').dataset.id;
|
|
messenger.respond_pubKey(vectorClock).then(() => {
|
|
notify('Request accepted', 'success')
|
|
render.notifications()
|
|
}).catch(err => {
|
|
notify(`Error accepting request\n${err}`, 'error')
|
|
})
|
|
})
|
|
})
|
|
// delegate(getRef('notifications_list'), 'click', '.reject', e => {
|
|
// getConfirmation('Are you sure you want to reject this request?').then((res) => {
|
|
// if (!res) return;
|
|
// const vectorClock = e.target.closest('.notification').dataset.id;
|
|
// floCloudAPI.noteApplicationData(vectorClock, 'rejected').then(() => {
|
|
// e.target.closest('.notification').remove()
|
|
// notify('Request rejected', 'success')
|
|
// }).catch(err => {
|
|
// notify(`Error rejecting request\n${err}`, 'error')
|
|
// })
|
|
// })
|
|
// })
|
|
</script>
|
|
</body>
|
|
|
|
</html> |