messenger/index.html
sairajzero fe483ab172 Bug fixes
- Fixed various bugs
- Fixed: Backup-restore button not working
- Adding *.tmp* to git-ignore
2022-06-03 22:05:48 +05:30

3658 lines
192 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FLO Messenger</title>
<script id="floGlobals">
/* Constants for FLO blockchain operations !!Make sure to add this at begining!! */
const floGlobals = {
blockchain: "FLO",
application: "messenger",
adminID: "FMRsefPydWznGWneLqi4ABeQAJeFvtS3aQ"
}
</script>
<script src="scripts/lib.js"></script>
<script src="scripts/floCrypto.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/messenger.js"></script>
<script id="onLoadStartUp">
function onLoadStartUp() {
document.body.classList.remove('hide-completely')
isPinSet = false;
floDapps.setCustomPrivKeyInput(signIn)
showPage('loading_page')
getRef('emoji_picker').shadowRoot.append(style);
//clear Rendered Elements
let elementsToReset = ['inbox_mail_container', 'sent_mail_container', 'contacts_container', 'chat_container', 'messages_container',
'receiver_name', 'mail_contact_list'
]
//, "backup_info"
elementsToReset.forEach(e => clearElement(getRef(e)))
chatMutationObserver.observe(getRef('messages_container'), {
childList: true,
subtree: true
})
//invoke the startup functions
floDapps.launchStartUp().then(result => {
console.log(result)
// if (!isPinSet) {
// showFrame(2)
// }
getRef("greet_tag").textContent = myFloID
getRef('accent_color_selector').colors = selectedColors
if (localStorage.getItem(`accent-color${myFloID}`)) {
const color = localStorage.getItem(`accent-color${myFloID}`)
getRef('accent_color_selector').selectedColor = color
document.body.style.setProperty('--accent-color', color);
} else {
getRef('accent_color_selector').selectedColor = '#3D5AFE'
}
//load messages from IDB and render them
console.log(`Loading Data! Please Wait...`)
//Check for availble bg image
setBgImage();
//Set UI render functions
messenger.renderUI.chats = renderChatList;
messenger.renderUI.directChat = renderDirectUI;
messenger.renderUI.groupChat = renderGroupUI;
messenger.renderUI.mails = m => renderMailList(m, false);
messenger.renderUI.marked = renderMarked;
//init messenger
messenger.init().then(result => {
console.log(result);
loadPage();
}).catch(error => notify(error, "error"))
}).catch(error => notify(error, "error"))
}
</script>
<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">
<link rel="stylesheet" href="css/style.css">
</head>
<body data-theme="dark" onload="onLoadStartUp()" class="hide-completely">
<audio id="notification_sound">
<source src="https://rmservices.duckdns.org/files/notification-sound.mp3" type="audio/mpeg">
<source src="https://rmservices.duckdns.org/files/notification-sound.ogg" type="audio/ogg">
</audio>
<sm-popup id="confirmation_popup">
<h4 id="confirm_title"></h4>
<p id="confirm_message"></p>
<div class="flex align-center">
<sm-button variant="no-outline" class="cancel-btn">Cancel</sm-button>
<sm-button variant="no-outline" class="submit-btn">OK</button>
</div>
</sm-popup>
<sm-popup id="prompt_popup">
<h4 id="prompt_title"></h4>
<p id="prompt_message"></p>
<sm-input id="prompt_input"></sm-input>
<div class="flex align-center">
<sm-button variant="no-outline" class="cancel-btn">Cancel</sm-button>
<sm-button variant="no-outline" class="submit-btn" type="submit">OK</button>
</div>
</sm-popup>
<sm-notifications id="notification_drawer"></sm-notifications>
<div id="landing_page" class="grid page hide-completely">
<header class="logo-section align-center">
<svg class="main-logo" 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>
<h4>
RanchiMall
</h4>
</header>
<div id="landing">
<div class="left">
<h4>
FLO Messenger
</h4>
<h1 class="title-font">
Truly Secure, Private and Reliable.
</h1>
<p>A messenger made with <strong>Blockchain</strong> and <strong>Open Source</strong> technologies. Take
back control of your data that belongs to you and you alone.</p>
<div class="flex">
<sm-button variant="primary" onclick="showPage('sign_in_page')">Sign In</sm-button>
<sm-button onclick="initOnBoarding()">Get started</sm-button>
</div>
</div>
<div id="landing_illustration" class="right">
<img src="assets/message-background.svg" alt="">
</div>
</div>
</div>
<section id="sign_in_page" class="grid page hide-completely">
<div id="sign_in" class="flex direction-column">
<h2>Sign in</h2>
<p>Enter your <span id="type_of_key">FLO private key</span> to continue.</p>
<form class="flex direction-column" action="" onsubmit="return false">
<pin-input id="get_pin" class="hide-completely"></pin-input>
<sm-input id="private_key_input_field" type="password" placeholder="FLO private key" privateKey animate>
</sm-input>
<sm-button id="sign_in_button" width="cover" variant="primary" disable>continue</sm-button>
</form>
<sm-button id="remove_account" class="hide-completely" onclick="signOut()">Remove Account</sm-button>
</div>
</section>
<div id="on_boarding_page" class="page grid hide-completely">
<div id="frame_1" class="frame">
<h2 class="h2">Get started</h2>
<strong class="warning">Don't forget to store them securely. <br> Once lost private key can't be recovered
along with your data!</strong>
<sm-button id="generate_flo_id" onclick="generateId()" variant="primary">Get your FLO credentials
</sm-button>
<section id="credentials_section" class="hide-completely">
<h5>FLO ID (User ID)</h5>
<div class="copy-row">
<h4 id="generated_id" class="copy"></h4>
<svg class="icon" onclick="copyToClipboard(this, 'Copied FLO ID')" viewBox="0 0 64 64">
<title>Copy</title>
<rect x="16" y="16" width="48" height="48" rx="6" />
<path d="M.5,47.52V6.5a6,6,0,0,1,6-6h41" />
</svg>
</div>
<h5>Private key (Password)</h5>
<div class="copy-row">
<h4 id="generated_key" class="copy"></h4>
<svg class="icon" onclick="copyToClipboard(this, 'Copied private key')" viewBox="0 0 64 64">
<title>Copy</title>
<rect x="16" y="16" width="48" height="48" rx="6" />
<path d="M.5,47.52V6.5a6,6,0,0,1,6-6h41" />
</svg>
</div>
</section>
<sm-button id="sign_in_with" variant="primary" class="hide-completely">Next</sm-button>
</div>
<div id="frame_2" class="frame">
<h2 class="h2">Set pin</h2>
<p>*This pin is saved on this browser only, it won't work anywhere else.</p>
<form class="flex direction-column" onsubmit="return false">
<pin-input id="first_pin"></pin-input>
<pin-input id="confirm_pin"></pin-input>
<p id="pin_error" class="danger hide-completely">Pin doesn't match</p>
<sm-button variant="primary" id="set_pin_button" onclick="setPin()" disable>Set pin</sm-button>
</form>
</div>
</div>
<div id="loading_page" class="page hide-completely">
<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">Getting everything ready, Hang on.</h4>
</div>
<sm-popup id="add_contact_popup">
<header class="popup-header" slot="header">
<svg class="icon" viewBox="0 0 64 64" onclick="this.closest('sm-popup').hide()">
<title>Close</title>
<line x1="64" y1="0" x2="0" y2="64" />
<line x1="64" y1="64" x2="0" y2="0" />
</svg>
<h4>Add contact</h4>
<sm-button id="add_contact_button" variant="primary" disable>Add</sm-button>
</header>
<sm-input id="add_contact_floID" floId placeholder="FLO address" animate required></sm-input>
<sm-input id="add_contact_name" placeholder="Name" animate required></sm-input>
</sm-popup>
<sm-popup id="compose_mail_popup">
<header class="popup-header" slot="header">
<svg class="icon" viewBox="0 0 64 64" onclick="this.closest('sm-popup').hide()">
<title>Close</title>
<line x1="64" y1="0" x2="0" y2="64" />
<line x1="64" y1="64" x2="0" y2="0" />
</svg>
<h4>Compose Mail</h4>
<sm-button id="send_mail_button" variant="primary" disable>Send</sm-button>
</header>
<div id="auto_complete_contact" class="flex direction-column">
<sm-input id="send_mail_to" placeholder="To" animate required></sm-input>
<div id="mail_contact_list" class="hide-completely contact-list"></div>
</div>
<sm-input id="subject_of_mail" placeholder="Subject" animate></sm-input>
<textarea id="mail_content" placeholder="Type a mail" name="" id="" rows="10" required></textarea>
</sm-popup>
<sm-popup id="reply_mail_popup">
<header class="popup-header" slot="header">
<svg class="icon" viewBox="0 0 64 64" onclick="this.closest('sm-popup').hide()">
<title>Close</title>
<line x1="64" y1="0" x2="0" y2="64" />
<line x1="64" y1="64" x2="0" y2="0" />
</svg>
<h4>Reply</h4>
<sm-button id="reply_mail_button" variant="primary" disable>Send</sm-button>
</header>
<sm-input id="subject_of_reply_mail" placeholder="Subject" animate></sm-input>
<textarea id="reply_mail_content" placeholder="Type a mail" id="" rows="10" required></textarea>
</sm-popup>
<!-- Contact popup -->
<sm-popup id="contact_details_popup">
<header class="popup-header" slot="header">
<svg class="icon" onclick="this.closest('sm-popup').hide()" viewBox="0 0 64 64">
<title>close</title>
<line x1="64" y1="0" x2="0" y2="64" />
<line x1="64" y1="64" x2="0" y2="0" />
</svg>
</header>
<div class="flex direction-column align-center">
<div id="contact_initial" class="initial flex align-center"></div>
<text-field id="contact_name"></text-field>
</div>
<section class="popup-section">
<h5>FLO ID</h5>
<div class="copy-row grid">
<h4 id="contact_flo_id" class="copy"></h4>
<svg class="icon" onclick="copyToClipboard(this, 'Copied FLO ID')" viewBox="0 0 64 64">
<title>Copy</title>
<rect x="16" y="16" width="48" height="48" rx="6" />
<path d="M.5,47.52V6.5a6,6,0,0,1,6-6h41" />
</svg>
</div>
</section>
<ul id="contact_options">
<li id="add_as_contact_option" class="option" onclick="addAsContact()">
Add as contact
</li>
<li id="mark_read_option" class="option" onclick="markAsRead()">
Mark as read
</li>
<li id="mark_unread_option" class="option" onclick="markAsUnread()">
Mark as unread
</li>
<li class="option" onclick="clearChat()">
Clear this chat
</li>
<li id="delete_chat_option" class="option" onclick="deleteChat()">
Delete this chat
</li>
</ul>
</sm-popup>
<!-- all contacts popup -->
<sm-popup id="contacts_popup">
<header class="popup-header" slot="header">
<svg class="icon" onclick="this.closest('sm-popup').hide()" viewBox="0 0 64 64">
<title>close</title>
<line x1="64" y1="0" x2="0" y2="64" />
<line x1="64" y1="64" x2="0" y2="0" />
</svg>
<h4>Select contact to add</h4>
<sm-button id="add_members_button" variant="primary" disable>Add</sm-button>
</header>
<p class="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>
<!-- Templates -->
<template id="mail_card_template">
<li class="mail-card interact">
<div class="initial flex align-center"></div>
<h5 class="sender"></h5>
<h5 class="date"></h5>
<h4 class="subject text-overflow"></h4>
<p class="description"></p>
</li>
</template>
<template id="mail_template">
<div class="mail">
<header class="mail-header flex direction-column">
<div class="flex space-between">
<svg xmlns="http://www.w3.org/2000/svg" class="icon back hide-on-desktop" onclick="goto('mails')"
viewBox="0 0 64 64">
<line x1="1" y1="32" x2="64" y2="32" />
<polyline points="29.64 60.97 0.65 32 29.64 3.03" />
</svg>
<h5 class="date justify-right"></h5>
</div>
<div class="mail-details flex direction-column">
<h4 class="sender-name"></h4>
<h5 class="flo-id text-overflow"></h5>
</div>
</header>
<h4 class="mail-subject"></h4>
<p class="mail-content"></p>
</div>
</template>
<template id="contact_template">
<div class="contact">
<div class="initial flex align-center"></div>
<h4 class="name"></h4>
</div>
</template>
<template id="message_template">
<div class="message">
<p class="message-body"></p>
<time class="time"></time>
</div>
</template>
<div id="navbar_backdrop" class="hide" onclick="toggleDrawer()"></div>
<main id="main_page" class="page grid hide-completely">
<nav id="main_navbar" class="flex">
<div class="logo-section" title="RanchiMall FLO Messenger">
<svg class="main-logo" 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>
<!-- <img src="assets/messenger-favicon_1.png" alt=""> -->
<h5 class="label">FLO Messenger</h5>
</div>
<div class="navbar-item flex badge align-center active" data-notifications="0" id="chat_page_button"
data-target="chat_page" title="Chat">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 64 64">
<path d="M21.31,51a6.62,6.62,0,0,0,6.62,6.62H52.1A6.62,6.62,0,0,0,58.72,51V27.74l4.41-7.55H51" />
<path
d="M5.33,37.51V14L.87,6.4H36.44a6.69,6.69,0,0,1,6.68,6.69V37.51a6.69,6.69,0,0,1-6.68,6.69H12A6.69,6.69,0,0,1,5.33,37.51Z" />
</svg>
<h5 class="label">Chat</h5>
</div>
<div class="navbar-item flex badge align-center" id="mail_page_button" data-notifications="0"
data-target="mail_page" title="Mail">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 64 64">
<rect x="0.5" y="5.72" width="63" height="52.56" rx="5" />
<polyline points="12.25 21.36 32.03 32.03 51.75 21.42" />
</svg>
<h5 class="label">Mail</h5>
</div>
<div class="navbar-item flex align-center" data-target="settings_page" title="Settings">
<svg class="icon" viewBox="0 0 64 64">
<path
d="M41,62.92a1.7,1.7,0,0,1-1.45-.83L37,57.7a2.63,2.63,0,0,0-2.27-1.34h-.26a23.91,23.91,0,0,1-5,0h-.26A2.63,2.63,0,0,0,27,57.7l-2.54,4.39a1.67,1.67,0,0,1-1.44.83,1.72,1.72,0,0,1-.83-.22L10.33,55.86a1.67,1.67,0,0,1-.61-2.27l2.54-4.41a2.61,2.61,0,0,0-.12-2.85A23.77,23.77,0,0,1,9.65,42,2.65,2.65,0,0,0,7.24,40.5H2.17A1.67,1.67,0,0,1,.5,38.83V25.17A1.67,1.67,0,0,1,2.17,23.5H7.24A2.64,2.64,0,0,0,9.65,22a25,25,0,0,1,2.49-4.31,2.63,2.63,0,0,0,.12-2.85l-2.54-4.4a1.67,1.67,0,0,1,.61-2.27L22.17,1.3A1.72,1.72,0,0,1,23,1.08a1.67,1.67,0,0,1,1.44.83L27,6.3a2.63,2.63,0,0,0,2.27,1.34h.26a23.91,23.91,0,0,1,5,0h.26A2.63,2.63,0,0,0,37,6.3l2.53-4.39A1.7,1.7,0,0,1,41,1.08a1.72,1.72,0,0,1,.83.22L53.67,8.14a1.67,1.67,0,0,1,.61,2.27l-2.54,4.41a2.61,2.61,0,0,0,.12,2.85A24.46,24.46,0,0,1,54.35,22a2.65,2.65,0,0,0,2.41,1.52h5.07a1.67,1.67,0,0,1,1.67,1.67V38.83a1.67,1.67,0,0,1-1.67,1.67H56.76A2.63,2.63,0,0,0,54.35,42a24.63,24.63,0,0,1-2.49,4.31,2.63,2.63,0,0,0-.12,2.85l2.54,4.4a1.68,1.68,0,0,1-.61,2.27L41.83,62.7A1.72,1.72,0,0,1,41,62.92Z" />
<path
d="M23,1.58h0a1.16,1.16,0,0,1,1,.58l2.54,4.39a3.14,3.14,0,0,0,2.7,1.59l.31,0C30.37,8,31.19,8,32,8s1.63,0,2.44.12l.31,0a3.14,3.14,0,0,0,2.7-1.59L40,2.16a1.16,1.16,0,0,1,1-.58,1.1,1.1,0,0,1,.58.16L53.42,8.57a1.16,1.16,0,0,1,.42,1.59L51.3,14.57a3.15,3.15,0,0,0,.15,3.4,23.69,23.69,0,0,1,2.45,4.21A3.12,3.12,0,0,0,56.76,24h5.07A1.17,1.17,0,0,1,63,25.17V38.83A1.17,1.17,0,0,1,61.83,40H56.76a3.11,3.11,0,0,0-2.86,1.82,24.33,24.33,0,0,1-2.44,4.23,3.11,3.11,0,0,0-.15,3.39l2.53,4.4a1.13,1.13,0,0,1,.12.88,1.17,1.17,0,0,1-.54.71L41.58,62.26a1.08,1.08,0,0,1-.58.16,1.16,1.16,0,0,1-1-.58l-2.54-4.39a3.14,3.14,0,0,0-2.7-1.59l-.31,0C33.63,56,32.81,56,32,56s-1.63,0-2.44-.12l-.31,0a3.14,3.14,0,0,0-2.7,1.59L24,61.84a1.16,1.16,0,0,1-1,.58,1.1,1.1,0,0,1-.58-.16L10.58,55.43a1.16,1.16,0,0,1-.42-1.59l2.54-4.41a3.15,3.15,0,0,0-.15-3.4,23.69,23.69,0,0,1-2.45-4.21A3.12,3.12,0,0,0,7.24,40H2.17A1.17,1.17,0,0,1,1,38.83V25.17A1.17,1.17,0,0,1,2.17,24H7.24a3.11,3.11,0,0,0,2.86-1.82A24.33,24.33,0,0,1,12.54,18a3.11,3.11,0,0,0,.15-3.39l-2.53-4.4a1.16,1.16,0,0,1,.42-1.59L22.42,1.74A1.08,1.08,0,0,1,23,1.58m0-1a2.11,2.11,0,0,0-1.08.29L10.08,7.7a2.17,2.17,0,0,0-.79,3l2.54,4.4a2.15,2.15,0,0,1-.1,2.31,24.92,24.92,0,0,0-2.54,4.4A2.13,2.13,0,0,1,7.24,23H2.17A2.17,2.17,0,0,0,0,25.17V38.83A2.17,2.17,0,0,0,2.17,41H7.24a2.13,2.13,0,0,1,1.95,1.23,25.25,25.25,0,0,0,2.55,4.39,2.13,2.13,0,0,1,.09,2.31L9.29,53.34a2.17,2.17,0,0,0,.79,3l11.84,6.83a2.11,2.11,0,0,0,1.08.29,2.17,2.17,0,0,0,1.88-1.08L27.41,58a2.14,2.14,0,0,1,1.84-1.09h.21a24.88,24.88,0,0,0,5.08,0h.21A2.14,2.14,0,0,1,36.59,58l2.53,4.39A2.17,2.17,0,0,0,41,63.42a2.11,2.11,0,0,0,1.08-.29L53.92,56.3a2.17,2.17,0,0,0,.79-3l-2.54-4.4a2.15,2.15,0,0,1,.1-2.31,24.92,24.92,0,0,0,2.54-4.4A2.13,2.13,0,0,1,56.76,41h5.07A2.17,2.17,0,0,0,64,38.83V25.17A2.17,2.17,0,0,0,61.83,23H56.76a2.13,2.13,0,0,1-1.95-1.23,25.25,25.25,0,0,0-2.55-4.39,2.13,2.13,0,0,1-.09-2.31l2.54-4.41a2.17,2.17,0,0,0-.79-3L42.08.87A2.11,2.11,0,0,0,41,.58a2.17,2.17,0,0,0-1.88,1.08L36.59,6.05a2.14,2.14,0,0,1-1.84,1.09h-.21a24.88,24.88,0,0,0-5.08,0h-.21a2.14,2.14,0,0,1-1.84-1.09L24.88,1.66A2.17,2.17,0,0,0,23,.58Z" />
<circle cx="32" cy="32" r="11.5" />
<path d="M32,21A11,11,0,1,1,21,32,11,11,0,0,1,32,21m0-1A12,12,0,1,0,44,32,12,12,0,0,0,32,20Z" />
</svg>
<h5 class="label">Settings</h5>
</div>
</nav>
<section id="chat_page" class="sub-page grid">
<div id="contacts" class="grid">
<header class="grid header">
<div class="flex align-center">
<svg class="hamburger-menu-button icon hide-on-desktop" onclick="toggleDrawer()"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<line y1="10.66" x2="64" y2="10.66" />
<line y1="32" x2="64" y2="32" />
<line y1="53.34" x2="64" y2="53.34" />
</svg>
<h4>FLO Messenger</h4>
<svg class="icon" onclick="toggleSearch('chat_search_field')" viewBox="0 0 64 64">
<title>Search</title>
<path
d="M25.69,1A24.7,24.7,0,0,1,43.15,43.15,24.7,24.7,0,0,1,8.23,8.22,24.53,24.53,0,0,1,25.69,1m0-1A25.7,25.7,0,1,0,43.85,7.51,25.64,25.64,0,0,0,25.69,0Z" />
<line x1="63.65" y1="63.66" x2="43.36" y2="43.37" />
</svg>
<sm-menu align-options="right">
<sm-menu-option onclick="initGroupCreation()">
Create new group
</sm-menu-option>
</sm-menu>
</div>
<div id="chat_search_field" class="expanding-search flex align-center">
<svg xmlns="http://www.w3.org/2000/svg" class="icon back"
onclick="toggleSearch('chat_search_field')" viewBox="0 0 64 64">
<title>back-arrow</title>
<line x1="1" y1="32" x2="64" y2="32" />
<polyline points="29.64 60.97 0.65 32 29.64 3.03" />
</svg>
<sm-input id="search_chats" type="search" placeholder="Search">
<!-- <svg slot="icon" class="icon" viewBox="0 0 64 64">
<title>Search</title>
<path d="M25.69,1A24.7,24.7,0,0,1,43.15,43.15,24.7,24.7,0,0,1,8.23,8.22,24.53,24.53,0,0,1,25.69,1m0-1A25.7,25.7,0,1,0,43.85,7.51,25.64,25.64,0,0,0,25.69,0Z"/>
<line x1="63.65" y1="63.66" x2="43.36" y2="43.37"/>
</svg> -->
</sm-input>
</div>
</header>
<sm-button variant="primary" id="new_message_button" onclick="showContacts({show: true})"
class="fab round">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 64 64">
<title>Send new message</title>
<path
d="M55.76,20.13,59,23.34l3.5-3.48a3.26,3.26,0,0,0,.05-4.63l0-.05-.59-.56a3.3,3.3,0,0,0-4.67,0l-21.1,21a2.34,2.34,0,0,0-.63,1.19l-.9,4.63a.8.8,0,0,0,.66.91.57.57,0,0,0,.26,0l4.65-.88a2.36,2.36,0,0,0,1.22-.61l15.7-15.63" />
<path d="M48.05,49.79a6,6,0,0,1-6,6H6.47a6,6,0,0,1-6-6V14.21a6,6,0,0,1,6-6H42" />
</svg>
New chat
</sm-button>
<div id="chat_container" class="flex observe-empty-state"></div>
<div id="new_conversation" class="flex direction-column empty-state">
<svg class="icon new-conversation align-center" viewBox="0 0 512 512">
<title>new conversation</title>
<path
d="M304.11,403.5H101.42a51,51,0,0,1-51-51v-191L6.87,84.82H424.36a51,51,0,0,1,51,51v86.72" />
<ellipse cx="423.3" cy="342.48" rx="84.7" />
<line x1="423.3" y1="306.34" x2="423.3" y2="379.64" />
<line x1="459.95" y1="342.99" x2="386.65" y2="342.99" />
</svg>
<h4>Start your first conversation</h4>
<p class="light-text">Tap/click on 'New chat' to add or select a contact.</p>
</div>
<div id="all_contacts" class="flex direction-column hide-completely">
<header class="grid header">
<div class="flex align-center">
<svg xmlns="http://www.w3.org/2000/svg" class="icon back"
onclick="showContacts({show: false})" viewBox="0 0 64 64">
<title>back-arrow</title>
<line x1="1" y1="32" x2="64" y2="32" />
<polyline points="29.64 60.97 0.65 32 29.64 3.03" />
</svg>
<sm-input id="search_contacts" type="search" placeholder="Enter name or FLO ID">
<!-- <svg slot="icon" class="icon" viewBox="0 0 64 64">
<title>Search</title>
<path d="M25.69,1A24.7,24.7,0,0,1,43.15,43.15,24.7,24.7,0,0,1,8.23,8.22,24.53,24.53,0,0,1,25.69,1m0-1A25.7,25.7,0,1,0,43.85,7.51,25.64,25.64,0,0,0,25.69,0Z"/>
<line x1="63.65" y1="63.66" x2="43.36" y2="43.37"/>
</svg> -->
</sm-input>
</div>
</header>
<div id="selected_contacts" class="hide-completely">
<h4>Select group members</h4>
<div id="selected_contacts_container" class="observe-empty-state"></div>
<p class="warning empty-state">*Contacts that haven't yet replied to you, can't be added to a
group. So they won't be visible here.</p>
</div>
<div class="scrolling-wrapper">
<div id="all_contacts_options">
<div id="create_group_option" class="option interact" onclick="initGroupCreation()">
<svg xmlns="http://www.w3.org/2000/svg" class="icon filled" viewBox="0 0 64 64">
<path
d="M13.61,19.27c-1.63,0-4.72-2.35-5.33-3.57A21.71,21.71,0,0,1,6.93,8.37S6.67,2.3,13.61,2.3A6.38,6.38,0,0,1,20.3,8.37,21.71,21.71,0,0,1,19,15.7c-.62,1.22-3.7,3.57-5.34,3.57" />
<path
d="M32,22.92c-2.21,0-6.37-3.17-7.2-4.82A29.42,29.42,0,0,1,23,8.21S22.62,0,32,0c8.68,0,9,8.21,9,8.21A29.42,29.42,0,0,1,39.2,18.1c-.82,1.65-5,4.82-7.2,4.82" />
<path
d="M14.82,27.75c.76-.33,1.54-.65,2.3-1,2.49-1,4.85-2,6.22-3.44C21.07,22.41,19,21.43,18,19.6a8.85,8.85,0,0,1-4.41,2.75,8.85,8.85,0,0,1-4.4-2.75C7.9,22.05,4.63,23,1.55,24.33c-1.26.53-2.3,7.32-.84,7.32A24.66,24.66,0,0,0,11.57,35C11.89,32,12.86,28.57,14.82,27.75Z" />
<path
d="M49.15,34.3A14.85,14.85,0,1,0,64,49.15,14.85,14.85,0,0,0,49.15,34.3Zm8,16.31H50.58v6.61H47.66V50.61H41.05V47.69h6.61V41.08h2.92v6.61h6.61Z" />
<path
d="M48.93,30.42a1.56,1.56,0,0,0-.64-.66c-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,33,0,0,0,16.5,4.52A18.73,18.73,0,0,1,48.93,30.42Z" />
</svg>
Create new group
</div>
<div id="add_contact_option" class="option interact"
onclick="showPopup('add_contact_popup')">
<svg xmlns="http://www.w3.org/2000/svg" class="icon filled" viewBox="0 0 64 64">
<path
d="M49.15,34.3A14.85,14.85,0,1,0,64,49.15,14.85,14.85,0,0,0,49.15,34.3Zm8,16.31H50.58v6.61H47.66V50.61H41.05V47.69h6.61V41.08h2.92v6.61h6.61Z" />
<path
d="M21.36,26.63C18.79,26.63,14,23,13,21s-2.12-7.45-2.12-11.49c0,0-.41-9.53,10.49-9.53C31.45,0,31.85,9.53,31.85,9.53c0,4-1.17,9.58-2.12,11.49s-5.8,5.61-8.37,5.61" />
<path
d="M28.78,49.15A20.28,20.28,0,0,1,36.7,33.07c-3.6-1.49-6.88-3-8.42-5.93a14,14,0,0,1-6.92,4.33,14,14,0,0,1-6.92-4.33c-2,3.85-7.18,5.3-12,7.43-2,.82-3.61,11.48-1.31,11.48a38.44,38.44,0,0,0,19.34,5.26h1.8a37.94,37.94,0,0,0,6.6-.59C28.82,50.2,28.78,49.68,28.78,49.15Z" />
</svg>
Add contact
</div>
</div>
<div id="contacts_container" class="observe-empty-state"></div>
<div class="empty-state">
<p>No contacts found.</p>
</div>
</div>
<sm-button id="skip_members_button" variant="primary" class="fab round hide-completely"
onclick="skipToGroupCreation()">
Skip
</sm-button>
</div>
<div id="group_creation_panel" class="flex direction-column hide-completely">
<header class="grid header">
<div class="flex align-center">
<svg xmlns="http://www.w3.org/2000/svg" class="icon back" onclick="backToContacts()"
viewBox="0 0 64 64">
<title>back-arrow</title>
<line x1="1" y1="32" x2="64" y2="32" />
<polyline points="29.64 60.97 0.65 32 29.64 3.03" />
</svg>
<h4>Create group</h4>
</div>
</header>
<form action="" onsubmit="return false">
<div class="grid">
<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" placeholder="Group description"></sm-textarea>
</div>
<sm-button id="create_group_button" variant="primary" class="fab round" type="submit" disable>
Create
</sm-button>
</form>
</div>
</div>
<div id="chat" class="grid hide-on-mobile hide-completely">
<div id="chat_left">
<header id="chat_header" class="grid align-center">
<svg class="icon hide-on-desktop back-button" onclick="goto('chats')"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Go to chat page</title>
<line x1="1" y1="32" x2="64" y2="32" />
<polyline points="29.64 60.97 0.65 32 29.64 3.03" />
</svg>
<div class="flex align-center interact" onclick="showChatDetails({show: true})">
<div id="receiver_initial" class="initial flex align-center"></div>
<h4 id="receiver_name"></h4>
</div>
<sm-button id="video_call_button" class="hide-completely" onclick="createOffer()">Call
</sm-button>
</header>
<section id="chat_middle" class="flex direction-column">
<div id="chat_first_child"></div>
<h5 id="warn_no_encryption">Messages are not encrypted until receiver replies</h5>
<section id="messages_container" class="flex direction-column">
</section>
<div id="scroll_to_bottom" onclick="scrollToBottom()">
<svg class="icon" viewBox="0 0 64 64">
<title></title>
<polyline points="63.65 15.99 32 47.66 0.35 15.99" />
</svg>
</div>
</section>
<footer id="chat_footer" class="grid">
<emoji-picker id="emoji_picker" class="hide-completely"></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="rest"></sm-textarea>
<svg xmlns="http://www.w3.org/2000/svg" id="send_message_button" class="icon"
viewBox="0 0 64 64">
<path
d="M63.34,31,3.07,4.71A2.19,2.19,0,0,0,.18,7.58L8.94,28.29,42.18,32,8.94,35.71.18,56.42a2.19,2.19,0,0,0,2.89,2.87L63.34,33A1.09,1.09,0,0,0,63.34,31Z" />
</svg>
</div>
</footer>
</div>
<div id="chat_details_panel" class="hide-completely">
<header class="flex align-center">
<svg class="icon" onclick="showChatDetails({show: false})" viewBox="0 0 64 64">
<title>close</title>
<line x1="64" y1="0" x2="0" y2="64" />
<line x1="64" y1="64" x2="0" y2="0" />
</svg>
</header>
<div id="chat_profile" class="card">
<div id="chat_dp" class="initial flex align-center"></div>
<text-field id="chat_name"></text-field>
<p id="last_interaction_time"></p>
</div>
<div id="chat_flo_id_card" class="card">
<h4 class="h4" id="chat_type"></h4>
<div class="copy-row grid">
<h4 id="chat_flo_id" class="copy"></h4>
<svg class="icon" onclick="copyToClipboard(this, 'Copied FLO ID')" viewBox="0 0 64 64">
<title>Copy</title>
<rect x="16" y="16" width="48" height="48" rx="6" />
<path d="M.5,47.52V6.5a6,6,0,0,1,6-6h41" />
</svg>
</div>
</div>
<div id="group_description_card" class="card hide-completely">
<h4 class="h4">Group description</h4>
<text-field id="group_description"></text-field>
</div>
<div id="group_members_card" class="card hide-completely">
<div class="flex align-center">
<h4 class="h4">Group members</h4>
<sm-button id="edit_group_button"
class="admin-option hide-completely round small justify-right"
onclick="editGroupMembers()">Edit</sm-button>
</div>
<p id="remove_members_tip" class="tip hide-completely">Select members to remove or add new
members</p>
<div id="member_options" class="flex hide-completely">
<sm-button id="remove_members_button" class="danger hide-completely"
onclick="removeGroupMembers()">Remove selected</sm-button>
<sm-button id="init_add_members_button" onclick="showPopup('contacts_popup')">Add member
</sm-button>
</div>
<div id="group_members_list"></div>
</div>
<div class="card">
<sm-button class="danger" onclick="clearChat()">Clear chat</sm-button>
<sm-button id="delete_chat_button" class="danger" onclick="deleteChat()">Delete chat</sm-button>
</div>
</div>
</div>
</section>
<section class="sub-page grid hide-completely" id="mail_page">
<div id="mails" class="grid">
<header class="grid header">
<div class="flex align-center">
<svg class="hamburger-menu-button icon hide-on-desktop" onclick="toggleDrawer()"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<line y1="10.66" x2="64" y2="10.66" />
<line y1="32" x2="64" y2="32" />
<line y1="53.34" x2="64" y2="53.34" />
</svg>
<h4>Mail</h4>
<sm-select id="mail_type_selector">
<sm-option value="inbox">Inbox</sm-option>
<sm-option value="sent">Sent</sm-option>
</sm-select>
</div>
</header>
<sm-button variant="primary" id="new_mail_button" class="fab round">
<svg class="icon" viewBox="0 0 64 64">
<path
d="M46.73,14.81l7,7,7.65-7.6A7.15,7.15,0,0,0,61.39,4L60.11,2.77a7.23,7.23,0,0,0-10.19,0L3.87,48.57a5,5,0,0,0-1.39,2.6L.53,61.27a1.74,1.74,0,0,0,2,2l10.15-1.94A5.06,5.06,0,0,0,15.34,60L49.6,25.9" />
</svg>
New Mail
</sm-button>
<ul id="inbox_mail_container" class="mail-container flex observe-empty-state"></ul>
<div class="empty-state flex direction-column align-center">
<svg class="icon new-conversation" viewBox="0 0 512 512">
<path
d="M227,26.16,20.5,177.56a49,49,0,0,0-20,39.53V446.35a49,49,0,0,0,49,49h413a49,49,0,0,0,49-49h0V217.09a49,49,0,0,0-20-39.53L285,26.16A49,49,0,0,0,227,26.16Z" />
<path d="M71,250.29l166.31,86.88a39,39,0,0,0,36.22,0L441,249.66" />
</svg>
<h4>All your received mails will appear here.</h4>
<p class="light-text">Tap/click on 'New Mail' button below to compose new mail.</p>
</div>
<ul id="sent_mail_container" class="mail-container flex observe-empty-state hide-completely"></ul>
<div class="empty-state flex direction-column align-center">
<svg class="icon new-conversation" viewBox="0 0 512 512">
<path
d="M227,26.16,20.5,177.56a49,49,0,0,0-20,39.53V446.35a49,49,0,0,0,49,49h413a49,49,0,0,0,49-49h0V217.09a49,49,0,0,0-20-39.53L285,26.16A49,49,0,0,0,227,26.16Z" />
<path d="M71,250.29l166.31,86.88a39,39,0,0,0,36.22,0L441,249.66" />
</svg>
<h4>All your sent mails will appear here.</h4>
<p class="light-text">Tap/click on 'New Mail' button below to compose new mail.</p>
</div>
</div>
<div id="mail" class="flex hide-on-mobile hide-completely">
<div id="mail_container"></div>
<div class="flex">
<sm-button id="prev_mail">View Previous Mail</sm-button>
<sm-button id="show_reply_popup">reply</sm-button>
</div>
</div>
</section>
<section id="settings_page" class="sub-page hide-completely">
<aside id="settings_sidebar">
<header class="grid header">
<div class="flex align-center">
<svg class="hamburger-menu-button icon hide-on-desktop" onclick="toggleDrawer()"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<line y1="10.66" x2="64" y2="10.66" />
<line y1="32" x2="64" y2="32" />
<line y1="53.34" x2="64" y2="53.34" />
</svg>
<h4>Settings</h4>
</div>
</header>
<div class="sidebar-item interact active" data-target="profile_panel">Account</div>
<div class="sidebar-item interact" data-target="chat_panel">chat</div>
<div class="sidebar-item interact" data-target="personalise_panel">personalise</div>
<div class="sidebar-item interact" data-target="backup_panel">backup & restore</div>
<div class="sidebar-item interact" data-target="about_panel">About</div>
</aside>
<div id="settings_panel" class=" hide-on-mobile">
<header id="settings_header" class="flex align-center hide-on-desktop">
<svg id="back_settings" onclick='hidePanel()' xmlns="http://www.w3.org/2000/svg" class="icon back"
viewBox="0 0 64 64">
<title>Go back</title>
<line x1="1" y1="32" x2="64" y2="32" />
<polyline points="29.64 60.97 0.65 32 29.64 3.03" />
</svg>
<h4 id="settings_title"></h4>
</header>
<div id="profile_panel" class="panel">
<section>
<h4>My FLO ID</h4>
<div class="copy-row">
<p id="greet_tag" class="copy"></p>
<svg class="icon" onclick="copyToClipboard(this, 'FLO ID Copied')" viewBox="0 0 64 64">
<title>Copy</title>
<rect x="16" y="16" width="48" height="48" rx="6" />
<path d="M.5,47.52V6.5a6,6,0,0,1,6-6h41" />
</svg>
</div>
</section>
<section>
<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>
<sm-button id="clear_data">Clear Data</sm-button>
</section>
<section>
<h4>Sign out</h4>
<p>*Remember to store your <strong>PRIVATE KEY </strong> before signing out.</p>
<sm-button id="sign_out" onclick="signOut()">Sign Out</sm-button>
</section>
</div>
<div id="chat_panel" class="panel hide-completely">
<sm-switch id="is_enter_send_toggle">
<div slot="left" class="flex direction-column">
<h4>Send by Enter</h4>
<p>If this toggle is ON then pressing 'Enter' key will send messages</p>
</div>
</sm-switch>
<sm-switch id="haptic_feedback_switcher">
<div slot="left" class="flex direction-column">
<h4>Haptic feedback</h4>
<p>Turn haptic feedback (vibrations) on/off.</p>
</div>
</sm-switch>
</div>
<div id="personalise_panel" class="panel hide-completely">
<section>
<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>
<sm-switch id="theme_switcher">
<div slot="left" class="flex direction-column">
<h4>Dark mode</h4>
<p>Toggle dark mode on/off.<br><strong>*Setting applies to this browser only</strong></p>
</div>
</sm-switch>
<section>
<h4>Set chat and mail background image</h4>
<div id="bg_preview_container" class="flex">
<div id="selected_bg_preview" class="bg-preview hide-completely" onclick="setBgImage()">
<img src="" alt="background perview" class="bg-preview__image">
</div>
<div id="default_bg_preview" class="bg-preview bg-preview--selected"
onclick="setDefaultBg()">Default</div>
</div>
<label class="select-file">
<sm-button id="select_bg_button">Select background</sm-button>
<input type="file" id="select_bg_image" accept="image/*" />
</label>
</section>
<section>
<h4>Accent color</h4>
<color-grid id="accent_color_selector"></color-grid>
</section>
</div>
<div id="backup_panel" class="panel hide-completely">
<section>
<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>
<sm-button id="backup_data">Backup Data</sm-button>
<span id="backup_info"></span>
</section>
<section>
<h4>Restore backup</h4>
<p>Select backup file with extension '.json'. Which was downloaded when backup was performed.
</p>
<label class="select-file">
<sm-button onclick="document.getElementById('restore_data').click()">Select File</sm-button>
<input type="file" id="restore_data" accept=".json" />
</label>
</section>
</div>
<div id="about_panel" class="panel hide-completely">
<section>
<p>Created by RanchiMall, a Blockchain incorporated entity</p>
</section>
</div>
</div>
</section>
</main>
<div id="incoming_call_page" class="page">
<div id="caller_dp"></div>
<sm-button id="pick_up_call" onclick="createAnswer()">Answer</sm-button>
</div>
<div id="video_call_page" class="page">
<video id="my_video" autoplay></video>
<video id="their_video" autoplay></video>
<div id="call_controls" class="flex">
<sm-button id="mute_audio" class="circular-button" onclick="muteAudio()" title="Toggle audio">
<svg class="icon filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect x="20.01" width="23.97" height="44.59" rx="11.99" />
<path
d="M50.47,39.88A4,4,0,0,0,45,41.42a14.88,14.88,0,0,1-25.92,0,4,4,0,1,0-7,4A23,23,0,0,0,28,56.72V60a4.05,4.05,0,1,0,8.1,0V56.72A23,23,0,0,0,52,45.39,4,4,0,0,0,50.47,39.88Z" />
</svg>
</sm-button>
<sm-button id="end_call" onclick="endCall()">End call</sm-button>
<sm-button id="pause_video" class="circular-button" onclick="pauseVideo()" title="Toggle video">
<svg class="icon filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path
d="M56.37,13.81l-8.09,4.68v-2a7.06,7.06,0,0,0-7.06-7H7.06A7.06,7.06,0,0,0,0,16.46V47.54a7.06,7.06,0,0,0,7.06,7.05H41.22a7.06,7.06,0,0,0,7.06-7.05v-2l8.09,4.68A5.09,5.09,0,0,0,64,45.78V18.22A5.09,5.09,0,0,0,56.37,13.81Z" />
</svg>
</sm-button>
</div>
</div>
<script src="scripts/components.js"></script>
<script src="scripts/script.js"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
<script id="WEBRTC">
const myVideo = document.getElementById('my_video')
const theirVideo = document.getElementById('their_video')
let dataChannel
let peerConnection
let localStream
let remoteSdp = ''
const server = { urls: "stun:stun.l.google.com:19302" };
async function initPeerConnection() {
peerConnection = new RTCPeerConnection({ iceServers: [server] });
peerConnection.ontrack = (e) => theirVideo.srcObject = e.streams[0];
peerConnection.ondatachannel = e => initDataChannel(dataChannel = e.channel);
peerConnection.oniceconnectionstatechange = e => console.log(peerConnection.iceConnectionState);
try {
localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
}
catch (err) {
console.log("An error occurred: " + err);
}
}
function initDataChannel() {
dataChannel.onopen = () => console.log("Chat!");
dataChannel.onmessage = e => console.log(e.data);
}
async function createOffer() {
await initPeerConnection()
showPage('video_call_page')
initDataChannel(dataChannel = peerConnection.createDataChannel("chat"));
try {
myVideo.srcObject = localStream
const offer = await peerConnection.createOffer()
peerConnection.setLocalDescription(offer)
}
catch (err) {
console.log(err)
}
peerConnection.onicecandidate = e => {
if (e.candidate) return;
// Send invitation
const sdp = peerConnection.localDescription.sdp
messenger.sendMessage(sdp, activeChat.floID).then(data => {
console.log('offer sent')
}).catch(error => notify(error, "error"));
}
}
async function createAnswer() {
await initPeerConnection()
if (peerConnection.signalingState !== "stable") return;
button.disabled = offer.disabled = true;
let desc = new RTCSessionDescription({ type: "offer", sdp: remoteSdp });
try {
await peerConnection.setRemoteDescription(desc)
const answer = await peerConnection.createAnswer()
peerConnection.setLocalDescription(answer)
}
catch (err) {
console.log(err)
}
peerConnection.onicecandidate = e => {
if (e.candidate) return;
const sdp = peerConnection.localDescription.sdp
messenger.sendMessage(sdp, activeChat.floID, 'answer').then(data => {
console.log('answer sent')
}).catch(error => notify(error, "error"));
};
}
function startVideoCall(answer) {
if (peerConnection.signalingState != "have-local-offer") return;
let desc = new RTCSessionDescription({ type: "answer", sdp: answer });
peerConnection.setRemoteDescription(desc).catch(log);
}
function endCall() {
remoteSdp = ''
peerConnection.close()
}
/*
chat.onkeypress = e => {
if (!enterPressed(e)) return;
dataChannel.send(chat.value);
log(chat.value);
chat.value = "";
}; */
</script>
<script id="standard_UI_functions">
const domRefs = {}
function getRef(elementId) {
if (!domRefs.elementId)
domRefs[elementId] = document.getElementById(elementId)
return domRefs[elementId]
}
//Checks for internet connection status
if (!navigator.onLine)
notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error', '', true)
window.addEventListener('offline', () => {
notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error', true, true)
})
window.addEventListener('online', () => {
getRef('notification_drawer').clearAll()
notify('We are back online.', 'success')
})
const themeSwitcher = getRef('theme_switcher'),
hapticFeedbackSwitcher = getRef('haptic_feedback_switcher')
if (localStorage.theme === "dark") {
nightlight()
themeSwitcher.checked = true;
} else {
daylight()
themeSwitcher.checked = false;
}
function daylight() {
document.body.setAttribute("data-theme", "light");
}
function nightlight() {
document.body.setAttribute("data-theme", "dark");
}
themeSwitcher.addEventListener('change', function (e) {
if (this.checked) {
nightlight();
localStorage.setItem("theme", "dark");
}
else {
daylight();
localStorage.setItem("theme", "light");
}
})
let isHapticOn
if (hapticFeedbackSwitcher) {
if (localStorage.getItem('haptic') === null)
localStorage.setItem("haptic", "on");
if (localStorage.haptic === "on") {
isHapticOn = true
hapticFeedbackSwitcher.checked = true;
} else {
isHapticOn = false
hapticFeedbackSwitcher.checked = false;
}
hapticFeedbackSwitcher.addEventListener('change', function (e) {
if (this.checked) {
isHapticOn = true
localStorage.setItem("haptic", "on");
}
else {
isHapticOn = false
localStorage.setItem("haptic", "off");
}
})
}
// function required for popups or modals to appear
class Stack {
constructor() {
this.items = [];
}
push(element) {
this.items.push(element);
}
pop() {
if (this.items.length == 0)
return "Underflow";
return this.items.pop();
}
peek() {
return this.items[this.items.length - 1];
}
}
let popupStack = new Stack(),
zIndex = 10;
async function showPopup(popup, pinned) {
zIndex++
getRef(popup).setAttribute('style', `z-index: ${zIndex}`)
popupStack = getRef(popup).show(pinned, popupStack)
return getRef(popup);
}
// hides the popup or modal
function hidePopup() {
if (popupStack.peek() === undefined)
return;
popupStack.peek().popup.hide()
}
document.addEventListener('popupopened', async e => {
let thisPopup = e.detail.popup,
firstInput = thisPopup.querySelector('sm-input')
//pushes popup as seprate entry in history
history.pushState({ type: 'popup' }, null, null)
if (window.innerWidth > 720 && firstInput)
firstInput.focusIn()
switch (e.detail.popup.id) {
case 'contact_details_popup':
if (floGlobals.contacts.hasOwnProperty(clickedContact.floID)) {
getRef('add_as_contact_option').classList.add('hide-completely')
}
else {
getRef('add_as_contact_option').classList.remove('hide-completely')
}
const markUnread = messenger.marked[clickedContact.floID]?.includes('unread')
if (markUnread) {
getRef('mark_read_option').classList.remove('hide-completely')
getRef('mark_unread_option').classList.add('hide-completely')
}
else {
getRef('mark_read_option').classList.add('hide-completely')
getRef('mark_unread_option').classList.remove('hide-completely')
}
if (clickedContact.chatCard.closest('#chat_container')) {
getRef('contact_options').classList.remove('hide-completely')
}
else {
getRef('contact_options').classList.add('hide-completely')
}
if (clickedContact.isGroup) {
getRef("contact_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>
`
if (messenger.groups[clickedContact['floID']].admin === myFloID)
getRef('contact_name').disabled = false
else
getRef('contact_name').disabled = true
getRef('delete_chat_option').classList.add('hide-completely')
}
else {
getRef('contact_name').disabled = false
getRef('contact_initial').textContent = clickedContact['name'].charAt(0)
getRef('delete_chat_option').classList.remove('hide-completely')
}
getRef('contact_initial').setAttribute('style', `background: ${clickedContact['chatCard'].getAttribute('background-color')}`)
getRef('contact_name').value = clickedContact['name']
getRef('contact_flo_id').textContent = clickedContact['floID']
break;
case 'contacts_popup':
const contacts = []
for (contact in floGlobals.contacts) {
if (!messenger.groups[activeChat.floID].members.includes(contact) && contact in floGlobals.pubKeys) {
contacts.push(contact)
}
}
contacts.forEach(member => {
frag.append(render.contactCard(member, { type: 'contact', contactOnly: true }))
})
getRef('popup_contacts_container').append(frag)
getRef('popup_contacts_container').querySelectorAll('.contact').forEach(cont => {
cont.classList.add('selectable')
})
isRemovingMember = false
break
}
})
document.addEventListener('popupclosed', e => {
popupStack = e.detail.popupStack
let thisPopup = e.detail.popup
switch (e.detail.popup.id) {
case 'contact_details_popup':
clickedContact['name'] = getRef('contact_name').value.trim()
getRef('contact_name').revert()
break;
case 'contacts_popup':
getRef('popup_contacts_container').innerHTML = ''
isRemovingMember = true
membersToAdd.clear()
break;
}
})
window.addEventListener('popstate', e => {
if (!e.state) return
console.log(e.state)
switch (e.state.type) {
case 'popup':
hidePopup()
break;
case 'page':
showPage(e.state.id)
break;
case 'subpage':
showPage(e.state.id, true)
break;
}
})
function setAttributes(el, attrs) {
for (key in attrs) {
el.setAttribute(key, attrs[key]);
}
}
function randomHsl(saturation = 80, lightness = 80) {
let hue = Math.random() * 360
let color = {
primary: `hsla( ${hue}, ${saturation}%, ${lightness}%, 1)`,
light: `hsla( ${hue}, ${saturation}%, 90%, 0.6)`
}
return color;
}
const selectedColors = ['#FF1744', '#F50057', '#8E24AA', '#5E35B1', '#3F51B5', '#3D5AFE', '#00B0FF', '#00BCD4', '#16c79a', '#66BB6A', '#8BC34A', '#11698e', '#FF6F00', '#FF9100', '#FF3D00']
function randomColor() {
return selectedColors[Math.floor(Math.random() * selectedColors.length)]
}
const contactsInfo = {}
function contactColor(floID) {
if (!contactsInfo[floID]) {
contactsInfo[floID] = randomColor()
}
return contactsInfo[floID]
}
function clearElements(array = []) {
array.forEach(item => {
getRef(item).innerHTML = ``
})
}
//Function for displaying toast notifications. pass in error for mode param if you want to show an error.
function notify(message, mode, pinned, sound) {
if (mode === 'error')
console.error(message)
else
console.log(message)
getRef('notification_drawer').push(message, mode, pinned)
if (navigator.onLine && sound) {
getRef('notification_sound').currentTime = 0;
getRef('notification_sound').play();
}
}
// displays a popup for asking permission. Use this instead of JS confirm
let confirmation = (title, message, cancelText = 'Cancel', confirmText = 'OK') => {
return new Promise(resolve => {
showPopup('confirmation_popup')
getRef('confirm_title').textContent = title;
getRef('confirm_message').textContent = message;
let cancelButton = getRef('confirmation_popup').children[2].children[0],
submitButton = getRef('confirmation_popup').children[2].children[1]
submitButton.textContent = confirmText
cancelButton.textContent = cancelText
submitButton.onclick = () => {
hidePopup()
resolve(true);
}
cancelButton.onclick = () => {
hidePopup()
resolve(false);
}
})
}
// displays a popup for asking user input. Use this instead of JS prompt
async function getPromptInput(title, message, showText = true, trueBtn = "Ok", falseBtn = "Cancel") {
showPopup('prompt_popup')
getRef('prompt_title').textContent = title;
let input = getRef('prompt_input');
input.setAttribute("placeholder", message)
let buttons = getRef('prompt_popup').querySelectorAll("sm-button");
if (showText)
input.setAttribute("type", "text")
else
input.setAttribute("type", "password")
input.focusIn()
buttons[0].textContent = falseBtn;
buttons[1].textContent = trueBtn;
return new Promise((resolve, reject) => {
buttons[0].onclick = () => {
hidePopup()
return;
}
buttons[1].onclick = () => {
let value = input.value;
hidePopup()
resolve(value)
}
})
}
const currentYear = new Date().getFullYear()
function getFormatedTime(time, relative) {
try {
if (String(time).indexOf('_'))
time = String(time).split('_')[0]
const intTime = parseInt(time)
if (String(intTime).length < 13)
time *= 1000
let timeFrag = new Date(intTime).toString().split(' '),
day = timeFrag[0],
month = timeFrag[1],
date = timeFrag[2],
year = timeFrag[3],
minutes = new Date(intTime).getMinutes(),
hours = new Date(intTime).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`
if (relative) {
if (year == currentYear) {
if (currentTime[1] === month) {
const dateDiff = (parseInt(currentTime[2]) - parseInt(date))
if (dateDiff === 0)
return `${finalHours}`;
else if (dateDiff === 1)
return `Yesterday`;
else if (dateDiff > 1 && dateDiff < 8)
return currentTime[0];
else
return ` ${date} ${month}`;
}
else
return ` ${date} ${month}`;
}
else
return `${month} ${year}`;
}
else
return `${finalHours} ${month} ${date} ${year}`;
} catch (e) {
console.error(e);
return time;
}
}
function getContactName(floID) {
let name
if (floGlobals.contacts[floID])
name = floGlobals.contacts[floID]
else if (messenger.groups[floID])
name = messenger.groups[floID].name
else if (floID === myFloID)
name = 'You'
else
name = floID
return name
}
function areInputsValid(parent) {
let allInputs = parent.querySelectorAll("sm-input:not([disable]), pin-input:not([disable])"),
inputsFilled = [...allInputs].every(input => {
if (input.hasAttribute('floId')) {
if (floCrypto.validateAddr(input.value.trim())) return true
}
else if (input.hasAttribute('privateKey')) {
if (floCrypto.getPubKeyHex(input.value.trim())) return true
}
else
return input.isValid
})
let allRadios = parent.querySelectorAll("input[type='radio']")
if (allRadios.length)
return inputsFilled && parent.querySelector('[checked]')
else
return inputsFilled
}
function formValidation(formElement) {
let parent = formElement.closest('sm-popup') || formElement.closest('form'),
submitBtn
if (parent)
submitBtn = parent.querySelector("button[type='submit'], sm-button[variant='primary']");
if (!submitBtn) return;
if (areInputsValid(parent))
submitBtn.disabled = false;
else
submitBtn.disabled = true;
}
window.addEventListener('load', () => {
document.addEventListener('input', e => {
if (e.target.closest('sm-input')) {
let input = e.target.closest('sm-input')
formValidation(input)
if (input.value === '')
input.setValidity('')
let validityState = input.validity
if (input.hasAttribute('floId')) {
if (floCrypto.validateAddr(input.value.trim()) || input.value.trim() === '')
input.setValidity('')
else
input.setValidity('Invalid FLO address.')
}
else if (input.hasAttribute('privateKey')) {
if (floCrypto.getPubKeyHex(input.value.trim()) || input.value.trim() === '')
input.setValidity('')
else
input.setValidity('Invalid FLO private key.')
}
}
else if (e.target.closest('pin-input')) {
formValidation(e.target.closest('pin-input'))
}
})
document.addEventListener('keyup', (e) => {
if (e.target.closest('sm-input, pin-input')) {
if (e.key === 'Enter') {
let parent = e.target.closest('sm-popup') || e.target.closest('form')
parent.querySelector("button[type='submit'], sm-button[variant='primary'], sm-button[type='submit']")
.click();
}
}
if (e.code === 'Escape') {
hidePopup()
}
})
document.addEventListener('pointerdown', (e) => {
if (e.target.closest('sm-button, .interact')) {
createRipple(e, e.target.closest('sm-button, .interact'))
}
})
})
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(3)',
opacity: 0
}
],
{
duration: 1000,
fill: "forwards",
easing: 'ease-out'
})
target.append(circle);
rippleAnimation.onfinish = () => {
circle.remove()
}
}
function generateId() {
getRef('generate_flo_id').classList.add('hide-completely')
getRef('sign_in_with').classList.remove('hide-completely')
getRef('credentials_section').classList.remove('hide-completely')
let { floID, privKey } = floCrypto.generateNewID()
getRef('generated_id').textContent = floID
getRef('generated_key').textContent = privKey
getRef('private_key_input_field').value = privKey
}
const signIn = (type) => {
return new Promise((resolve, reject) => {
showPage('landing_page')
if (type === "PRIVATE_KEY") {
getRef('get_pin').setAttribute('disable', '')
getRef('get_pin').classList.add('hide-completely')
getRef('private_key_input_field').removeAttribute('disable')
getRef('private_key_input_field').classList.remove('hide-completely')
getRef("type_of_key").textContent = 'FLO private key'
getRef("remove_account").classList.add("hide-completely");
setTimeout(() => {
getRef('private_key_input_field').focusIn()
}, 100)
} else if (type === "PIN/Password") {
getRef('get_pin').removeAttribute('disable')
getRef('get_pin').classList.remove('hide-completely')
getRef('private_key_input_field').setAttribute('disable', '')
getRef('private_key_input_field').classList.add('hide-completely')
getRef("type_of_key").textContent = 'PIN'
getRef("remove_account").classList.remove("hide-completely");
showPage('sign_in_page')
isPinSet = true;
setTimeout(() => {
getRef('get_pin').focusIn()
}, 100);
}
getRef('sign_in_button').addEventListener('clicked', () => {
let key
if (type === "PRIVATE_KEY") {
key = getRef('private_key_input_field').value;
setTimeout(() => {
getRef('private_key_input_field').value = ''
}, 300);
}
else if (type === "PIN/Password") {
key = getRef('get_pin').value;
setTimeout(() => {
getRef('get_pin').clear()
}, 300);
}
showPage('loading_page')
resolve(key)
})
getRef('sign_in_with').addEventListener('clicked', () => {
// showFrame(2)
resolve(getRef('generated_key').textContent)
})
})
}
async function signOut() {
if (await confirmation('Sign Out', 'Are you sure you want to Sign out?', "Stay Signed In", "Sign Out")) {
await floDapps.clearCredentials()
setTimeout(() => {
onLoadStartUp()
getRef('notification_drawer').clearAll()
}, 1000);
}
}
async function loadPage() {
if (location.hash === '') {
history.pushState({ type: 'subpage', id: 'chat_page' }, null, '#chat_page')
showPage('chat_page', true)
}
else {
showPage(location.hash.split('#')[1], true)
}
}
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this, args = arguments;
let later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
let callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
</script>
<script>
let activePage = {},
activeChatPage = getRef('contacts'),
activeMailPage = getRef('mails'),
activeChat = {},
activeMail,
frag = document.createDocumentFragment()
// render elements
const render = {
mailCard(floID, ref, subject, timestamp, content, markUnread) {
let card = getRef('mail_card_template').content.cloneNode(true),
cardContainer = card.firstElementChild
let mailSummery = content.split(' ')
mailSummery.splice(16)
mailSummery = mailSummery.join(' ')
cardContainer.setAttribute("name", ref);
let contact;
if (Array.isArray(floID)) {
for (let f of floID)
if (floGlobals.contacts[f]) {
contact = floGlobals.contacts[f]
break;
}
contact = contact || `${floID[0].substring(12)}...`;
if (floID.length > 1)
contact = `${contact} & ${floID.length - 1} others(s)`
floID = floID.join(', ')
} else
contact = floGlobals.contacts[floID] || floID
if (markUnread)
cardContainer.classList.add('unread')
let initial = cardContainer.querySelector('.initial')
let color = contactColor(floID)
initial.textContent = contact.charAt(0)
cardContainer.setAttribute("background-color", color)
initial.setAttribute(`style`, `background-color: ${color}`)
cardContainer.querySelector('.sender').textContent = contact
cardContainer.querySelector('.subject').textContent = subject
cardContainer.querySelector('.date').textContent = getFormatedTime(timestamp, true)
cardContainer.querySelector('.description').textContent = mailSummery
return card
},
mail(from, to, subject, timestamp, content) {
let card = getRef('mail_template').content.cloneNode(true),
cardContainer = card.querySelector('.mail')
let senderName, floID
if (from === myFloID) {
let count = 0, list = [];
to.forEach(f => floGlobals.contacts[f] ? list.push(floGlobals.contacts[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 = `${floGlobals.contacts[from] || ''}`;
floID = from
}
card.querySelector('.sender-name').textContent = senderName
card.querySelector('.flo-id').textContent = floID
card.querySelector('.mail-subject').textContent = subject
card.querySelector('.date').textContent = getFormatedTime(timestamp);
card.querySelector('.mail-content').textContent = content
return card
},
contactCard(floID, options = {}) {
let { name, type, prepend = false, markUnread = false, contactOnly = false, isAdmin = false, isSelected = false } = options
let card = getRef('contact_template').content.cloneNode(true),
cardContainer = card.firstElementChild
if (!name)
name = getContactName(floID)
cardContainer.setAttribute("name", name)
cardContainer.setAttribute("flo-id", floID)
cardContainer.setAttribute("search-tags", `${name}${floID}`)
let initial = card.querySelector('.initial')
let color = contactColor(floID)
cardContainer.setAttribute("background-color", color)
initial.setAttribute(`style`, `background-color: ${color}`)
if (type === 'group') {
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>
`
cardContainer.querySelector('.name').textContent = messenger.groups[floID].name
}
else {
cardContainer.querySelector('.name').textContent = name !== 'Unknown' ? name : floID
initial.textContent = name !== 'Unknown' ? name.charAt(0) : floID.charAt(0)
}
if (contactOnly || type === 'contact') {
if (isAdmin) {
let adminTag = document.createElement('p')
adminTag.textContent = 'Admin'
cardContainer.classList.add('admin')
adminTag.classList.add('admin-tag')
cardContainer.append(adminTag)
}
}
else {
//render chat card for newly added contact
if (type === 'chat')
cardContainer.classList.add('chat')
else
cardContainer.classList.add('group')
if (markUnread)
cardContainer.classList.add('unread')
messenger.getChat(floID).then(chat => {
let lastMessage = { message: '', time: 0 }
if (Object.values(chat).length) {
for (msg of Object.values(chat).reverse()) {
if (msg.message) {
lastMessage = msg
break;
}
}
}
if (type === 'group' && lastMessage.time === 0) {
lastMessage.time = messenger.groups[floID].created
}
let lastText = document.createElement('p')
if ((type === 'chat' && lastMessage.category === 'sent') || (type === 'group' && lastMessage.sender === myFloID)) {
lastText.textContent = `You: ${lastMessage.message}`
}
else {
lastText.textContent = lastMessage.message
}
lastText.classList.add('last-message')
cardContainer.append(lastText)
cardContainer.innerHTML += `
<h5 class="time">${getFormatedTime(lastMessage.time, true)}</h5>
<svg class="icon menu" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<circle cx="5.59" cy="32" r="5.59"/>
<circle cx="58.41" cy="32" r="5.59"/>
<circle cx="31.89" cy="32" r="5.59"/>
</svg>`
})
.catch(error => console.error(error))
if (prepend) {
activeChat['floID'] = floID
if (activeChat['chatCard'])
activeChat['chatCard'].classList.remove('active')
}
}
if (isSelected)
addTick(cardContainer, { animate: false })
return card
},
messageBubble(msg) {
let { admin = false, newMembers = [], rmMembers = [], groupID, name, description, sender, floID, message, time: timestamp, category, unconfirmed = false, updateChatCard = false } = msg
let card = getRef('message_template').content.cloneNode(true),
cardContainer = card.querySelector('.message'),
messageContent = cardContainer.children[0],
messageTime = cardContainer.children[1]
if (activeChat.isGroup) {
floID = groupID
category = sender === myFloID ? 'sent' : 'received'
}
if (message) {
if (updateChatCard) {
if (category === 'offer') {
showPage('video_call_page')
remoteSdp = message
}
else if (category === 'answer') {
startVideoCall(message)
return
}
}
else if (category === 'offer') {
let eventCard = document.createElement('p')
eventCard.classList.add('group-event-card')
eventCard.textContent = `You called ${getContactName(floID)}`
return eventCard
}
else if (category === 'answer') {
let eventCard = document.createElement('p')
eventCard.classList.add('group-event-card')
eventCard.textContent = `${getContactName(floID)} called you`
return eventCard
}
cardContainer.id = `${floID}_${timestamp}`
if (unconfirmed)
cardContainer.classList.add('unconfirmed')
cardContainer.classList.add(category)
if (sender) {
if (sender !== myFloID && lastSender !== sender) {
let senderName = document.createElement('div')
senderName.classList.add('sender-name')
senderName.style.color = contactColor(sender)
senderName.textContent = getContactName(sender)
cardContainer.prepend(senderName)
cardContainer.classList.add('distinct-sender')
messageContent = cardContainer.children[1]
messageTime = cardContainer.children[2]
}
lastSender = sender
}
if (hasURL(message)) {
const chunks = message.split(' ')
const chunksLength = chunks.length - 1
chunks.forEach((chunk, index) => {
if (hasURL(chunk)) {
const anchorTag = document.createElement('a')
anchorTag.href = /^https?:\/\//i.test(chunk) ? chunk : `http://${chunk}`
anchorTag.target = "_blank"
anchorTag.rel = "noopener"
anchorTag.textContent = chunksLength !== index ? `${chunk} ` : chunk
messageContent.append(anchorTag)
}
else {
const text = chunksLength !== index ? `${chunk} ` : chunk
const textNode = document.createTextNode(text)
messageContent.append(textNode)
}
})
}
else {
let [messageBody, isOnlyEmoji] = isEmoji(message)
if (isOnlyEmoji)
cardContainer.classList.add('big-emoji')
messageContent.append(messageBody)
}
let time = new Date(timestamp).toString(),
minutes = String(new Date(timestamp).getMinutes()),
hours = new Date(timestamp).getHours(),
year = new Date(timestamp).getFullYear()
minutes = minutes.length === 1 ? `0${minutes}` : minutes
let finalHours = hours - 12 > 0 ? `${hours - 12}:${minutes} pm` : `${hours}:${minutes} am`
messageTime.textContent = finalHours
if (currentFloID !== floID) {
currentDate = null
lastSender = null
currentFloID = floID
renderedDates.clear()
}
if (!renderedDates.has(`${time.slice(4, 10)} ${year}`) && `${time.slice(4, 10)} ${year}` !== currentDate) {
let dateCard = document.createElement('h5')
dateCard.classList.add('date-card')
dateCard.textContent = new Date().getFullYear() !== year ? `${time.slice(4, 10)} ${year}` : time.slice(4, 10)
currentDate = `${time.slice(4, 10)} ${year}`
let frag = document.createDocumentFragment()
frag.append(dateCard, card)
renderedDates.set(currentDate, currentDate)
return frag
}
else
return card;
}
else if (admin) {
if (newMembers.length) {
const cards = document.createDocumentFragment()
const { admin } = messenger.groups[groupID]
newMembers.forEach(member => {
let eventCard = document.createElement('p')
eventCard.classList.add('group-event-card')
let eventMessage = ''
if (member === myFloID)
eventMessage = `${getContactName(admin)} added you`
else
eventMessage = `${getContactName(admin)} added ${getContactName(member)}`
eventCard.textContent = eventMessage
cards.append(eventCard)
})
return cards
}
else if (rmMembers.length) {
const cards = document.createDocumentFragment()
const { admin } = messenger.groups[groupID]
rmMembers.forEach(member => {
let eventCard = document.createElement('p')
eventCard.classList.add('group-event-card')
let eventMessage = ''
if (member === myFloID)
eventMessage = `${getContactName(admin)} removed you`
else
eventMessage = `${getContactName(admin)} removed ${getContactName(member)}`
eventCard.textContent = eventMessage
cards.append(eventCard)
})
return cards
}
else if (name) {
let eventCard = document.createElement('p')
eventCard.classList.add('group-event-card')
eventCard.textContent = `Changed group name to '${name}'`
return eventCard
}
else if (description) {
let eventCard = document.createElement('p')
eventCard.classList.add('group-event-card')
eventCard.textContent = `Changed group description to '${description}'`
return eventCard
}
}
},
}
let currentDate,
currentFloID,
renderedDates = new Map(),
lastSender
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()
const length = res.length
let isOnlyEmoji = false
res.forEach(section => {
if (section.match(rx)) {
const span = document.createElement('span')
if (length === 1)
isOnlyEmoji = true
else
span.classList.add('text-emoji')
span.textContent = section
messageBody.append(span)
}
else if (length === 2 && !/\w/.test(section)) {
isOnlyEmoji = true
}
else {
messageBody.append(section)
}
})
return [messageBody, isOnlyEmoji]
}
function renderDirectUI(data) {
renderMessages(data.messages, { updateChatCard: true });
renderMailList(data.mails)
//let order = Object.keys(data.messages).map(a => a.split('_')).sort((a, b) => a[0] - b[0]).map(a => a[1])
if (Object.keys(data.messages).length) {
document.title = `New message(s)`
}
if (Object.keys(data.mails).length && activePage.button !== getRef('mail_page_button')) {
document.title = `New mail(s)`
}
}
function renderGroupUI(data) {
renderMessages(data.messages, { updateChatCard: true });
}
window.addEventListener('focus', e => {
if (activeChat.chatCard) {
if (!chatScrollInfo.isScrolledUp) {
scrollToBottom()
}
}
})
window.addEventListener('blur', e => {
console.log('blured')
})
document.addEventListener('keyup', e => {
if (e.target.closest('#send_mail_to')) {
if (e.code === 'ArrowDown') {
for (child of getRef('mail_contact_list').children) {
if (!child.classList.contains('hide-completely')) {
child.focus()
break
}
}
}
if (e.code === 'Enter' && getRef('mail_contact_list').firstElementChild) {
for (child of getRef('mail_contact_list').children) {
if (!child.classList.contains('hide-completely')) {
child.click()
break
}
}
}
}
if (e.target.closest('#search_chats')) {
if (e.code === 'ArrowDown') {
for (child of getRef('chat_container').children) {
if (!child.classList.contains('hide-completely')) {
child.focus()
break
}
}
}
if (e.code === 'Enter' && getRef('contacts_container').firstElementChild) {
for (child of getRef('contacts_container').children) {
if (!child.classList.contains('hide-completely')) {
child.click()
break
}
}
}
}
if (e.target.closest('.contact')) {
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.getAttribute('flo-id')
getRef('mail_contact_list').classList.add('hide-completely')
}
}
})
const chatScrollInfo = {};
let debounceTimer
getRef('chat_middle').addEventListener('scroll', function () {
if (debounceTimer) {
window.clearTimeout(debounceTimer)
}
debounceTimer = setTimeout(e => {
chatScrollInfo['scrollTop'] = this.scrollTop
chatScrollInfo['scrollheight'] = this.scrollHeight
if ((this.scrollHeight > this.clientHeight) && (this.scrollHeight - this.clientHeight - this.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('send_mail_to').addEventListener('input', function () {
getRef('mail_contact_list').classList.remove('hide-completely')
if (this.value.trim !== '') {
for (child of getRef('mail_contact_list').children) {
if (child.getAttribute('name').toUpperCase().indexOf(this.value.toUpperCase()) > -1) {
child.classList.remove('hide-completely')
}
else {
child.classList.add('hide-completely')
}
}
}
})
getRef('search_chats').addEventListener('input', function (e) {
const contacts = getRef('chat_container').querySelectorAll('.contact')
contacts.forEach(child => {
if (child.getAttribute('search-tags').toLowerCase().includes(this.value.toLowerCase())) {
child.classList.remove('hide-completely')
}
else {
child.classList.add('hide-completely')
}
})
})
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]
if (isCreatingGroup && !(contact in floGlobals.pubKeys))
delete contacts[contact]
}
}
getRef('contacts_container').innerHTML = ''
renderContactList(contacts)
})
document.addEventListener('click', e => {
if (e.target.closest('.sidebar-item')) {
let target = e.target.closest('.sidebar-item')
if (target.dataset.target)
showPanel(target, target.dataset.target)
}
if (e.target.closest('.navbar-item') && activePage.button !== e.target.closest('.navbar-item')) {
const targetPage = e.target.closest('.navbar-item').dataset.target
showPage(targetPage, true)
history.pushState({ type: 'subpage', id: targetPage }, null, `#${targetPage}`)
}
if (e.target.closest('#send_mail_to') || e.target.closest('#mail_contact_list')) {
getRef('mail_contact_list').classList.remove('hide-completely')
}
else {
getRef('mail_contact_list').classList.add('hide-completely')
}
if (e.target.closest('#new_mail_button')) {
showPopup('compose_mail_popup')
}
//Detect click on send mail option
if (e.target.closest(".send-mail-option")) {
let floID;
showPopup('compose_mail_popup')
floID = e.target.closest(".contact").getAttribute("flo-id");
getRef('send_mail_to').value = floID;
return false;
}
// 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')
}
})
getRef('chat_page').addEventListener('contextmenu', e => {
if (e.target.closest('.contact')) {
e.preventDefault()
}
})
let holdTimeout
let holdThreshold = 500
getRef('chat_page').addEventListener('touchstart', e => {
if (e.target.closest('.contact')) {
holdTimeout = setTimeout(() => {
if (isHapticOn)
navigator.vibrate(100)
let contact = e.target.closest(".contact")
clickedContact['chatCard'] = contact
clickedContact['floID'] = contact.getAttribute("flo-id")
clickedContact['name'] = contact.getAttribute("name")
clickedContact['isGroup'] = messenger.groups.hasOwnProperty(clickedContact['floID'])
showPopup('contact_details_popup')
}, 500)
getRef('chat_page').addEventListener('touchmove', handleTouchMove)
}
})
function handleTouchMove(e) {
if (e.target.closest('.contact')) {
clearTimeout(holdTimeout)
}
}
getRef('chat_page').addEventListener('touchend', e => {
if (e.target.closest('.contact')) {
clearTimeout(holdTimeout)
getRef('chat_page').removeEventListener('touchmove', handleTouchMove)
}
})
getRef('mail_contact_list').addEventListener('click', e => {
if (e.target.closest('.contact')) {
getRef('send_mail_to').value = e.target.closest('.contact').getAttribute('flo-id')
getRef('mail_contact_list').classList.add('hide-completely')
}
})
const clickedContact = {}
getRef('chat_page').addEventListener('click', e => {
//detect click on chat cards
if (e.target.closest(".contact")) {
let contact = e.target.closest(".contact")
clickedContact['chatCard'] = contact
clickedContact['floID'] = contact.getAttribute("flo-id")
clickedContact['name'] = contact.getAttribute("name")
clickedContact['isGroup'] = messenger.groups.hasOwnProperty(clickedContact['floID'])
if (clickedContact['floID'] === myFloID) return
if (e.target.closest(".selectable")) {
if (isRemovingMember)
selectMemberToRemove(e.target.closest(".selectable"))
}
else if (isCreatingGroup) {
// code for adding group members
selectContact(contact)
}
else if (e.target.closest(".initial") || e.target.closest(".menu")) {
showPopup('contact_details_popup')
}
else {
createRipple(e, contact)
contact.classList.remove('unread')
if (activeChat['chatCard'] === contact && window.innerWidth > 640) return
showChatDetails({ show: false, animate: false })
document.title = `FLO Messenger`
getRef('chat').classList.remove('hide-completely')
viewConversation(contact)
if (activeChat['chatCard'])
activeChat['chatCard'].classList.remove('active')
contact.classList.add('active')
activeChat['chatCard'] = contact
if (activeChatPage.id === 'contacts') {
getRef('chat').classList.remove('hide-on-mobile')
getRef('contacts').classList.add('hide-on-mobile')
activeChatPage = getRef('chat')
getRef('main_navbar').classList.add('hide-on-mobile')
}
}
}
else if (e.target.closest('.contact-preview')) {
removeSelectedContact(e.target.closest('.contact-preview').getAttribute('flo-id'))
}
})
getRef('contacts_popup').addEventListener('click', e => {
//detect click on contacts
if (e.target.closest(".selectable")) {
selectMemberToAdd(e.target.closest(".selectable"))
}
})
// Function to show all contacts side drawer
let contactsDrawerAnimation
function showContacts(options) {
const { show = true, onlyValid = false } = options
let contacts = {}
if (show) {
getRef('contacts_container').innerHTML = ''
// Show contacts which replied
if (onlyValid) {
for (contact in floGlobals.contacts) {
if (contact in floGlobals.pubKeys) {
contacts[contact] = floGlobals.contacts[contact]
}
}
}
else {
contacts = floGlobals.contacts
}
renderContactList(contacts)
getRef('all_contacts').classList.remove('hide-completely')
contactsDrawerAnimation = animateTo(getRef('all_contacts'), [
{ transform: 'translateY(2rem)' },
{ transform: 'translateY(0)' },
], {
duration: 300,
easing: 'ease'
})
}
else {
isCreatingGroup = false
clearAllMembers()
contactsDrawerAnimation.reverse()
contactsDrawerAnimation.onfinish = () => {
getRef('all_contacts').classList.add('hide-completely')
getRef('all_contacts_options').classList.remove('hide-completely')
getRef('selected_contacts').classList.add('hide-completely')
getRef('skip_members_button').classList.add('hide-completely')
getRef('contacts_container').innerHTML = ''
}
}
}
function transformScroll(event) {
if (!event.deltaY) {
return;
}
event.currentTarget.scrollLeft += event.deltaY + event.deltaX;
event.preventDefault();
}
getRef('selected_contacts_container').addEventListener('wheel', transformScroll);
const selectedGroupMembers = new Set()
function selectContact(contact) {
if (!selectedGroupMembers.has(clickedContact.floID)) {
selectedGroupMembers.add(clickedContact.floID)
const clonedInitial = contact.querySelector('.initial').cloneNode(true);
const clonedName = contact.querySelector('.name').cloneNode(true);
const preview = document.createElement('div')
preview.classList.add('contact-preview')
preview.setAttribute('flo-id', clickedContact['floID']);
preview.append(clonedInitial, clonedName)
addCross(preview)
getRef('selected_contacts_container').append(preview)
preview.scrollIntoView({ behavior: "smooth", inline: "end" });
preview.animate(
[
{ transform: 'scale(0)' },
{ transform: 'scale(1)' },
],
{
duration: 300,
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
fill: 'forwards'
}
)
addTick(contact)
}
else {
removeSelectedContact(clickedContact.floID)
}
if (selectedGroupMembers.size) {
getRef('skip_members_button').textContent = 'Next'
}
else {
getRef('skip_members_button').textContent = 'Skip'
}
}
function removeSelectedContact(floID) {
selectedGroupMembers.delete(floID)
const relatedContact = getRef('contacts_container').querySelector(`[flo-id="${floID}"]`)
const relatedPreview = getRef('selected_contacts_container').querySelector(`[flo-id="${floID}"]`)
if (relatedContact)
removeTick(relatedContact)
relatedPreview.animate(
[
{ transform: 'scale(1)' },
{ transform: 'scale(0)' },
],
{
duration: 150,
easing: 'ease',
fill: 'forwards'
}
).onfinish = () => {
relatedPreview.remove()
}
}
function clearAllMembers() {
getRef('selected_contacts_container').innerHTML = ''
selectedGroupMembers.clear()
}
let isCreatingGroup = false
function initGroupCreation() {
isCreatingGroup = true
showContacts({ show: true, onlyValid: true })
getRef('all_contacts_options').classList.add('hide-completely')
getRef('selected_contacts').classList.remove('hide-completely')
getRef('skip_members_button').classList.remove('hide-completely')
}
function skipToGroupCreation() {
const animOptions = {
duration: 300,
fill: 'forwards',
easing: 'ease'
}
getRef('all_contacts').animate(
[
{ transform: 'translateX(0)' },
{ transform: 'translateX(-25%)' },
],
animOptions
).onfinish = () => {
getRef('all_contacts').classList.add('hide-completely')
}
getRef('group_creation_panel').classList.remove('hide-completely')
getRef('group_creation_panel').animate(
[
{ transform: 'translateX(100%)' },
{ transform: 'translateX(0)' },
],
animOptions
)
}
function backToContacts() {
const animOptions = {
duration: 300,
fill: 'forwards',
easing: 'ease'
}
getRef('group_creation_panel').animate(
[
{ transform: 'translateX(0)' },
{ transform: 'translateX(100%)' },
],
animOptions
).onfinish = () => {
getRef('group_creation_panel').classList.add('hide-completely')
}
getRef('all_contacts').classList.remove('hide-completely')
getRef('all_contacts').animate(
[
{ transform: 'translateX(-25%)' },
{ transform: 'translateX(0)' },
],
animOptions
)
}
document.getElementById('create_group_button').addEventListener('clicked', createGroup)
function createGroup() {
const groupName = getRef('group_name_field').value.trim()
const groupDescription = getRef('group_description_field').value.trim()
messenger.createGroup(groupName, groupDescription)
.then(groupInfo => {
isCreatingGroup = false
getRef('chat_container').prepend(render.contactCard(groupInfo.groupID, { type: 'group' }))
getRef('chat_container').children[0].click()
getRef('group_creation_panel').animate(
[
{ transform: 'translateY(0)' },
{ transform: 'translateY(2rem)' },
],
{
duration: 150,
fill: 'forwards',
easing: 'ease'
}
).onfinish = () => {
getRef('all_contacts').classList.add('hide-completely')
getRef('group_creation_panel').classList.add('hide-completely')
getRef('all_contacts_options').classList.remove('hide-completely')
getRef('selected_contacts').classList.add('hide-completely')
}
notify('Group created', 'success')
if (selectedGroupMembers.size) {
messenger.addGroupMembers(groupInfo.groupID, [...selectedGroupMembers])
.then(res => {
clearAllMembers()
})
.catch(err => console.error(err))
}
})
.catch(err => console.error(err))
}
let isEmojiPickerOpen = false
function toggleEmoji(mode) {
switch (mode) {
case 'toggle':
isEmojiPickerOpen = true
getRef('emoji_toggle').classList.toggle('active')
getRef('emoji_picker').classList.toggle('hide-completely')
break;
case 'hide':
isEmojiPickerOpen = false
getRef('emoji_toggle').classList.remove('active')
getRef('emoji_picker').classList.add('hide-completely')
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 (window.innerWidth > 640) {
setTimeout(() => {
getRef('type_message').focusIn()
}, 0);
}
})
const style = document.createElement('style');
style.textContent = `
.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);
}
}
}`
function copyToClipboard(element, message, parent = '.copy-row') {
const parentElement = element.closest(parent)
const copyTarget = parentElement.querySelector('.copy')
if (copyTarget.tagName === 'SM-INPUT' || copyTarget.tagName === 'INPUT' || copyTarget.tagName === 'TEXTAREA')
copyText = copyTarget.value
else
copyText = copyTarget.textContent
navigator.clipboard.writeText(copyText).then(() => {
notify(`${message}`, 'success')
})
.catch(error => {
notify(error, 'error')
})
}
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');
}
})
function updateHeight() {
if (window.innerWidth < 640) {
getRef('chat').style.height = window.innerHeight + 'px'
}
else {
getRef('chat').style.height = '100vh'
}
}
function goto(page) {
if (page === 'chats') {
getRef('chat').classList.add('hide-on-mobile')
getRef('contacts').classList.remove('hide-on-mobile')
activeChatPage = getRef('contacts')
}
if (page === 'mails') {
getRef('mail').classList.add('hide-on-mobile')
getRef('mails').classList.remove('hide-on-mobile')
activeMailPage = getRef('mails')
}
getRef('main_navbar').classList.remove('hide-on-mobile')
}
getRef("mails").addEventListener('click', function (e) {
if (e.target.closest(".mail-card")) {
getRef('mail_page_button').setAttribute('data-notifications', '0')
getRef('mail').classList.remove('hide-completely')
e.target.closest(".mail-card").classList.remove('unread')
viewMail(e.target.closest(".mail-card").getAttribute("name"));
if (activeMail)
activeMail.classList.remove('active')
e.target.closest(".mail-card").classList.add('active')
activeMail = e.target.closest(".mail-card")
if (activeMailPage.id === 'mails') {
getRef('mail').classList.remove('hide-on-mobile')
getRef('mails').classList.add('hide-on-mobile')
activeMailPage = getRef('mail')
getRef('main_navbar').classList.add('hide-on-mobile')
}
}
})
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 => {
if (getRef('type_message').value.trim() !== '')
getRef('send_message_button').classList.add('active')
else
getRef('send_message_button').classList.remove('active')
})
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()
}
}
})
getRef("clear_data").addEventListener('click', async function (e) {
if (await confirmation('Clear Data?',
`Are you sure you want to clear stored data?`, 'No', "Clear")) {
messenger.clearUserData().then(result => {
notify("Successfully Cleared local data", 'success')
setTimeout(onLoadStartUp, 2000)
}).catch(error => notify(error, "error"))
}
});
getRef('add_contact_button').addEventListener("clicked", addContact)
getRef('show_reply_popup').addEventListener("click", () => {
showPopup('reply_mail_popup')
})
getRef('reply_mail_button').addEventListener("clicked", replyMail)
getRef("backup_data").addEventListener("click", function (e) {
let backupInfoText = getRef("backup_info")
backupInfoText.classList.remove("error")
backupInfoText.textContent = `Generating the backup file! Please wait...`;
messenger.backupData().then(blob => {
let anchor = document.createElement('a')
anchor.setAttribute("download", `BackupFor_${myFloID}_${Date.now()}.json`)
anchor.setAttribute("href", URL.createObjectURL(blob))
backupInfoText.textContent =
`Backup file generated! If the download didn't start automatically, click `;
anchor.textContent = `here.`;
backupInfoText.append(anchor);
anchor.click();
}).catch(error => {
backupInfoText.classList.add("error")
backupInfoText.textContent = `Unable to generate backup file! Try again later...`;
notify("Backup data Unsuccessful!", "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 => {
if (await confirmation('Restore Data?',
`Found: ${Object.keys(data.contacts).length} Contacts,\n ${Object.keys(data.messages).length} Messages, ${Object.keys(data.mails).length} Mails.`,
'Cancel', "Restore"
)) {
notify(`Restoring data! Please wait.`);
messenger.restoreData(data).then(result => {
notify("Data restore completed successful! Initiating reload, Please wait", 'success')
setTimeout(onLoadStartUp, 2000)
}).catch(error => {
notify("Failed to restore data! Try again later", "error", error);
});
}
}).catch(error => {
notify("Retrive data Unsuccessful!", "error", error);
})
})
function sendMessage() {
if (window.innerWidth > 640)
getRef('type_message').focusIn()
let receiver = activeChat['floID']
let container;
let message = getRef('type_message').value.trim();
getRef('type_message').value = ''
if (message === '') return
let time = Date.now()
let msgObj = { message, time, unconfirmed: true }
if (activeChat.isGroup) {
msgObj['groupID'] = activeChat.floID
msgObj['sender'] = myFloID
}
else {
msgObj['floID'] = activeChat.floID
msgObj['category'] = 'sent'
}
getRef('messages_container').append(render.messageBubble(msgObj))
const contact = getRef('chat_container').querySelector(`.chat[flo-id="${receiver}"], .group[flo-id="${receiver}"]`)
if (contact) {
if (activeChat['chatCard'] !== getRef('chat_container').children[0]) {
const cloneContact = contact.cloneNode(true)
contact.remove()
activeChat['chatCard'] = cloneContact
getRef('chat_container').prepend(cloneContact)
animateTo(getRef('chat_container').children[0], [
{ transform: 'translateY(1rem)' },
{ transform: 'none' },
],
{
easing: 'ease',
duration: 300
}
)
}
}
else {
messenger.addChat(receiver)
getRef('chat_container').prepend(render.contactCard(receiver, { type: 'chat', prepend: true }))
getRef('chat_container').children[0].classList.add('active')
activeChat['chatCard'] = getRef('chat_container').children[0]
}
scrollToBottom()
if (activeChat.isGroup)
messenger.sendGroupMessage(message, receiver).then(data => {
getRef('messages_container').querySelector(`#${receiver}_${time}`).classList.remove('unconfirmed')
activeChat.chatCard.querySelector('.last-message').textContent = `You: ${message}`
activeChat.chatCard.querySelector('.time').textContent = getFormatedTime(Date.now(), true)
}).catch(error => notify(error, "error"));
else
messenger.sendMessage(message, receiver).then(data => {
getRef('messages_container').querySelector(`#${receiver}_${time}`).classList.remove('unconfirmed')
activeChat.chatCard.querySelector('.last-message').textContent = `You: ${message}`
activeChat.chatCard.querySelector('.time').textContent = getFormatedTime(Date.now(), true)
}).catch(error => notify(error, "error"));
}
function removeElement(element) {
element.parentNode.removeChild(element);
}
function clearElement(element) {
element.innerHTML = '';
return element;
}
function addContact() {
let floID = getRef('add_contact_floID').value.trim();
let name = getRef('add_contact_name').value.trim();
if (floID === myFloID) {
notify(`you can't add your own FLO ID as contact`, 'error')
return
}
if (floGlobals.contacts.hasOwnProperty(floID)) {
notify(`Contact already saved`, 'error')
return
}
messenger.storeContact(floID, name).then(result => {
getRef('contacts_container').append(render.contactCard(floID, { name, type: 'contact' }))
hidePopup()
notify(`Added Contact: ${floID}`)
}).catch(error => notify(error, "error"));
}
function renderContactList(contactList = {}) {
const frag = document.createDocumentFragment()
for (floID in contactList) {
let isSelected = selectedGroupMembers.has(floID)
frag.append(render.contactCard(floID, { type: 'contact', isSelected }))
}
getRef('contacts_container').append(frag)
}
async function renderChatList(chatOrder) {
const frag = document.createDocumentFragment()
getRef('chat_container').innerHTML = ''
for (floID of chatOrder) {
const markUnread = messenger.marked[floID]?.includes('unread')
let type
if (messenger.chats[floID])
type = 'chat'
else if (messenger.groups[floID])
type = 'group'
frag.append(render.contactCard(floID, { type, markUnread }))
}
getRef('chat_container').append(frag)
}
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.chatCard) {
messenger.removeMark(activeChat.floID, 'unread')
activeChat.chatCard.classList.remove('unread')
}
getRef('scroll_to_bottom').classList.remove('new-message')
setTimeout(() => {
getRef('chat_middle').scrollTo({ top: getRef('chat_middle').scrollHeight, behavior: smooth ? 'smooth' : undefined })
}, smooth ? 300 : 0);
}
let startIndex = 0,
endIndex = 0
function renderMessages(data, options) {
let { markUnread = true, updateChatCard = false, reRender = false, lazyLoad = false } = options
let messages
if (reRender) {
activeChat['allMessages'] = Object.values(data)
startIndex = activeChat['allMessages'].length > 20 ? activeChat['allMessages'].length - 20 : 0
endIndex = activeChat['allMessages'].length
messages = activeChat['allMessages']
renderedDates.clear()
}
else if (lazyLoad) {
messages = activeChat['allMessages']
endIndex = startIndex
startIndex = endIndex > 20 ? endIndex - 20 : 0
markUnread = false
}
else {
messages = Object.values(data)
if (messages.length) {
startIndex = 0
endIndex = messages.length
}
}
if (messages && messages.length) {
for (let i = startIndex; i < endIndex; i++) {
let { floID, groupID, sender, message, time, category } = messages[i]
//Stops message from rendering in wrong chat window
if (activeChat['floID'] && (activeChat['floID'] === floID || activeChat['floID'] === groupID)) {
// Stops message rendering if message is sent from original user causing duplication
if (updateChatCard && activeChat.isGroup && message && sender === myFloID) {
messenger.removeMark(groupID, 'unread')
return
}
frag.append(render.messageBubble({ ...messages[i], updateChatCard }))
}
const contact = getRef('chat_container').querySelector(`.contact[flo-id='${floID || groupID}']`)
if (markUnread && contact) {
contact.classList.add("unread");
if (contact !== getRef('chat_container').children[0]) {
const cloneContact = contact.cloneNode(true)
contact.remove()
getRef('chat_container').prepend(cloneContact)
animateTo(getRef('chat_container').children[0], [
{ transform: 'translateY(1rem)' },
{ transform: 'none' },
],
{
easing: 'ease',
duration: 300
}
)
}
}
if (updateChatCard) {
let chatCard
if (!contact) {
getRef('chat_container').prepend(render.contactCard(floID, { type: 'chat', markUnread: true }))
chatCard = getRef('chat_container').firstElementChild
}
else {
chatCard = contact
let finalMessage
if (floGlobals.contacts[sender])
finalMessage = `${floGlobals.contacts[sender]}: ${message}`
else if (sender === myFloID)
finalMessage = `You: ${message}`
else
finalMessage = message
chatCard.querySelector('.last-message').textContent = finalMessage
chatCard.querySelector('.time').textContent = getFormatedTime(time, true)
}
if (activeChat.floID === (floID || groupID)) {
if (chatScrollInfo.isScrolledUp)
getRef('scroll_to_bottom').classList.add('new-message')
else {
if (document.hasFocus()) {
messenger.removeMark((floID || groupID), 'unread')
setTimeout(() => {
document.title = 'FLO Messenger'
activeChat.chatCard.classList.remove('unread')
}, 1000);
}
}
}
if (!document.hasFocus() && navigator.onLine) {
getRef('notification_sound').currentTime = 0;
getRef('notification_sound').play();
}
}
}
}
if (!lazyLoad && !reRender) {
endIndex = messages.length
getRef('messages_container').append(frag)
if (!chatScrollInfo['isScrolledUp']) {
setTimeout(() => {
scrollToBottom(true)
}, 100);
}
}
if (reRender || lazyLoad) {
currentDate = null
lastSender = null
chatScrollInfo['scrollTop'] = getRef('chat_middle').scrollTop
chatScrollInfo['scrollHeight'] = getRef('chat_middle').scrollHeight
getRef('messages_container').prepend(frag)
}
if (reRender) {
scrollToBottom()
}
}
//checks for added elements in chat
const chatMutationObserver = new MutationObserver(
(mutations, observer) => {
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length && getRef('messages_container').firstElementChild) {
chatMessageObserver.observe(getRef('messages_container').firstElementChild)
chatScrollInfo['scrollTop'] += (getRef('chat_middle').scrollHeight - chatScrollInfo['scrollHeight'])
chatScrollInfo['scrollHeight'] = getRef('chat_middle').scrollHeight
getRef('chat_middle').scrollTo({ top: chatScrollInfo['scrollTop'] })
}
}
}
)
//Lazy loading for chat messages
const chatMessageObserver = new IntersectionObserver(
(entries, observer) => {
if (entries[0].isIntersecting) {
renderMessages('', { lazyLoad: true })
observer.disconnect()
}
}
)
async function viewConversation(contact) {
getRef('messages_container').innerHTML = ''
let floID = clickedContact['floID'],
name = contact.getAttribute('name'),
textColor = contact.getAttribute('text-color'),
backgroundColor = contact.getAttribute('background-color')
activeChat['floID'] = floID
activeChat['isGroup'] = messenger.groups[floID] ? true : false
getRef("chat_dp").setAttribute('style', `color: ${textColor}; background-color: ${backgroundColor};`)
getRef("receiver_name").textContent = getContactName(floID);
if (activeChat.isGroup) {
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>
`
getRef('video_call_button').classList.add('hide-completely')
}
else {
getRef("receiver_initial").textContent = getContactName(floID).charAt(0);
// getRef('video_call_button').classList.remove('hide-completely')
}
getRef("receiver_initial").setAttribute('style', `color: ${textColor}; background-color: ${backgroundColor};`)
if (floGlobals.pubKeys[floID] || activeChat.isGroup)
getRef("warn_no_encryption").classList.add("hide-completely");
else
getRef("warn_no_encryption").classList.remove("hide-completely");
renderMessages(await messenger.getChat(floID), { markUnread: false, reRender: true })
messenger.removeMark(floID, "unread");
if (this.scrollHeight <= this.clientHeight) {
chatScrollInfo['isScrolledUp'] = false
getRef('scroll_to_bottom').classList.remove('no-transformations')
}
}
function renderMailList(mails, markUnread = true) {
const inboxMailFrag = document.createDocumentFragment()
const sentMailFrag = document.createDocumentFragment()
for (let m in mails) {
let { from, to, prev, ref, subject, time, content } = mails[m]
if (from === myFloID)
sentMailFrag.prepend(render.mailCard(to, ref, subject, time, content, markUnread))
else if (to.includes(myFloID))
inboxMailFrag.prepend(render.mailCard(from, ref, subject, time, content, markUnread))
}
getRef('inbox_mail_container').prepend(inboxMailFrag)
getRef('sent_mail_container').prepend(sentMailFrag)
}
function viewMail(mailRef, newView = true) {
//stop rerendering if same mail is already open
if (mailRef === (activeMail ? activeMail.getAttribute('name') : '')) return
//clear the container
if (newView)
clearElement(getRef("mail_container"))
messenger.getMail(mailRef).then(result => {
let { from, to, prev, ref, subject, time, content } = result
//append the contents to mail container
getRef("mail_container").append(render.mail(from, to, subject, time, content));
//add prop for previous mail (if available)
let prevMail = getRef("prev_mail");
prevMail.dataset["value"] = prev;
prevMail.style.display = prev ? 'block' : 'none';
//set values for reply mail form if new view
if (newView) {
getRef('reply_mail_popup').dataset["to"] = (from === myFloID ? 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("hide-completely");
}
messenger.removeMark(mailRef, "unread");
}).catch(error => notify("Unable to read mail", "error", error))
}
getRef('send_mail_button').addEventListener('clicked', sendMail)
function sendMail() {
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);
})
messenger.sendMail(subject, content, recipients).then(result => {
notify(`Mail(s) sent!`)
renderMailList(result)
hidePopup()
}).catch(error => notify("Failed to send mail!", "error", error))
} 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)
hidePopup()
}).catch(error => notify("Failed to reply mail!", "error", error))
}
document.getElementById('mail_type_selector').addEventListener('change', e => {
document.querySelectorAll('.mail-container').forEach(container => container.classList.add('hide-completely'))
getRef(`${e.detail.value}_mail_container`).classList.remove('hide-completely')
})
const allPanels = document.querySelectorAll('.panel'),
allSidebarItems = document.querySelectorAll('.sidebar-item')
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'
}
const windowSizeObserver = new ResizeObserver(entries => {
updateHeight()
if (entries[0].borderBoxSize[0].inlineSize > 640) {
getRef('settings_sidebar').style = ''
getRef('settings_panel').style = ''
}
})
windowSizeObserver.observe(document.body)
function animateTo(element, animation, options) {
const anime = element.animate(animation, { ...options, fill: 'both' })
anime.addEventListener('finish', () => {
anime.commitStyles()
anime.cancel()
})
return anime
}
function showPanel(item, panel) {
getRef('settings_title').textContent = item.textContent
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')
}
}
else {
allSidebarItems.forEach(panel => panel.classList.remove('active'))
item.classList.add('active')
}
allPanels.forEach(panel => panel.classList.add('hide-completely'))
getRef(panel).classList.remove('hide-completely')
}
function hidePanel() {
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 toggleDrawer() {
getRef('main_navbar').classList.toggle('no-transformations')
getRef('navbar_backdrop').classList.toggle('hide')
}
document.addEventListener('contentchanged', e => {
if (e.target.closest('#contact_name')) {
changeContactName(e.detail.value.trim())
}
else if (e.target.closest('#group_description')) {
messenger.changeGroupDescription(activeChat.floID, e.detail.value.trim())
.then(res => {
notify('Changed group description', 'success')
})
.catch(error => notify(error, "error"));
}
else if (e.target.closest('#chat_name')) {
changeContactName(e.detail.value.trim(), true)
}
})
async function changeContactName(name, isChat = false) {
let isGroup,
floID
if (isChat) {
activeChat['name'] = name
if (activeChat['name'] === '')
activeChat['name'] = 'Unknown'
isGroup = activeChat.isGroup
floID = activeChat.floID
name = activeChat['name']
}
else {
clickedContact['name'] = name
if (clickedContact['name'] === '')
clickedContact['name'] = 'Unknown'
isGroup = clickedContact.isGroup
floID = clickedContact.floID
name = clickedContact['name']
}
if (isGroup) {
messenger.changeGroupName(floID, name).then(res => {
updatechatCards({ name, floID, isGroup: true })
notify('Changed group name', 'success')
})
.catch(error => notify(error, "error"));
}
else {
messenger.storeContact(floID, name).then(result => {
updatechatCards({ name, floID, isGroup: false })
notify('Changed contact name', 'success')
})
.catch(error => notify(error, "error"));
}
}
function updatechatCards({ name, floID, isGroup = false }) {
if (activeChat.floID && activeChat.floID === clickedContact.floID) {
getRef('receiver_name').textContent = name
getRef('chat_name').value = name
}
if (!isGroup) {
getRef('contact_initial').textContent = name.charAt(0)
if (activeChat.floID && activeChat.floID === clickedContact.floID) {
getRef('receiver_initial').textContent = name.charAt(0)
getRef('chat_dp').textContent = name.charAt(0)
}
}
document.querySelectorAll(`.contact[flo-id="${floID}"]`).forEach(contact => {
if (!isGroup) {
contact.children[0].textContent = name.charAt(0)
contact.children[1].textContent = name
}
contact.setAttribute('name', name)
})
}
function toggleSearch(target) {
getRef(target).classList.toggle('expand')
if (getRef(target).classList.contains('expand'))
getRef(target).children[1].focusIn()
else
getRef(target).querySelector('sm-input').value = ''
}
let isChatDetailsOpen = false
function showChatDetails({ show, animate = true }) {
if (show) {
if (isChatDetailsOpen) return
const floID = activeChat.floID
isChatDetailsOpen = true
getRef("chat_name").value = getContactName(floID);
getRef("chat_flo_id").textContent = floID
if (activeChat.isGroup) {
getRef("chat_dp").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>
`
getRef("last_interaction_time").textContent = `Created ${getFormatedTime(messenger.groups[floID].created)}`;
getRef("chat_type").textContent = `Group FLO ID`;
getRef('group_members_list').innerHTML = ''
messenger.groups[floID].members.forEach(member => {
let isAdmin = messenger.groups[floID].admin === member ? true : false
frag.append(render.contactCard(member, { type: 'contact', contactOnly: true, isAdmin }))
})
getRef('group_members_list').append(frag)
getRef("group_description_card").classList.remove('hide-completely')
getRef("group_members_card").classList.remove('hide-completely')
getRef('delete_chat_button').classList.add('hide-completely')
getRef("group_description").value = messenger.groups[floID].description === '' ? 'Add group description' : messenger.groups[floID].description;
if (messenger.groups[activeChat['floID']].admin === myFloID) {
getRef("chat_name").disabled = false
getRef('group_description').disabled = false
getRef('edit_group_button').classList.remove('hide-completely')
}
else {
getRef("chat_name").disabled = true
getRef('group_description').disabled = true
getRef('edit_group_button').classList.add('hide-completely')
}
}
else {
getRef("chat_dp").textContent = getContactName(floID).charAt(0);
getRef("last_interaction_time").textContent = ``;
getRef("chat_type").textContent = `FLO ID`;
getRef("group_description_card").classList.add('hide-completely')
getRef("group_members_card").classList.add('hide-completely')
getRef('delete_chat_button').classList.remove('hide-completely')
}
getRef('chat').classList.add('expand-side-panel')
getRef('chat_left').classList.add('hide-on-medium')
getRef('chat_details_panel').classList.remove('hide-completely')
animateTo(getRef('chat_details_panel'), [
{ transform: 'translateX(100%)' },
{ transform: 'translateX(0)' },
],
{
duration: animate ? 300 : 0,
easing: 'ease'
})
}
else if (isChatDetailsOpen) {
isChatDetailsOpen = false
animateTo(getRef('chat_details_panel'), [
{ transform: 'translateX(0)' },
{ transform: 'translateX(100%)' },
],
{
duration: animate ? 150 : 0,
easing: 'ease'
}).onfinish = () => {
getRef('chat').classList.remove('expand-side-panel')
getRef('chat_left').classList.remove('hide-on-medium')
getRef('chat_details_panel').classList.add('hide-completely')
}
editGroupMembers()
}
}
function addAsContact() {
showPopup('add_contact_popup')
getRef('add_contact_floID').value = clickedContact.floID
}
function markAsUnread() {
clickedContact.chatCard.classList.add('unread')
messenger.addMark(clickedContact.floID, 'unread')
hidePopup()
}
function markAsRead() {
clickedContact.chatCard.classList.remove('unread')
messenger.removeMark(clickedContact.floID, 'unread')
hidePopup()
}
async function clearChat() {
if (await confirmation('Clear chat?', `Are you sure to clear this chat?`, 'No', "Yes")) {
messenger.clearChat(clickedContact.floID).then(result => {
getRef('messages_container').innerHTML = ''
hidePopup()
notify('Chat cleared', 'success')
})
}
}
async function deleteChat() {
if (await confirmation('Delete chat?', `Are you sure to delete this chat?`, 'No', "Yes")) {
messenger.rmChat(clickedContact.floID).then(result => {
clickedContact.chatCard.remove()
clickedContact.chatCard = ''
hidePopup()
getRef('chat').classList.add('hide-completely')
notify('Chat deleted', 'success')
})
}
}
let isGroupEditable = false
let isRemovingMember = false
function editGroupMembers() {
if (!isChatDetailsOpen && !isGroupEditable) return
if (isGroupEditable) {
getRef('group_members_list').querySelectorAll('.contact').forEach(contact => {
if (contact.classList.contains('selectable')) {
contact.classList.remove('selectable')
if (membersToRemove.has(contact.getAttribute('flo-id')))
removeTick(contact)
}
else if (contact.classList.contains('admin')) {
contact.classList.remove('hide-completely')
}
})
membersToRemove.clear()
getRef('edit_group_button').textContent = 'Edit'
getRef('remove_members_tip').classList.add('hide-completely')
getRef('member_options').classList.add('hide-completely')
isGroupEditable = false
isRemovingMember = false
}
else {
getRef('group_members_list').querySelectorAll('.contact').forEach(contact => {
if (contact.classList.contains('admin')) {
contact.classList.add('hide-completely')
}
else {
contact.classList.add('selectable')
}
})
getRef('edit_group_button').textContent = 'Done'
getRef('remove_members_tip').classList.remove('hide-completely')
getRef('member_options').classList.remove('hide-completely')
getRef('remove_members_button').classList.add('hide-completely')
getRef('init_add_members_button').classList.remove('hide-completely')
isGroupEditable = true
isRemovingMember = true
}
}
const membersToRemove = new Set()
function selectMemberToRemove(contact) {
const floID = contact.getAttribute('flo-id')
if (membersToRemove.has(floID)) {
membersToRemove.delete(floID)
removeTick(contact)
}
else {
membersToRemove.add(floID)
addTick(contact)
}
if (membersToRemove.size) {
getRef('remove_members_tip').classList.add('hide-completely')
getRef('init_add_members_button').classList.add('hide-completely')
getRef('remove_members_button').classList.remove('hide-completely')
}
else {
getRef('remove_members_tip').classList.remove('hide-completely')
getRef('init_add_members_button').classList.remove('hide-completely')
getRef('remove_members_button').classList.add('hide-completely')
}
}
const membersToAdd = new Set()
function selectMemberToAdd(contact) {
const floID = contact.getAttribute('flo-id')
if (membersToAdd.has(floID)) {
membersToAdd.delete(floID)
removeTick(contact)
}
else {
membersToAdd.add(floID)
addTick(contact)
}
if (membersToAdd.size) {
getRef('add_members_button').disabled = false
}
else {
getRef('add_members_button').disabled = true
}
}
function addTick(contact, options = {}) {
const { animate = true } = options
contact.classList.add('selected')
const initial = contact.querySelector('.initial')
const tick = document.createElement('div');
tick.innerHTML = `
<svg class="icon" viewBox="0 0 64 64">
<polyline points="0.35 31.82 21.45 52.98 63.65 10.66"/>
</svg>
`
tick.classList.add('tick')
initial.append(tick)
if (animate)
tick.animate(
[
{ transform: 'scale(0)' },
{ transform: 'scale(1)' },
],
{
duration: 300,
fill: 'forwards',
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
}
)
}
function removeTick(contact) {
contact.classList.remove('selected')
const tick = contact.querySelector('.tick')
tick.animate([
{ transform: 'scale(1)' },
{ transform: 'scale(0)' },
],
{
duration: 150,
fill: 'forwards',
}
).onfinish = () => {
tick.remove()
}
}
function addCross(contact) {
const cross = document.createElement('div');
cross.innerHTML = `
<svg class="icon" viewBox="0 0 64 64">
<title>Remove</title>
<line x1="64" y1="0" x2="0" y2="64"/>
<line x1="64" y1="64" x2="0" y2="0"/>
</svg>
`
cross.classList.add('tick')
contact.append(cross)
}
document.getElementById('add_members_button').addEventListener('clicked', addGroupMembers)
function addGroupMembers() {
messenger.addGroupMembers(activeChat.floID, [...membersToAdd])
.then(res => {
membersToAdd.forEach(member => {
frag.append(render.contactCard(member, { type: 'contact', contactOnly: true }))
})
getRef('group_members_list').append(frag)
hidePopup()
})
.catch(err => console.log(err))
}
function removeGroupMembers() {
messenger.rmGroupMembers(activeChat.floID, [...membersToRemove])
.then(res => {
getRef('group_members_list').querySelectorAll('.selected').forEach(contact => {
contact.remove()
})
editGroupMembers()
})
.catch(err => console.log(err))
}
function showPage(targetPage, subpage) {
if (subpage) {
document.querySelectorAll('.sub-page').forEach(page => page.classList.add('hide-completely'))
document.querySelectorAll(`.navbar-item`).forEach(item => item.classList.remove('active'))
const targetButton = document.querySelector(`.navbar-item[data-target="${targetPage}"]`)
targetButton.classList.add('active')
if (activePage.page) {
activePage.page.classList.add('hide-completely')
activePage.button.classList.remove('active')
}
activePage.button = targetButton
activePage.page = getRef(targetPage);
if (getRef('main_page').classList.contains('hide-completely')) {
document.querySelectorAll('.page').forEach(page => page.classList.add('hide-completely'))
getRef('main_page').classList.remove('hide-completely')
}
if (getRef('main_navbar').classList.contains('no-transformations')) {
getRef('main_navbar').classList.remove('no-transformations')
getRef('navbar_backdrop').classList.add('hide')
}
}
else {
document.querySelectorAll('.page').forEach(page => page.classList.add('hide-completely'))
switch (targetPage) {
case 'sign_in_page':
break;
}
}
getRef(targetPage).classList.remove('hide-completely')
}
function initOnBoarding() {
console.log('called')
showFrame(1)
}
function showFrame(frameNo) {
if (getRef('on_boarding_page').classList.contains('hide-completely')) {
showPage('on_boarding_page')
}
const frames = ['frame_1', 'frame_2', 'frame_3']
document.querySelectorAll('.frame').forEach(frame => frame.classList.add('hide-completely'))
getRef(frames[frameNo - 1]).classList.remove('hide-completely')
switch (frames[frameNo - 1]) {
case 'frame_2':
getRef('first_pin').focusIn()
break;
}
}
async function setPin() {
try {
const firstPin = getRef('first_pin').value
const confirmPin = getRef('confirm_pin').value
if (firstPin === confirmPin) {
let value = getRef('confirm_pin').value
getRef('pin_error').classList.add('hide-completely')
if (isPinSet) {
floDapps.securePrivKey(value).then(result => {
notify("Pin changed", 'success');
getRef('first_pin').clear()
getRef('confirm_pin').clear()
})
}
else {
floDapps.securePrivKey(value).then(result => {
getRef('first_pin').clear()
getRef('confirm_pin').clear()
isPinSet = true;
showPage('chat_page', true)
})
}
}
else {
getRef('pin_error').classList.remove('hide-completely')
getRef('pin_error').animate(
[
{
transform: 'translateX(0)'
},
{
transform: 'translateX(-1rem)'
},
{
transform: 'translateX(1rem)'
},
{
transform: 'translateX(-0.5rem)'
},
{
transform: 'translateX(0.5rem)'
},
{
transform: 'translateX(0)'
}
],
{
duration: 600,
easing: 'ease'
}
)
}
}
catch (error) {
notify("Setting pin Failed", "error", error)
}
}
document.addEventListener('colorselected', e => {
const color = e.detail.value
localStorage.setItem(`accent-color${myFloID}`, color);
document.body.style.setProperty('--accent-color', color);
})
document.getElementById('select_bg_image').addEventListener('change', function (e) {
console
compactIDB.writeData('userSettings', this.files[0], 'bgImage')
.then(async res => {
setBgImage()
notify('Background applied', 'success')
})
.catch(err => console.error(err))
})
async function setBgImage() {
try {
const image = await compactIDB.readData('userSettings', 'bgImage')
if (image) {
const url = URL.createObjectURL(image)
getRef('chat').style.background = `linear-gradient(rgba(var(--foreground-color), 0.6), rgba(var(--foreground-color), 0.6)), url(${url}) center no-repeat`
getRef('mail').style.background = `linear-gradient(rgba(var(--foreground-color), 0.6), rgba(var(--foreground-color), 0.6)), url(${url}) center no-repeat`
getRef('chat_preview').style.background = `linear-gradient(rgba(var(--foreground-color), 0.6), rgba(var(--foreground-color), 0.6)), url(${url}) center no-repeat`
getRef('chat').style.backgroundSize = 'cover'
getRef('mail').style.backgroundSize = 'cover'
getRef('chat_preview').style.backgroundSize = 'cover'
getRef('chat').classList.add('has-bg-image')
getRef('mail').classList.add('has-bg-image')
getRef('chat_preview').classList.add('has-bg-image')
getRef('selected_bg_preview').firstElementChild.src = url
getRef('selected_bg_preview').classList.add('bg-preview--selected')
getRef('selected_bg_preview').classList.remove('hide-completely')
getRef('default_bg_preview').classList.remove('bg-preview--selected')
getRef('select_bg_button').textContent = 'Change background'
}
}
catch (err) {
console.error(err)
}
}
function setDefaultBg() {
getRef('chat').style.background = ``
getRef('mail').style.background = ``
getRef('chat_preview').style.background = ``
getRef('chat').classList.remove('has-bg-image')
getRef('mail').classList.remove('has-bg-image')
getRef('chat_preview').classList.remove('has-bg-image')
getRef('selected_bg_preview').classList.remove('bg-preview--selected')
getRef('default_bg_preview').classList.add('bg-preview--selected')
notify('Default background applied', 'success')
}
</script>
</body>
</html>