eu.static.mega.co.nz
Open in
urlscan Pro
2a0b:e40:3::11
Public Scan
URL:
https://eu.static.mega.co.nz/4/js/mega-13_0b58206a3728c4a59b6294c5b820e304f70534c681ec9dc7104c029ce2ebe5ee.js
Submission: On June 18 via manual from CA — Scanned from NZ
Submission: On June 18 via manual from CA — Scanned from NZ
Form analysis
0 forms found in the DOMText Content
/* Bundle Includes: * js/fm/account.js * js/fm/account-change-password.js * js/fm/account-change-email.js * js/fm/dialogs.js * js/fm/properties.js * js/ui/dropdowns.js * js/notify.js * js/vendor/avatar.js * js/fm/affiliate.js */ function accountUI() { "use strict"; // Prevent ephemeral session to access account settings via url if (u_type === 0) { msgDialog('confirmation', l[998], l[17146] + ' ' + l[999], l[1000], function(e) { if (e) { loadSubPage('register'); return false; } loadSubPage('fm'); }); return false; } var $fmContainer = $('.fm-main', '.fmholder'); var $settingsMenu = $('.content-panel.account', $fmContainer); accountUI.$contentBlock = $('.fm-right-account-block', $fmContainer); $('.fm-account-notifications', accountUI.$contentBlock).removeClass('hidden'); $('.settings-button', $settingsMenu).removeClass('active'); $('.fm-account-sections', accountUI.$contentBlock).addClass('hidden'); $('.fm-right-files-block, .section.conversations, .fm-right-block.dashboard', $fmContainer) .addClass('hidden'); $('.nw-fm-left-icon', $fmContainer).removeClass('active'); $('.nw-fm-left-icon.settings', $fmContainer).addClass('active'); $('.account.data-block.storage-data', accountUI.$contentBlock).removeClass('exceeded'); $('.fm-account-save', accountUI.$contentBlock).removeClass('disabled'); accountUI.$contentBlock.removeClass('hidden'); if ($('.fmholder', 'body').hasClass('transfer-panel-opened')) { $.transferClose(); } M.onSectionUIOpen('account'); if (u_attr && u_attr.b && !u_attr.b.m) { $('.settings-button.slide-in-out.plan', $settingsMenu).addClass('hidden'); } else { $('.settings-button.slide-in-out.plan', $settingsMenu).removeClass('hidden'); } M.accountData((account) => { accountUI.renderAccountPage(account); // Init account content scrolling // Scrolling init/update became faster than promise based operations in "renderAccountPage" // instead or refactoring "accounts" page to return a promise in "rendering" a non noticeable // heuristic 300ms delay has been added. I believe this delay simulate the slowness which allowed // the previous logic to work. delay('settings:scrollbarinit', accountUI.initAccountScroll, 300); }, 1); } accountUI.initAccountScroll = function() { 'use strict'; const $scrollBlock = accountUI.$contentBlock || $('.fm-right-account-block'); if (!$scrollBlock.length) { return false; } if ($scrollBlock.is('.ps')) { Ps.update($scrollBlock[0]); } else { Ps.initialize($scrollBlock[0]); } }; accountUI.renderAccountPage = function(account) { 'use strict'; if (d) { console.log('Rendering account pages'); } var id = getSitePath(); if (u_attr && u_attr.b && !u_attr.b.m && id.startsWith('/fm/account/plan')) { id = '/fm/account'; } // Parse subSectionId and remove sub section part from id const accountRootUrl = '/fm/account'; let subSectionId; if (id.length > accountRootUrl.length) { let urlPart0; let urlPart1; const sectionUrl = id.substr(accountRootUrl.length + 1, id.length); const sectionUrlParts = sectionUrl.split('/'); if (sectionUrlParts.length === 1) { urlPart0 = sectionUrlParts[0]; } else { urlPart0 = sectionUrlParts[0]; urlPart1 = sectionUrlParts[1]; } const $accountSubSectionElement = $(`.settings-button.account-s .sub-title[data-scrollto='${urlPart0}']`); if ($accountSubSectionElement.length > 0) { id = accountRootUrl; subSectionId = urlPart0; } else { id = `${accountRootUrl}/${urlPart0}`; subSectionId = urlPart1; } } var sectionClass; accountUI.general.init(account); accountUI.inputs.text.init(); var showOrHideBanner = function(sectionName) { var $banner = $('.quota-banner', accountUI.$contentBlock); if (sectionName === '/fm/account' || sectionName === '/fm/account/plan' || sectionName === '/fm/account/transfers') { $banner.removeClass('hidden'); } else { $banner.addClass('hidden'); } // If Pro Flexi or Business, hide the banner if (u_attr && (u_attr.pf || u_attr.b)) { $banner.addClass('hidden'); } }; showOrHideBanner(id); // Always hide the add-phone banner if it was shown by the account profile sub page accountUI.account.profiles.hidePhoneBanner(); switch (id) { case '/fm/account': $('.fm-account-profile').removeClass('hidden'); sectionClass = 'account-s'; accountUI.account.init(account); break; case '/fm/account/plan': if ($.openAchievemetsDialog) { delete $.openAchievemetsDialog; onIdle(function() { $('.fm-account-plan .btn-achievements:visible', accountUI.$contentBlock).trigger('click'); }); } $('.fm-account-plan', accountUI.$contentBlock).removeClass('hidden'); sectionClass = 'plan'; accountUI.plan.init(account); break; case '/fm/account/security': $('.fm-account-security', accountUI.$contentBlock).removeClass('hidden'); sectionClass = 'security'; accountUI.security.init(); if ($.scrollIntoSection && $($.scrollIntoSection, accountUI.$contentBlock).length) { $($.scrollIntoSection, accountUI.$contentBlock)[0].scrollIntoView(); } break; case '/fm/account/file-management': $('.fm-account-file-management', accountUI.$contentBlock).removeClass('hidden'); sectionClass = 'file-management'; accountUI.fileManagement.init(account); break; case '/fm/account/transfers': $('.fm-account-transfers', accountUI.$contentBlock).removeClass('hidden'); sectionClass = 'transfers'; accountUI.transfers.init(account); break; case '/fm/account/contact-chats': $('.fm-account-contact-chats', accountUI.$contentBlock).removeClass('hidden'); sectionClass = 'contact-chats'; accountUI.contactAndChat.init(); break; case '/fm/account/reseller' /** && M.account.reseller **/: if (!account.reseller) { loadSubPage('fm/account'); return false; } $('.fm-account-reseller', accountUI.$contentBlock).removeClass('hidden'); sectionClass = 'reseller'; accountUI.reseller.init(account); break; case '/fm/account/notifications': $('.fm-account-notifications').removeClass('hidden'); $('.quota-banner', '.fm-account-main', accountUI.$contentBlock).addClass('hidden'); sectionClass = 'notifications'; accountUI.notifications.init(account); break; case '/fm/account/calls': $('.fm-account-calls').removeClass('hidden'); sectionClass = 'calls'; accountUI.calls.init(); break; case '/fm/account/vpn': $('.fm-account-vpn').removeClass('hidden'); sectionClass = 'vpn'; accountUI.vpn.init(); break; default: // This is the main entry point for users who just had upgraded their accounts if (isNonActivatedAccount()) { alarm.nonActivatedAccount.render(true); break; } // If user trying to use wrong url within account page, redirect them to account page. loadSubPage('fm/account'); break; } accountUI.leftPane.init(sectionClass); mBroadcaster.sendMessage('settingPageReady'); fmLeftMenuUI(); if (subSectionId) { $(`.settings-button.${sectionClass} .sub-title[data-scrollto='${subSectionId}']`).trigger('click'); } loadingDialog.hide(); }; accountUI.general = { init: function(account) { 'use strict'; clickURLs(); this.charts.init(account); this.userUIUpdate(); this.bindEvents(); }, bindEvents: function() { 'use strict'; // Upgrade Account Button $('.upgrade-to-pro', accountUI.$contentBlock).rebind('click', function() { if (u_attr && u_attr.b && u_attr.b.m && (u_attr.b.s === -1 || u_attr.b.s === 2)) { loadSubPage('repay'); } else { loadSubPage('pro'); } }); $('.download-sync', accountUI.$contentBlock).rebind('click', function() { var pf = navigator.platform.toUpperCase(); // If this is Linux send them to desktop page to select linux type if (pf.indexOf('LINUX') > -1) { mega.redirect('mega.io', 'desktop', false, false, false); } // else directly give link of the file. else { window.location = megasync.getMegaSyncUrl(); } }); }, /** * Helper function to fill common charts into the dashboard and account sections * @param {Object} account User account data (I.e. same as M.account) */ charts: { perc_c_s : 0, perc_c_b : 0, init: function(account) { 'use strict'; /* Settings and Dasboard ccontent blocks */ this.$contentBlock = $('.fm-right-block.dashboard, .fm-right-account-block', '.fm-main'); this.bandwidthChart(account); this.usedStorageChart(account); this.chartWarningNoti(account); }, bandwidthChart: function(account) { 'use strict'; /* New Used Bandwidth chart */ this.perc_c_b = account.tfsq.perc > 100 ? 100 : account.tfsq.perc; var $bandwidthChart = $('.fm-account-blocks.bandwidth', this.$contentBlock); var fullDeg = 360; var deg = fullDeg * this.perc_c_b / 100; // Used Bandwidth chart if (this.perc_c_b < 50) { $('.left-chart span', $bandwidthChart).css('transform', 'rotate(180deg)'); $('.right-chart span', $bandwidthChart).css('transform', `rotate(${180 - deg}deg)`); $('.right-chart', $bandwidthChart).addClass('low-percent-clip'); $('.left-chart', $bandwidthChart).addClass('low-percent-clip'); } else { $('.left-chart span', $bandwidthChart).css('transform', 'rotate(180deg)'); $('.right-chart span', $bandwidthChart).css('transform', `rotate(${(deg - 180) * -1}deg)`); $('.right-chart', $bandwidthChart).removeClass('low-percent-clip'); $('.left-chart', $bandwidthChart).removeClass('low-percent-clip'); } if (this.perc_c_b > 99 || dlmanager.isOverQuota) { $bandwidthChart.addClass('exceeded'); } else if (this.perc_c_b > 80) { $bandwidthChart.addClass('going-out'); } // Maximum bandwidth var b2 = bytesToSize(account.tfsq.max, 0).split(' '); var usedB = bytesToSize(account.tfsq.used); $('.chart.data .size-txt', $bandwidthChart).text(usedB); $('.chart.data .pecents-txt', $bandwidthChart).text(bytesToSize(account.tfsq.max, 3, 4)); $('.chart.data .of-txt', $bandwidthChart).text('/'); $('.account.chart.data', $bandwidthChart).removeClass('hidden'); if ((u_attr.p || account.tfsq.ach) && b2[0] > 0) { if (this.perc_c_b > 0) { $bandwidthChart.removeClass('no-percs'); $('.chart .perc-txt', $bandwidthChart).text(formatPercentage(this.perc_c_b / 100)); } else { $bandwidthChart.addClass('no-percs'); $('.chart .perc-txt', $bandwidthChart).text('---'); } } else { $bandwidthChart.addClass('no-percs'); $('.chart .perc-txt', $bandwidthChart).text('---'); $('.chart.data > span:not(.size-txt)', $bandwidthChart).text(''); var usedW; if (usedB[0] === '1') { usedW = l[17524].toLowerCase().replace('%tq1', '').trim(); } else if (usedB[0] === '2') { usedW = l[17525].toLowerCase().replace('%tq2', '').trim(); } else { usedW = l[17517].toLowerCase().replace('%tq', '').trim(); } $('.chart.data .pecents-txt', $bandwidthChart).text(usedW); } if (!account.maf) { this.$contentBlock.removeClass('active-achievements'); } else { this.$contentBlock.addClass('active-achievements'); } /* End of New Used Bandwidth chart */ }, usedStorageChart: function(account) { 'use strict'; /* New Used Storage chart */ var $storageChart = $('.fm-account-blocks.storage', this.$contentBlock); var usedPercentage = Math.floor(account.space_used / account.space * 100); this.perc_c_s = usedPercentage; if (this.perc_c_s > 100) { this.perc_c_s = 100; } $storageChart.removeClass('exceeded going-out'); if (this.perc_c_s === 100) { $storageChart.addClass('exceeded'); } else if (this.perc_c_s >= account.uslw / 100) { $storageChart.addClass('going-out'); } var fullDeg = 360; var deg = fullDeg * this.perc_c_s / 100; // Used space chart if (this.perc_c_s < 50) { $('.left-chart span', $storageChart).css('transform', 'rotate(180deg)'); $('.right-chart span', $storageChart).css('transform', `rotate(${180 - deg}deg)`); $('.right-chart', $storageChart).addClass('low-percent-clip'); $('.left-chart', $storageChart).addClass('low-percent-clip'); } else { $('.left-chart span', $storageChart).css('transform', 'rotate(180deg)'); $('.right-chart span', $storageChart).css('transform', `rotate(${(deg - 180) * -1}deg)`); $('.right-chart', $storageChart).removeClass('low-percent-clip'); $('.left-chart', $storageChart).removeClass('low-percent-clip'); } // Maximum disk space $('.chart.data .pecents-txt', $storageChart).text(bytesToSize(account.space, 0)); $('.chart .perc-txt', $storageChart).text(formatPercentage(usedPercentage / 100)); $('.chart.data .size-txt', $storageChart).text(bytesToSize(account.space_used)); $('.account.chart.data', $storageChart).removeClass('hidden'); /** End New Used Storage chart */ }, // TODO: this need to be modified to using on dashboard chartWarningNoti: function(account) { 'use strict'; var b_exceeded = this.perc_c_t > 99 || dlmanager.isOverQuota; var s_exceeded = this.perc_c_s === 100; // Charts warning notifications var $chartsBlock = $('.account.quota-banner', this.$contentBlock); $('.chart-warning:not(.hidden)', $chartsBlock).addClass('hidden'); if (b_exceeded && s_exceeded) { // Bandwidth and Storage quota exceeded $('.chart-warning.storage-and-bandwidth', $chartsBlock).removeClass('hidden'); } else if (s_exceeded) { // Storage quota exceeded $('.chart-warning.storage', $chartsBlock).removeClass('hidden'); } else if (b_exceeded) { // Bandwidth quota exceeded $('.chart-warning.bandwidth', $chartsBlock).removeClass('hidden'); } else if (this.perc_c_s >= account.uslw / 100) { // Running out of cloud space $('.chart-warning.out-of-space', $chartsBlock).removeClass('hidden'); } if (b_exceeded || s_exceeded || this.perc_c_s >= account.uslw / 100) { $('.chart-warning', $chartsBlock).rebind('click', function() { loadSubPage('pro'); }); } /* End of Charts warning notifications */ } }, /** * Update user UI (pro plan, avatar, first/last name, email) */ userUIUpdate: function() { 'use strict'; var $fmContent = $('.fm-main', '.fmholder'); var $dashboardPane = $('.content-panel.dashboard', $fmContent); // Show Membership plan $('.account .plan-icon', $dashboardPane).removeClass('pro1 pro2 pro3 pro4 pro100 pro101 free'); // Default is Free (proNum undefined) let proNum = u_attr.p; let planClass = 'free'; let planText = l[1150]; // If Business or Pro Flexi, always show the icon & name (even if expired, which is when u_attr.p is undefined) if (u_attr.b || u_attr.pf) { proNum = u_attr.b ? pro.ACCOUNT_LEVEL_BUSINESS : pro.ACCOUNT_LEVEL_PRO_FLEXI; planClass = 'pro' + proNum; planText = pro.getProPlanName(proNum); } // Otherwise if it's an active Pro account else if (proNum) { planClass = 'pro' + proNum; planText = pro.getProPlanName(proNum); } $('.account .plan-icon', $dashboardPane).addClass(planClass); $('.account.membership-plan', $dashboardPane).text(planText); // update avatar $('.fm-account-avatar', $fmContent).safeHTML(useravatar.contact(u_handle, '', 'div', false)); $('.fm-avatar', $fmContent).safeHTML(useravatar.contact(u_handle)); $('.top-menu-popup .avatar-block .wrapper', $fmContent).safeHTML(useravatar.contact(u_handle)); // Show first name or last name $('.membership-big-txt.name', $dashboardPane).text(u_attr.fullname); // Show email address if (u_attr.email) { $('.membership-big-txt.email', $dashboardPane).text(u_attr.email); } else { $('.membership-big-txt.email', $dashboardPane).addClass('hidden'); } }, }; accountUI.controls = { disableElement: function(element) { 'use strict'; $(element).addClass('disabled').prop('disabled', true); }, enableElement: function(element) { 'use strict'; $(element).removeClass('disabled').prop('disabled', false); }, }; accountUI.inputs = { text: { init: function() { 'use strict'; var $inputs = $('.underlinedText', '.fm-account-main, .fm-voucher-popup'); var megaInputs = new mega.ui.MegaInputs($inputs); } }, radio: { init: function(identifier, $container, currentValue, onChangeCb) { 'use strict'; var self = this; var $radio = $(identifier, $container); var $labels = $('.radio-txt', $container); if (String(currentValue)) { this.set(identifier, $container, currentValue); } $('input', $radio).rebind('click.radio', function() { var newVal = $(this).val(); self.set(identifier, $container, newVal, onChangeCb); }); $labels.rebind('click.radioLabel', function() { $(this).prev(identifier).find('input', $radio).trigger('click'); }); }, set: function(identifier, $container, newVal, onChangeCb) { 'use strict'; var $input = $('input' + identifier + '[value="' + newVal + '"]', $container); if ($input.is('.disabled')) { return; } $(identifier + '.radioOn', $container).addClass('radioOff').removeClass('radioOn'); $input.removeClass('radioOff').addClass('radioOn').prop('checked', true); $input.parent().addClass('radioOn').removeClass('radioOff'); if (typeof onChangeCb === 'function') { onChangeCb(newVal); } }, disable: function(value, $container) { 'use strict'; $('input.[value="' + value + '"]', $container).addClass('disabled').prop('disabled', true); }, enable: function(value, $container) { 'use strict'; $('input.[value="' + value + '"]', $container).removeClass('disabled').prop('disabled', false); }, }, radioCard: { init(identifier, $container, currentValue, onChangeCb) { 'use strict'; var $radio = $(identifier, $container); var $labels = $('.chat', $container); if (String(currentValue)) { this.set(identifier, $container, currentValue); } $('input', $radio).rebind('click.radio', (e, val) => { this.set(identifier, $container, val, onChangeCb); }); $labels.rebind('click.radioLabel', function() { var newVal = $(this).prev(identifier).children('input', $radio).val(); $(this).prev(identifier).children('input', $radio).trigger('click', newVal); }); }, set: function(identifier, $container, newVal, onChangeCb) { 'use strict'; var $input = $('input' + identifier + '[value="' + newVal + '"]', $container); $(identifier + '.radioOn', $container).addClass('radioOff').removeClass('radioOn'); $(identifier + '.radioOff', $container).next().addClass('radioOff').removeClass('radioOn'); $(identifier + '.radioOff', $container).next().children('.chat-selected').removeClass('checked'); $input.removeClass('radioOff').addClass('radioOn').prop('checked', true); $input.parent().addClass('radioOn').removeClass('radioOff'); $input.parent().next().addClass('radioOn').removeClass('radioOff'); $input.parent().next().children('.chat-selected').addClass('checked'); if (typeof onChangeCb === 'function') { onChangeCb(newVal); } } }, switch: { init: function(identifier, $container, currentValue, onChangeCb, onClickCb) { 'use strict'; var self = this; var $switch = $(identifier, $container); if ((currentValue && !$switch.hasClass('toggle-on')) || (!currentValue && $switch.hasClass('toggle-on'))) { this.toggle(identifier, $container); } Soon(function() { $('.no-trans-init', $switch).removeClass('no-trans-init'); }); $switch.rebind('click.switch', function() { var val = $switch.hasClass('toggle-on'); if (typeof onClickCb === 'function') { onClickCb(val).done(function() { self.toggle(identifier, $container, onChangeCb); }); } else { self.toggle(identifier, $container, onChangeCb); } }); }, toggle: function(identifier, $container, onChangeCb) { 'use strict'; var $switch = $(identifier, $container); var newVal; if ($switch.hasClass('toggle-on')) { $switch.removeClass('toggle-on'); newVal = 0; } else { $switch.addClass('toggle-on'); newVal = 1; } $switch.trigger('update.accessibility'); if (typeof onChangeCb === 'function') { onChangeCb(newVal); } } } }; accountUI.leftPane = { init: function(sectionClass) { 'use strict'; this.render(sectionClass); this.bindEvents(); }, render: function(sectionClass) { 'use strict'; var $settingsPane = $('.content-panel.account', '.fm-main'); var $menuItems = $('.settings-button', $settingsPane); var $currentMenuItem = $menuItems.filter('.' + sectionClass); if (M.account.reseller) { // Show reseller button on naviation $menuItems.filter('.reseller').removeClass('hidden'); } if (accountUI.plan.paymentCard.validateUser(M.account)) { $('.acc-setting-menu-card-info', $menuItems).removeClass('hidden'); } else { $('.acc-setting-menu-card-info', $menuItems).addClass('hidden'); } $menuItems.filter(':not(.' + sectionClass + ')').addClass('closed').removeClass('active'); $currentMenuItem.addClass('active'); setTimeout(function() { $currentMenuItem.removeClass('closed'); initTreeScroll(); }, 600); }, getPageUrlBySection: function($section) { 'use strict'; switch (true) { case $section.hasClass('account-s'): return 'fm/account'; case $section.hasClass('plan'): return 'fm/account/plan'; case $section.hasClass('notifications'): return 'fm/account/notifications'; case $section.hasClass('security'): return 'fm/account/security'; case $section.hasClass('file-management'): return 'fm/account/file-management'; case $section.hasClass('transfers'): return 'fm/account/transfers'; case $section.hasClass('contact-chats'): return 'fm/account/contact-chats'; case $section.hasClass('reseller'): return 'fm/account/reseller'; case $section.hasClass('calls'): return 'fm/account/calls'; case $section.hasClass('vpn'): return 'fm/account/vpn'; default: return 'fm/account'; } }, bindEvents: function() { 'use strict'; var $settingsPane = $('.content-panel.account', '.fm-main'); $('.settings-button', $settingsPane).rebind('click', function() { const $this = $(this); if (!$this.hasClass('active')) { accountUI.$contentBlock.scrollTop(0); loadSubPage(accountUI.leftPane.getPageUrlBySection($this)); } }); $('.settings-button i.expand', $settingsPane).rebind('click', function(e) { var $button = $(this).closest('.settings-button'); e.stopPropagation(); $button.toggleClass('closed'); }); $('.settings-button .sub-title', $settingsPane).rebind('click', function() { const $this = $(this); const $parentBtn = $this.closest('.settings-button'); const dataScrollto = $this.attr('data-scrollto'); const $target = $(`.data-block.${dataScrollto}`); const parentPage = accountUI.leftPane.getPageUrlBySection($parentBtn); const page = `${parentPage}/${dataScrollto}`; const isHidden = $target.hasClass('hidden'); if (isHidden) { // display: block removes an element from DOM, so we need to mimic it's location for a bit $target.addClass('v-hidden').removeClass('hidden'); } const targetPosition = $target.position().top; if (isHidden) { $target.addClass('hidden').removeClass('v-hidden'); } if ($parentBtn.hasClass('active')) { accountUI.$contentBlock.animate({ scrollTop: targetPosition }, 500); pushHistoryState(page); } else { $parentBtn.trigger('click'); mBroadcaster.once('settingPageReady', function () { pushHistoryState(page); accountUI.$contentBlock.animate({scrollTop: $target.position().top}, 500); }); } }); } }; accountUI.account = { init: function(account) { 'use strict'; var $settingsPane = $('.content-panel.account', '.fm-main'); var $profileContent = $('.settings-sub-section.profile', accountUI.$contentBlock); // Profile this.profiles.resetProfileForm(); this.profiles.renderPhoneBanner().catch(dump); this.profiles.renderFirstName(); this.profiles.renderLastName(); this.profiles.renderBirth(); this.profiles.renderPhoneDetails(); // if this is a business user, we want to hide some parts in profile page :) var hideOrViewCancelSection = function(setToHidden) { if (setToHidden) { $('.cancel-account-block', accountUI.$contentBlock).addClass('hidden'); $('.acc-setting-menu-cancel-acc', $settingsPane).addClass('hidden'); $('#account-firstname', $profileContent).prop('disabled', true); $('#account-lastname', $profileContent).prop('disabled', true); } else { $('.cancel-account-block', accountUI.$contentBlock).removeClass('hidden'); $('.acc-setting-menu-cancel-acc', $settingsPane).removeClass('hidden'); $('#account-firstname', $profileContent).prop('disabled', false); $('#account-lastname', $profileContent).prop('disabled', false); } }; if (u_attr && u_attr.b) { $('.acc-setting-country-sec', $profileContent).addClass('hidden'); if (!u_attr.b.m) { hideOrViewCancelSection(true); } else { $('.cancel-account-block .content-txt.bus-acc', accountUI.$contentBlock).removeClass('hidden'); hideOrViewCancelSection(false); } } else { // user can set country only in non-business accounts $('.acc-setting-country-sec', $profileContent).removeClass('hidden'); this.profiles.renderCountry(); // we allow cancel for only non-business account + master users. hideOrViewCancelSection(false); } this.profiles.bindEvents(); // QR Code this.qrcode.render(account); this.qrcode.bindEvents(); // Preference this.preference.render(); // Cancel Account this.cancelAccount.bindEvents(); }, profiles: { hidePhoneBanner: async function() { 'use strict'; $('.add-phone-num-banner', accountUI.$contentBlock).addClass('hidden'); }, /** * Render a banner at the top of the My Account section for enticing a user to add their phone number * so that they can get an achievement bonus and link up with their phone contacts that might be on MEGA */ renderPhoneBanner: async function() { 'use strict'; // Cache selectors var $addPhoneBanner = $('.add-phone-num-banner', accountUI.$contentBlock); var $usageBanner = $('.quota-banner', accountUI.$contentBlock); var $text = $('.add-phone-text', $addPhoneBanner); var $addPhoneButton = $('.js-add-phone-button', $addPhoneBanner); var $skipButton = $('.skip-button', $addPhoneBanner); var $notAgainCheckbox = $('.notagain', $addPhoneBanner); // M.maf is cached in its getter, however, repeated gets will cause unnecessary checks. var ach = M.maf; const hideOrDisplayBanner = () => { // If not Business/Pro Flexi, show the standard storage/bandwidth usage banner instead of phone banner if (typeof u_attr.b === 'undefined' && typeof u_attr.pf === 'undefined') { $usageBanner.removeClass('hidden'); $addPhoneBanner.addClass('hidden'); } else { // Otherwise for Business or Pro Flexi accounts hide both banners $usageBanner.addClass('hidden'); $addPhoneBanner.addClass('hidden'); } }; // If SMS verification enable is not on level 2 (Opt-in and unblock SMS allowed) then do nothing. Or if // they already have already added a phone number then don't show this banner again. Or if they clicked the // skip button then don't show the banner. if (u_attr.flags.smsve !== 2 || typeof u_attr.smsv !== 'undefined' || fmconfig.skipsmsbanner || ach && ach[9] && ach[9].rwd) { hideOrDisplayBanner(); return false; } const phoneBannerTimeChecker = await mega.TimeChecker.PhoneBanner.init( () => { return !$addPhoneBanner.hasClass('hidden'); } ); if (!phoneBannerTimeChecker || phoneBannerTimeChecker && !phoneBannerTimeChecker.shouldShow() && !phoneBannerTimeChecker.hasUpdated() ) { hideOrDisplayBanner(); return false; } // On click of the Add Number button load the add phone dialog $addPhoneButton.rebind('click.phonebanner', () => { sms.phoneInput.init(); }); $notAgainCheckbox.rebind('click.phonebanner', () => { const $input = $('.checkinput', $notAgainCheckbox); const $checkboxDiv = $('.checkdiv', $notAgainCheckbox); // If unticked, tick the box if ($input.hasClass('checkboxOff')) { $input.removeClass('checkboxOff').addClass('checkboxOn').prop('checked', true); $checkboxDiv.removeClass('checkboxOff').addClass('checkboxOn'); } else { // Otherwise untick the box $input.removeClass('checkboxOn').addClass('checkboxOff').prop('checked', false); $checkboxDiv.removeClass('checkboxOn').addClass('checkboxOff'); } return false; }); sms.renderAddPhoneText($text); // Show the phone banner, hide the storage/bandwidth usage banner $usageBanner.addClass('hidden'); $addPhoneBanner.removeClass('hidden'); if (phoneBannerTimeChecker) { if (!phoneBannerTimeChecker.hasUpdated()) { phoneBannerTimeChecker.update(); } if (phoneBannerTimeChecker.isMoreThan10Times()) { $notAgainCheckbox.removeClass('hidden'); } $skipButton.removeClass('hidden'); // Show the skip button // On click of the Skip button, hide the banner and don't show it again $skipButton.rebind('click.phonebanner', () => { phoneBannerTimeChecker.update(); // Hide the banner $addPhoneBanner.addClass('hidden'); // Save in fmconfig so it is not shown again on reload or login on different machine const notAgain = $('.checkinput', $notAgainCheckbox).prop('checked'); if (notAgain) { mega.config.set('skipsmsbanner', 1); } }); } // If a Business / Pro Flexi account, permanently hide the usage and phone banners if (typeof u_attr.b !== 'undefined' || typeof u_attr.pf !== 'undefined') { $usageBanner.addClass('hidden'); $addPhoneBanner.addClass('hidden'); } }, renderFirstName: function() { 'use strict'; $('#account-firstname', accountUI.$contentBlock).val(u_attr.firstname).trigger('blur'); }, renderLastName: function() { 'use strict'; $('#account-lastname', accountUI.$contentBlock).val(u_attr.lastname).trigger('blur'); }, renderBirth: function () { 'use strict'; // If $.dateTimeFormat['stucture'] is not set, prepare it for birthday if (!$.dateTimeFormat.structure) { $.dateTimeFormat.structure = getDateStructure() || 'ymd'; } // Display only date format that is correct with current locale. $('.mega-input.birth', accountUI.$contentBlock).addClass('hidden'); $('.mega-input.birth.' + $.dateTimeFormat.structure, accountUI.$contentBlock) .removeClass('hidden'); this.renderBirthYear(); this.renderBirthMonth(); this.renderBirthDay(); }, renderBirthYear: function() { 'use strict'; var i = new Date().getFullYear() - 16; var formatClass = '.' + $.dateTimeFormat.structure + ' .byear'; var $input = $('.mega-input.birth' + formatClass, accountUI.$contentBlock) .attr('max', i); if (u_attr.birthyear) { $input.val(u_attr.birthyear).trigger('input'); } }, renderBirthMonth: function() { 'use strict'; if (u_attr.birthmonth) { var formatClass = '.' + $.dateTimeFormat.structure + ' .bmonth'; var $input = $('.mega-input.title-ontop.birth' + formatClass, accountUI.$contentBlock); $input.val(u_attr.birthmonth).trigger('input'); if ($input.length) { this.zerofill($input[0]); } } }, renderBirthDay: function() { 'use strict'; if (u_attr.birthday) { var formatClass = '.' + $.dateTimeFormat.structure + ' .bdate'; var $input = $('.mega-input.title-ontop.birth' + formatClass, accountUI.$contentBlock); $input.val(u_attr.birthday).trigger('input'); if ($input.length) { this.zerofill($input[0]); } } }, renderCountry: function() { 'use strict'; const $country = $('#account-country', accountUI.$contentBlock); createDropdown($country, { placeholder: $('span', $country).text(l[996]), items: M.getCountries(), selected: u_attr.country }); // Bind Dropdowns events bindDropdownEvents($country, 1); }, /** * Show the phone number section if applicable */ renderPhoneDetails: function() { 'use strict'; // If SMS Verification Enable is on level 1 (SMS suspended unlock allowed only) and they've verified // by phone already, show the section and number. Or if SMS Verification Enable is on level 2 (Opt-in SMS // allowed), then show the section (and number if added, or an Add button). if ((u_attr.flags.smsve === 1 && typeof u_attr.smsv !== 'undefined') || u_attr.flags.smsve === 2) { // Cache selectors var $content = $('.fm-account-main', accountUI.$contentBlock); var $phoneSettings = $('.phone-number-settings', $content); var $text = $('.add-phone-text', $phoneSettings); var $phoneNumber = $('.phone-number', $phoneSettings); var $addNumberButton = $('.add-number-button', $phoneSettings); var $buttonsContainer = $('.gsm-mod-rem-btns', $content); var $removeNumberButton = $('.rem-gsm', $buttonsContainer); var $modifyNumberButton = $('.modify-gsm', $buttonsContainer); // If the phone is already added, show that if (typeof u_attr.smsv !== 'undefined') { $addNumberButton.addClass('hidden'); $text.addClass('hidden'); $buttonsContainer.removeClass('hidden'); $phoneNumber.removeClass('hidden').text(u_attr.smsv); $removeNumberButton.rebind('click.gsmremove', () => { msgDialog('confirmation', '', l[23425], l[23426], answer => { if (answer) { accountUI.account.profiles.removePhoneNumber(true).catch(dump); } }); }); $modifyNumberButton.rebind('click.gsmmodify', () => { msgDialog('confirmation', '', l[23429], l[23430], answer => { if (answer) { sms.phoneInput.init(); } }); }); } else { $addNumberButton.removeClass('hidden'); $text.removeClass('hidden'); $buttonsContainer.addClass('hidden'); $phoneNumber.addClass('hidden').text(''); // On click of the Add Number button load the add phone dialog $addNumberButton.rebind('click', function() { sms.phoneInput.init(); }); } // Show the section $phoneSettings.removeClass('hidden'); } }, zerofill: function(elem) { 'use strict'; if (elem.value.length === 1) { elem.value = '0' + elem.value; } }, /** * Send remove command to API, and update UI if needed * @param {Boolean} showSuccessMsg Show message dialog on success */ removePhoneNumber: promisify((resolve, reject, showSuccessMsg) => { 'use strict'; // lock UI loadingDialog.show(); api_req( { a: 'smsr' }, { callback: tryCatch(res => { // Unlock UI regardless of the result loadingDialog.hide(); if (res === 0) { // success // no APs, we need to rely on this response. delete u_attr.smsv; // update only relevant sections in UI accountUI.account.profiles.renderPhoneBanner(); accountUI.account.profiles.renderPhoneDetails(); if (showSuccessMsg) { msgDialog('info', '', l[23427]); } resolve(); } else { msgDialog('warningb', '', l[23428]); reject(res); } }, () => { loadingDialog.hide(); msgDialog('warningb', '', l[23428]); reject('Failed to remove the phone number.'); }) } ); }), resetProfileForm: function() { 'use strict'; var $personalInfoBlock = $('.profile-form', accountUI.$contentBlock); var $saveBlock = $('.fm-account-sections .save-block', accountUI.$contentBlock); $('input', $personalInfoBlock).val(''); $('.error, .errored', $personalInfoBlock).removeClass('error errored'); $saveBlock.addClass('closed'); }, bindEvents: function() { 'use strict'; // Cache selectors var self = this; var $personalInfoBlock = $('.profile-form', accountUI.$contentBlock); var $birthdayBlock = $('.mega-input.title-ontop.birth.' + $.dateTimeFormat.structure, $personalInfoBlock); var $firstNameField = $('#account-firstname', $personalInfoBlock); var $lastNameField = $('#account-lastname', $personalInfoBlock); var $countryDropdown = $('#account-country', $personalInfoBlock); var $saveBlock = $('.fm-account-sections .save-block', accountUI.$contentBlock); var $saveButton = $('.fm-account-save', $saveBlock); // Avatar $('.avatar-wrapper, .settings-sub-section.avatar .avatar', $personalInfoBlock) .rebind('click.showDialog', function() { avatarDialog(); }); // All profile text inputs $firstNameField.add($lastNameField).add('.byear, .bmonth, .bdate', $birthdayBlock) .rebind('input.settingsGeneral change.settingsGeneral', function() { var $this = $(this); var $parent = $this.parent(); var errorMsg = l[20960]; var max = parseInt($this.attr('max')); var min = parseInt($this.attr('min')); if ($this.is('.byear, .bmonth, .bdate')) { if (this.value > max || this.value < min) { if ($this.is('.byear') && this.value > max && this.value === u_attr.birthyear) { // To omit the case that users already set invalid year value // before implied the restrictions return true; } $this.addClass('errored'); $parent.addClass('error msg'); var $msg = $('.message-container', $parent).text(errorMsg); $parent.css('margin-bottom', $msg.outerHeight() + 20 + 'px'); $saveBlock.addClass('closed'); return false; } else { $this.removeClass('errored'); var $erroredInput = $parent.find('.errored'); if ($erroredInput.length){ $($erroredInput[0]).trigger('change'); } else { $parent.removeClass('error msg'); $parent.css('margin-bottom', ''); } } } var enteredFirst = $firstNameField.val().trim(); var enteredLast = $lastNameField.val().trim(); if (enteredFirst.length > 0 && enteredLast.length > 0 && !$('.errored', $personalInfoBlock).length && (enteredFirst !== u_attr.firstname || enteredLast !== u_attr.lastname || ($('.bdate', $birthdayBlock).val() | 0) !== (u_attr.birthday | 0) || ($('.bmonth', $birthdayBlock).val() | 0) !== (u_attr.birthmonth | 0) || ($('.byear', $birthdayBlock).val() | 0) !== (u_attr.birthyear | 0))) { $saveBlock.removeClass('closed'); } else { $saveBlock.addClass('closed'); } }); $('.byear, .bmonth, .bdate', $birthdayBlock).rebind('keydown.settingsGeneral', function(e) { var $this = $(this); var charCode = e.which || e.keyCode; // ff var $parent = $this.parent(); var max = parseInt($this.attr('max')); var min = parseInt($this.attr('min')); if (!e.shiftkey && !((charCode >= 48 && charCode <= 57) || (charCode >= 96 && charCode <= 105)) && (charCode !== 8 && charCode !== 9 && charCode !== 37 && charCode !== 39)){ e.preventDefault(); } if (charCode === 38) { if (!this.value || parseInt(this.value) < parseInt(min)) { this.value = min; } else if (parseInt(this.value) >= parseInt(max)) { this.value = max; } else { this.value++; } $parent.removeClass('error'); $this.removeClass('errored').trigger('change'); self.zerofill(this); } if (charCode === 40) { if (parseInt(this.value) <= parseInt(min)) { this.value = min; } else if (!this.value || parseInt(this.value) > parseInt(max)) { this.value = max; } else { this.value--; } $parent.removeClass('error'); $this.removeClass('errored').trigger('change'); self.zerofill(this); } }); $('.bmonth, .bdate', $birthdayBlock).rebind('blur.settingsGeneral', function() { self.zerofill(this); }); $('.birth-arrow', $personalInfoBlock).rebind('click', function() { var $this = $(this); var $target = $this.parent('.birth-arrow-container').prev('input'); var e = $.Event('keydown.settingsGeneral'); e.which = $this.hasClass('up-control') ? 38 : 40; $target.trigger(e); }); $('.mega-input-dropdown .option', $countryDropdown).rebind('click.showSave', function() { if ($firstNameField.val() && $firstNameField.val().trim().length > 0 && !$personalInfoBlock.find('.errored').length) { $saveBlock.removeClass('closed'); } else { $saveBlock.addClass('closed'); } }); $saveButton.rebind('click', function() { if ($(this).hasClass('disabled')) { return false; } const $bd = $('.bdate', $birthdayBlock); const $bm = $('.bmonth', $birthdayBlock); const $by = $('.byear', $birthdayBlock); const bd = $bd.val(); const bm = $bm.val(); const by = $by.val(); // Check whether the birthday info gets changed const bd_old = u_attr.birthday || ''; const bm_old = u_attr.birthmonth || ''; const by_old = u_attr.birthyear || ''; const birthdayChanged = bd_old !== bd || bm_old !== bm || by_old !== by; if (birthdayChanged && M.validateDate(parseInt(bd), parseInt(bm), parseInt(by)) !== 0) { const $parent = $bd.parent().addClass('error msg'); var $msg = $('.message-container', $parent).text(l[20960]); $parent.css('margin-bottom', `${$msg.outerHeight() + 20}px`); $saveBlock.addClass('closed'); return false; } if ($('.bdate', $birthdayBlock).val()) $('.fm-account-avatar').safeHTML(useravatar.contact(u_handle, '', 'div', false)); $('.fm-avatar').safeHTML(useravatar.contact(u_handle)); var checklist = { firstname: String($('#account-firstname', $personalInfoBlock).val() || '').trim(), lastname: String($('#account-lastname', $personalInfoBlock).val() || '').trim(), birthday: String(bd || ''), birthmonth: String(bm || ''), birthyear: String(by || ''), country: String( getDropdownValue($('#account-country', $personalInfoBlock)) ) }; var userAttrRequest = { a: 'up' }; var checkUpdated = function() { var result = false; for (var i in checklist) { if (u_attr[i] === null || u_attr[i] !== checklist[i]) { // we want also to catch the 'undefined' or null // and replace with the empty string (or given string) u_attr[i] = i === 'firstname' ? checklist[i] || 'Nobody' : checklist[i]; userAttrRequest[i] = base64urlencode(to8(u_attr[i])); result = true; } } return result; }; if (checkUpdated()) { api_req(userAttrRequest, { callback: function (res) { if (res === u_handle) { $('.user-name').text(u_attr.name); $('.name', '.account-dialog').text(u_attr.fullname) .attr('data-simpletip', u_attr.fullname); $('.top-menu-logged .name', '.top-menu-popup').text(u_attr.name); showToast('settings', l[7698]); accountUI.account.profiles.bindEvents(); // update file request username for existing folder mega.fileRequest.onUpdateUserName(u_attr.fullname); } } }); } // Reset current Internationalization API usage upon save. onIdle(function() { mega.intl.reset(); }); $saveBlock.addClass('closed'); $saveButton.removeClass('disabled'); }); }, }, qrcode: { $QRSettings: null, render: function(account) { 'use strict'; this.$QRSettings = $('.qr-settings', accountUI.$contentBlock); var QRoptions = { width: 106, height: 106, // high correctLevel: QRErrorCorrectLevel.H, background: '#f2f2f2', foreground: '#151412', text: getBaseUrl() + '/' + account.contactLink }; var defaultValue = (account.contactLink && account.contactLink.length); $('.qr-http-link', this.$QRSettings).text(QRoptions.text); var $container = $('.enable-qr-container', this.$QRSettings); if (defaultValue) { // Render the QR code $('.account.qr-icon', this.$QRSettings).text('').qrcode(QRoptions); $('.mega-switch.enable-qr', this.$QRSettings).addClass('toggle-on').trigger('update.accessibility'); $('.access-qr-container', this.$QRSettings).parent().removeClass('closed'); $('.qr-block', this.$QRSettings).removeClass('hidden'); $container.addClass('border'); } else { $('.account.qr-icon').text(''); $('.mega-switch.enable-qr', this.$QRSettings).removeClass('toggle-on').trigger('update.accessibility'); $('.access-qr-container', this.$QRSettings).parent().addClass('closed'); $('.qr-block', this.$QRSettings).addClass('hidden'); $container.removeClass('border'); } // Enable QR code accountUI.inputs.switch.init( '.enable-qr', $container, defaultValue, function(val) { if (val) { $('.access-qr-container', accountUI.account.qrcode.$QRSettings) .add('.qr-block', accountUI.account.qrcode.$QRSettings) .parent().removeClass('closed'); api_req({ a: 'clc' }, { myAccount: account, callback: function (res, ctx) { ctx.myAccount.contactLink = typeof res === 'string' ? 'C!' + res : ''; accountUI.account.qrcode.render(M.account); } }); } else { $('.access-qr-container', accountUI.account.qrcode.$QRSettings) .add('.qr-settings .qr-block').parent().addClass('closed'); api_req({ a: 'cld', cl: account.contactLink.substring(2, account.contactLink.length) }, { myAccount: account, callback: function (res, ctx) { if (res === 0) { // success ctx.myAccount.contactLink = ''; } } }); } }, function(val) { var promise = new MegaPromise(); // If it is toggle off, warn user. if (val) { msgDialog('confirmation', l[19990], l[20128], l[18229], function (answer) { if (answer) { promise.resolve(); } else { promise.reject(); } }); } else { // It is toggle on, just proceed; promise.resolve(); } return promise; }); // Automatic accept section mega.attr.get(u_handle, 'clv', -2, 0).always(function(res) { accountUI.inputs.switch.init( '.auto-qr', $('.access-qr-container', accountUI.account.qrcode.$QRSettings), parseInt(res), function(val) { mega.attr.set('clv', val, -2, 0); }); }); }, bindEvents: function() { 'use strict'; // Reset Section $('.reset-qr-label', this.$QRSettings).rebind('click', accountUI.account.qrcode.reset); // Copy link Section if (is_extension || M.execCommandUsable()) { $('.copy-qr-link', this.$QRSettings).removeClass('hidden'); $('.qr-dlg-cpy-lnk', this.$QRSettings).rebind('click', function() { var links = $.trim($(this).next('.qr-http-link').text()); var toastTxt = l[7654]; copyToClipboard(links, toastTxt); }); } else { $('.copy-qr-link', this.$QRSettings).addClass('hidden'); } }, reset: function() { 'use strict'; msgDialog('confirmation', l[18227], l[18228], l[18229], function (regenQR) { if (regenQR) { loadingDialog.show(); var delQR = { a: 'cld', cl: M.account.contactLink.substring(2, M.account.contactLink.length) }; var reGenQR = { a: 'clc' }; api_req(delQR, { callback: function (res) { if (res === 0) { // success api_req(reGenQR, { callback: function (res2) { if (typeof res2 !== 'string') { res2 = ''; } else { res2 = 'C!' + res2; } M.account.contactLink = res2; accountUI.account.qrcode.render(M.account); loadingDialog.hide(); } }); } else { loadingDialog.hide(); } } }); } }); } }, preference: { render: function() { 'use strict'; var self = this; // Date/time format setting accountUI.inputs.radio.init( '.uidateformat', $('.uidateformat', accountUI.$contentBlock).parent(), fmconfig.uidateformat || 0, function (val) { mega.config.setn('uidateformat', parseInt(val), l[16168]); } ); // Font size accountUI.inputs.radio.init( '.uifontsize', $('.uifontsize', accountUI.$contentBlock).parent(), fmconfig.font_size || 2, function (val) { $('body').removeClass('fontsize1 fontsize2').addClass('fontsize' + val); mega.config.setn('font_size', parseInt(val), l[16168]); } ); // Theme accountUI.inputs.radio.init( '.uiTheme', $('.uiTheme', accountUI.$contentBlock).parent(), u_attr['^!webtheme'] || 0, function(val) { mega.attr.set('webtheme', val, -2, 1); mega.ui.theme.set(val); } ); self.initHomePageDropdown(); }, /** * Render and bind events for the home page dropdown. * @returns {void} */ initHomePageDropdown: function() { 'use strict'; var $hPageSelect = $('.settings-choose-homepage-dropdown', accountUI.$contentBlock); var $textField = $('span', $hPageSelect); // Mark active item. var $activeItem = $('.option[data-value="' + getLandingPage() + '"]', $hPageSelect); $activeItem.addClass('active'); $textField.text($activeItem.text()); // Bind Dropdowns events bindDropdownEvents($hPageSelect, 1); $('.option', $hPageSelect).rebind('click.saveChanges', function() { var $selectedOption = $('.option[data-state="active"]', $hPageSelect); var newValue = $selectedOption.attr('data-value') || 'fm'; showToast('settings', l[16168]); setLandingPage(newValue); }); }, }, cancelAccount: { bindEvents: function() { 'use strict'; // Cancel account button on main Account page $('.cancel-account').rebind('click', function() { // Please confirm that all your data will be deleted var confirmMessage = l[1974]; if (u_attr.b && u_attr.b.m) { confirmMessage = l.bus_acc_delete_confirm_msg; } // Search through their Pro plan purchase history for (var i = 0; i < M.account.purchases.length; i++) { // Get payment method name var paymentMethodId = M.account.purchases[i][4]; var paymentMethod = pro.getPaymentGatewayName(paymentMethodId).name; // If they have paid with iTunes or Google Play in the past if (paymentMethod === 'apple' || paymentMethod === 'google') { // Update confirmation message to remind them to cancel iTunes or Google Play confirmMessage += ' ' + l[8854]; break; } } /** * Finalise the account cancellation process * @param {String|null} twoFactorPin The 2FA PIN code or null if not applicable */ var continueCancelAccount = function(twoFactorPin) { // Prepare the request var request = { a: 'erm', m: Object(M.u[u_handle]).m, t: 21 }; // If 2FA PIN is set, add it to the request if (twoFactorPin !== null) { request.mfa = twoFactorPin; } api_req(request, { callback: function(res) { loadingDialog.hide(); // Check for invalid 2FA code if (res === EFAILED || res === EEXPIRED) { msgDialog('warninga', l[135], l[19216]); } // Check for incorrect email else if (res === ENOENT) { msgDialog('warningb', l[1513], l[1946]); } else if (res === 0) { handleResetSuccessDialogs( l.ac_closure_email_sent_msg.replace('%s', u_attr.email), 'deleteaccount' ); } else { msgDialog('warningb', l[135], l[200]); } } }); }; // Ask for confirmation msgDialog('confirmation', l[6181], confirmMessage, false, function(event) { if (event) { loadingDialog.show(); // Check if 2FA is enabled on their account twofactor.isEnabledForAccount(function(result) { loadingDialog.hide(); // If 2FA is enabled on their account if (result) { // Show the verify 2FA dialog to collect the user's PIN twofactor.verifyActionDialog.init(function(twoFactorPin) { continueCancelAccount(twoFactorPin); }); } else { continueCancelAccount(null); } }); } }); }); } } }; accountUI.plan = { init: function(account) { "use strict"; const $planContent = $('.fm-account-plan.fm-account-sections', accountUI.$contentBlock); // Plan - Account type this.accountType.render(account); this.accountType.bindEvents(); // Plan - Account Balance this.balance.render(account); this.balance.bindEvents(); // Plan - History this.history.renderPurchase(account); this.history.renderTransaction(account); this.history.bindEvents(account); // Plan - Payment card this.paymentCard.init(account, $planContent); // check if business account if (u_attr && u_attr.b) { if (!u_attr.b.m || u_attr.b.s === -1) { $('.acc-storage-space', $planContent).addClass('hidden'); $('.acc-bandwidth-vol', $planContent).addClass('hidden'); } $('.btn-achievements', $planContent).addClass('hidden'); $('.data-block.account-balance', $planContent).addClass('hidden'); $('.acc-setting-menu-balance-acc', '.content-panel.account').addClass('hidden'); $('.upgrade-to-pro', $planContent).addClass('hidden'); // If Business Master account and if Expired or in Grace Period, show the Reactive Account button if (u_attr.b.m && u_attr.b.s !== pro.ACCOUNT_STATUS_ENABLED) { $('.upgrade-to-pro', $planContent).removeClass('hidden'); $('.upgrade-to-pro span', $planContent).text(l.reactivate_account); } } // If Pro Flexi if (u_attr && u_attr.pf) { // Hide the Upgrade Account button and Account Balance section on the Plan page $('.upgrade-to-pro', $planContent).addClass('hidden'); $('.data-block.account-balance', $planContent).addClass('hidden'); // Hide Storage space and Transfer quota blocks like Business (otherwise shows 4096 PB which is incorrect) if (u_attr.pf.s === pro.ACCOUNT_STATUS_EXPIRED) { $('.acc-storage-space', $planContent).addClass('hidden'); $('.acc-bandwidth-vol', $planContent).addClass('hidden'); } // If Expired or in Grace Period, show the Reactive Account button if (u_attr.pf.s !== pro.ACCOUNT_STATUS_ENABLED) { $('.upgrade-to-pro', $planContent).removeClass('hidden'); $('.upgrade-to-pro span', $planContent).text(l.reactivate_account); } } }, accountType: { render: function(account) { 'use strict'; var $planContent = $('.data-block.account-type', accountUI.$contentBlock); var renderSubscription = function _renderSubscription() { // Get the date their subscription will renew var timestamp = (account.srenew.length > 0) ? account.srenew[0] : 0; // Timestamp e.g. 1493337569 var paymentType = (account.sgw.length > 0) ? account.sgw[0] : ''; // Credit Card etc var gatewayId = (account.sgwids.length > 0) ? account.sgwids[0] : null; // Gateway ID e.g. 15, etc if (paymentType.indexOf('Credit Card') === 0) { paymentType = paymentType.replace('Credit Card', l[6952]); } // Display the date their subscription will renew if known if (timestamp > 0) { var dateString = time2date(timestamp, 2); // Use format: 14 March 2015 - Credit Card paymentType = dateString + ' - ' + paymentType; // Change placeholder 'Expires on' to 'Renews' $('.subtitle-txt.expiry-txt', $planContent).text(l[6971]); $('.account.plan-info.expiry', $planContent).text(paymentType); } else { // Otherwise show nothing $('.account.plan-info.expiry', $planContent).text(''); $('.subtitle-txt.expiry-txt', $planContent).text(''); } var $subscriptionBlock = $('.sub-container.subscription', $planContent); var $cancelSubscriptionButton = $('.btn-cancel-sub', $subscriptionBlock); var $achievementsButton = $('.btn-achievements', $planContent); if (!M.maf){ $achievementsButton.addClass('hidden'); } // If Apple or Google subscription (see pro.getPaymentGatewayName function for codes) if ((gatewayId === 2) || (gatewayId === 3)) { // Tell them they need to cancel their plan off-site and don't show the feedback dialog $subscriptionBlock.removeClass('hidden'); $cancelSubscriptionButton.rebind('click', function() { msgDialog('warninga', l[7179], l[16501]); }); } // Otherwise if ECP, Sabadell, or Stripe else if (gatewayId === 16 || gatewayId === 17 || gatewayId === 19) { // Check if there are any active subscriptions // ccqns = Credit Card Query Number of Subscriptions api_req({ a: 'ccqns' }, { callback: function(numOfSubscriptions) { // If there is an active subscription if (numOfSubscriptions > 0) { // Show cancel button and show cancellation dialog $subscriptionBlock.removeClass('hidden'); $cancelSubscriptionButton.rebind('click', function() { accountUI.plan.accountType.cancelSubscriptionDialog.init(); }); } } }); } }; if (u_attr.p) { // LITE/PRO account var planNum = u_attr.p; var planText = pro.getProPlanName(planNum); // if this is p=100 business if (planNum === pro.ACCOUNT_LEVEL_BUSINESS) { $('.account.plan-info.accounttype', $planContent).addClass('business'); $('.fm-account-plan .acc-renew-date-info', $planContent).removeClass('border'); } else { $('.account.plan-info.accounttype', $planContent).removeClass('business'); $('.fm-account-plan .acc-renew-date-info', $planContent).addClass('border'); } // Account type $('.account.plan-info.accounttype span', $planContent).text(planText); $('.account .plan-icon', $planContent).addClass('pro' + planNum); // Subscription if (account.stype === 'S') { renderSubscription(); } else if (account.stype === 'O') { var expiryTimestamp = account.nextplan ? account.nextplan.t : account.expiry; // one-time or cancelled subscription $('.subtitle-txt.expiry-txt', $planContent).text(l[987]); $('.account.plan-info.expiry span', $planContent).text(time2date(expiryTimestamp, 2)); $('.sub-container.subscription', $planContent).addClass('hidden'); } $('.account.plan-info.bandwidth', $planContent).parent().removeClass('hidden'); } else { // free account: $('.account.plan-info.accounttype span', $planContent).text(l[1150]); $('.account .plan-icon', $planContent).addClass('free'); $('.account.plan-info.expiry', $planContent).text(l[436]); $('.sub-container.subscription', $planContent).addClass('hidden'); if (account.mxfer) { $('.account.plan-info.bandwidth', $planContent).parent().removeClass('hidden'); } else { $('.account.plan-info.bandwidth', $planContent).parent().addClass('hidden'); } } // If Business or Pro Flexi, override to show the name (even if expired i.e. when u_attr.p is undefined) if (u_attr.b || u_attr.pf) { $('.account.plan-info.accounttype', $planContent).addClass('business'); $('.account.plan-info.accounttype span', $planContent).text( pro.getProPlanName(u_attr.b ? pro.ACCOUNT_LEVEL_BUSINESS : pro.ACCOUNT_LEVEL_PRO_FLEXI) ); } /* achievements */ if (!account.maf || (u_attr.p === pro.ACCOUNT_LEVEL_BUSINESS && u_attr.b && u_attr.b.m) || (u_attr.p === pro.ACCOUNT_LEVEL_PRO_FLEXI && u_attr.pf)) { $('.btn-achievements', $planContent).addClass('hidden'); // If active Business master account or active Pro Flexi account if ((u_attr.p === pro.ACCOUNT_LEVEL_BUSINESS && u_attr.b && u_attr.b.m) || (u_attr.p === pro.ACCOUNT_LEVEL_PRO_FLEXI && u_attr.pf)) { // Debug code ... if (d && localStorage.debugNewPrice) { M.account.space_bus_base = 3; M.account.space_bus_ext = 2; M.account.tfsq_bus_base = 3; M.account.tfsq_bus_ext = 1; M.account.tfsq_bus_used = 3848290697216; // 3.5 TB M.account.space_bus_used = 4617948836659; // 4.2 TB } // END Debug code const renderBars = (used, base, extra, $container, msg, $overall) => { let spaceTxt = `${bytesToSize(used)}`; let baseTxt = spaceTxt; let storageConsume = used / 1048576; // MB let storageQuota = (base || 3) * 1048576; // MB let extraTxt = l[5816].replace('[X]', base || 3); if (base) { spaceTxt += `/${l[5816] .replace('[X]', base + (extra || 0))}`; if (extra) { storageConsume = base; storageQuota = base + extra; baseTxt = extraTxt; extraTxt = msg.replace('%1', extra); } } $('.settings-sub-bar', $container) .css('width', `${100 - storageConsume * 100 / storageQuota}%`); $('.base-quota-note span', $container).text(baseTxt); $('.achieve-quota-note span', $container).text(extraTxt); $overall.text(spaceTxt); }; const $storageContent = $('.acc-storage-space', $planContent); const $bandwidthContent = $('.acc-bandwidth-vol', $planContent); renderBars(M.account.space_bus_used || M.account.space_used, M.account.space_bus_base, M.account.space_bus_ext, $storageContent, l.additional_storage, $('.plan-info.storage > span', $planContent)); renderBars(M.account.tfsq_bus_used || M.account.tfsq.used, M.account.tfsq_bus_base, M.account.tfsq_bus_ext, $bandwidthContent, l.additional_transfer, $('.plan-info.bandwidth > span', $planContent)); $('.bars-container', $planContent).removeClass('hidden'); } else { $('.plan-info.storage > span', $planContent).text(bytesToSize(M.account.space, 0)); $('.plan-info.bandwidth > span', $planContent).text(bytesToSize(M.account.tfsq.max, 0)); $('.bars-container', $planContent).addClass('hidden'); } } else { mega.achievem.parseAccountAchievements(); } }, bindEvents: function() { "use strict"; $('.btn-achievements', accountUI.$contentBlock).rebind('click', function() { mega.achievem.achievementsListDialog(); }); }, /** * Dialog to cancel subscriptions */ cancelSubscriptionDialog: { $backgroundOverlay: null, $dialog: null, $dialogSuccess: null, $accountPageCancelButton: null, $options: null, $formContent: null, $selectReasonDialog: null, $invalidDetailsDialog: null, $skipContinueButton: null, $continueButton: null, $textareaAndErrorDialog: null, $textarea: null, $cancelReason: null, $expiryTextBlock: null, $expiryDateBlock: null, /** * Initialise the dialog */ init: function() { 'use strict'; // Cache some selectors this.$dialog = $('.cancel-subscription-st1'); this.$dialogSuccess = $('.cancel-subscription-st2'); this.$accountPageCancelButton = $('.btn-cancel-sub'); this.$options = this.$dialog.find('.label-wrap'); this.$formContent = this.$dialog.find('section.content'); this.$selectReasonDialog = this.$dialog.find('.error-banner.select-reason'); this.$invalidDetailsDialog = this.$dialog.find('.error-banner.invalid-details'); this.$skipContinueButton = this.$dialog.find('.skip-cancel-subscription'); this.$continueButton = this.$dialog.find('.cancel-subscription'); this.$textareaAndErrorDialog = this.$dialog.find('.textarea-and-banner'); this.$textarea = this.$dialog.find('textarea'); this.$cancelReason = $('.cancel-textarea-bl', this.$textareaAndErrorDialog); this.$backgroundOverlay = $('.fm-dialog-overlay'); this.$expiryTextBlock = $('.account.plan-info.expiry-txt'); this.$expiryDateBlock = $('.account.plan-info.expiry'); // Show the dialog this.$dialog.removeClass('hidden'); this.$backgroundOverlay.removeClass('hidden').addClass('payment-dialog-overlay'); // Init textarea scrolling initTextareaScrolling($('.cancel-textarea textarea', this.$dialog)); // Init section scrolling if (this.$formContent.is('.ps')) { this.$formContent.scrollTop(0); Ps.destroy(this.$formContent[0]); } // Init functionality this.resetCancelSubscriptionForm(); this.checkReasonEnteredIsValid(); this.initClickReason(); this.initCloseAndDontCancelButtons(); this.$continueButton.add(this.$skipContinueButton).rebind('click', (e) => { const $buttonClicked = $(e.currentTarget); this.sendSubCancelRequestToApi($buttonClicked.hasClass('skip-cancel-subscription')); }); }, /** * Reset the form incase the user changes their mind and closes it, * so they always see a blank form if they choose to cancel their subscription again */ resetCancelSubscriptionForm: function() { 'use strict'; this.$selectReasonDialog.addClass('hidden'); this.$invalidDetailsDialog.addClass('hidden'); this.$textareaAndErrorDialog.addClass('hidden'); this.$dialog.removeClass('textbox-open'); this.$cancelReason.removeClass('error'); this.$textarea.val(''); $('.cancel-option', this.$options).addClass('radioOff').removeClass('radioOn'); }, /** * Close the dialog when either the close or "Don't cancel" buttons are clicked */ initCloseAndDontCancelButtons: function() { 'use strict'; var self = this; // Close main dialog self.$dialog.find('button.dont-cancel, button.js-close').rebind('click', () => { self.$dialog.addClass('hidden'); self.$backgroundOverlay.addClass('hidden').removeClass('payment-dialog-overlay'); }); }, /** * Set the radio button classes when a radio button or its text are clicked */ initClickReason: function() { 'use strict'; this.$options.rebind('click', (e) => { const $option = $(e.currentTarget); const value = $('input', $option).val(); const valueIsOtherOption = value === "8"; $('.cancel-option', this.$options).addClass('radioOff').removeClass('radioOn'); $('.cancel-option', $option).addClass('radioOn').removeClass('radioOff'); this.$selectReasonDialog.addClass('hidden'); this.$dialog.removeClass('select-reason'); this.$textareaAndErrorDialog.toggleClass('hidden', !valueIsOtherOption); this.$dialog.toggleClass('textbox-open', valueIsOtherOption); if (valueIsOtherOption) { Ps.initialize(this.$formContent[0]); this.$invalidDetailsDialog.toggleClass('hidden', !(this.$cancelReason.hasClass('error'))); this.$formContent.scrollTop(this.$formContent.height()); this.$textarea.trigger('focus'); } else { if (this.$formContent.is('.ps')) { this.$formContent.scrollTop(0); Ps.destroy(this.$formContent[0]); } this.$invalidDetailsDialog.addClass('hidden'); this.$textarea.trigger('blur'); } }); }, /** * Close success dialog */ initCloseButtonSuccessDialog: function() { 'use strict'; var self = this; self.$dialogSuccess.find('button.js-close').rebind('click', () => { self.$dialogSuccess.addClass('hidden'); self.$backgroundOverlay.addClass('hidden').removeClass('payment-dialog-overlay'); }); }, /** * Check the user has entered between 1 and 1000 characters into the text field */ checkReasonEnteredIsValid: function() { 'use strict'; var self = this; self.$textarea.rebind('keyup', function() { // Trim for spaces var reason = $(this).val(); reason = $.trim(reason); const responseIsValid = reason.length > 0 && reason.length <= 1000; // Make sure response is between 1 and 1000 characters if (responseIsValid) { self.$invalidDetailsDialog.addClass('hidden'); self.$cancelReason.removeClass('error'); } else { self.showTextareaError(!reason.length); } }); }, /** * Show the user an error message below the text field if their input is * invalid, or too long */ showTextareaError: function(emptyReason) { 'use strict'; var self = this; self.$invalidDetailsDialog.removeClass('hidden'); if (emptyReason) { self.$invalidDetailsDialog.text(l.cancel_sub_empty_textarea_error_msg); } else { self.$invalidDetailsDialog.text(l.cancel_sub_too_much_textarea_input_error_msg); } self.$cancelReason.addClass('error'); self.$formContent.scrollTop(self.$formContent.height()); }, /** * Send the subscription cancellation request to the API */ sendSubCancelRequestToApi: function(skippedReason) { 'use strict'; var reason = 'No reason (user skipped the survey)'; if (!skippedReason) { const $optionSelected = $('.cancel-option.radioOn', this.$options); if (!($optionSelected.length)) { this.$selectReasonDialog.removeClass('hidden'); this.$dialog.addClass('select-reason'); return; } const value = $('input', $optionSelected).val(); const radioText = $('.radio-txt', $optionSelected.parent()).text().trim(); // The cancellation reason (r) sent to the API is the radio button text, or // when the chosen option is "Other (please provide details)" it is // what the user enters in the text field if (value === "8") { reason = this.$textarea.val().trim(); if (!reason.length || reason.length > 1000) { this.showTextareaError(!reason.length); return; } } else { reason = value + ' - ' + radioText; } } // Hide the dialog and show loading spinner this.$dialog.addClass('hidden'); this.$backgroundOverlay.addClass('hidden').removeClass('payment-dialog-overlay'); loadingDialog.show(); // Setup standard request to 'cccs' = Credit Card Cancel Subscriptions const requests = [ { a: 'cccs', r: reason } ]; // If they were Pro Flexi, we need to also downgrade the user from Pro Flexi to Free if (u_attr && u_attr.pf) { requests.push({ a: 'urpf' }); } // Cancel the subscription/s api_req(requests, { callback: () => { // Hide loading dialog and cancel subscription button on // account page, set expiry date loadingDialog.hide(); this.$accountPageCancelButton.addClass('hidden'); this.$expiryTextBlock.text(l[987]); this.$expiryDateBlock .safeHTML('<span class="red">@@</span>', time2date(account.expiry, 2)); // Show success dialog this.$dialogSuccess.removeClass('hidden'); this.$backgroundOverlay.removeClass('hidden'); this.$backgroundOverlay.addClass('payment-dialog-overlay'); this.initCloseButtonSuccessDialog(); // Reset account cache so all account data will be refetched // and re-render the account page UI M.account.lastupdate = 0; accountUI(); } }); } } }, paymentCard: { $paymentSection: null, validateCardResponse: function(res) { 'use strict'; return res && (res.gw === (addressDialog || {}).gatewayId_stripe || 19) && res.brand && res.last4 && res.exp_month && res.exp_year; }, validateUser: function(account) { 'use strict'; return (u_attr.p || u_attr.b) && account.stype === 'S' && ((Array.isArray(account.sgw) && account.sgw.includes('Stripe')) || (Array.isArray(account.sgwids) && account.sgwids.includes((addressDialog || {}).gatewayId_stripe || 19))); }, init: function(account, $planSection) { 'use strict'; this.$paymentSection = $('.account.account-card-info', $planSection); const hideCardSection = () => { this.$paymentSection.addClass('hidden'); $('.settings-button .acc-setting-menu-card-info', '.content-panel.account') .addClass('hidden'); }; // check if we should show the section (uq response) if (this.validateUser(account)) { api_req({ a: 'cci' }, { callback: (res) => { if (typeof res === 'object' && this.validateCardResponse(res)) { return this.render(res); } hideCardSection(); } }); } else { hideCardSection(); } }, render: function(cardInfo) { 'use strict'; if (cardInfo && this.$paymentSection) { if (cardInfo.brand === 'visa') { this.$paymentSection.addClass('visa').removeClass('mc'); $('.payment-card-icon i', this.$paymentSection) .removeClass('sprite-fm-uni icon-mastercard-border'); } else if (cardInfo.brand === 'mastercard') { this.$paymentSection.addClass('mc').removeClass('visa'); $('.payment-card-icon i', this.$paymentSection).addClass('sprite-fm-uni icon-mastercard-border'); } else { this.$paymentSection.removeClass('visa mc'); } $('.payment-card-nb .payment-card-digits', this.$paymentSection).text(cardInfo.last4); $('.payment-card-expiry .payment-card-expiry-val', this.$paymentSection) .text(`${String(cardInfo.exp_month).padStart(2, '0')}/${String(cardInfo.exp_year).substr(-2)}`); $('.payment-card-bottom a.payment-card-edit', this.$paymentSection).rebind('click', () => { loadingDialog.show(); api_req({ a: 'gw19_ccc' }, { callback: (res) => { loadingDialog.hide(); if ($.isNumeric(res)) { msgDialog('warninga', '', l.edit_card_error.replace('%1', res), l.edit_card_error_des); } else if (typeof res === 'string') { addressDialog.processUtcResult( { EUR: res, edit: true }, true ); } } }); }); this.$paymentSection.removeClass('hidden'); } } }, balance: { render: function(account) { "use strict"; $('.account.plan-info.balance span', accountUI.$contentBlock).safeHTML( '€ @@', mega.intl.number.format(account.balance[0][0]) ); }, bindEvents: function() { "use strict"; var self = this; $('.redeem-voucher', accountUI.$contentBlock).rebind('click', function() { var $this = $(this); if ($this.attr('class').indexOf('active') === -1) { $('.fm-account-overlay').fadeIn(100); $this.addClass('active'); $('.fm-voucher-popup').removeClass('hidden'); $('.fm-account-overlay, .fm-purchase-voucher, .fm-voucher-button') .add('.fm-voucher-popup button.js-close') .rebind('click.closeDialog', function() { $('.fm-account-overlay').fadeOut(100); $('.redeem-voucher').removeClass('active'); $('.fm-voucher-popup').addClass('hidden'); }); } else { $('.fm-account-overlay').fadeOut(200); $this.removeClass('active'); $('.fm-voucher-popup').addClass('hidden'); } }); $('.fm-voucher-button').rebind('click.voucherBtnClick', function() { var $input = $('.fm-voucher-body input'); var code = $input.val(); $input.val(''); loadingDialog.show(); $('.fm-voucher-popup').addClass('hidden'); M.require('redeem_js') .then(function() { return redeem.redeemVoucher(code); }) .then(function() { Object(M.account).lastupdate = 0; onIdle(accountUI); }) .catch(function(ex) { loadingDialog.hide(); if (ex) { if (ex === ETOOMANY) { ex = l.redeem_etoomany; } msgDialog('warninga', l[135], l[47], ex); } }); }); $('.fm-purchase-voucher, button.topup').rebind('click', function() { mega.redirect('mega.io', 'resellers', false, false, false); }); } }, history: { renderPurchase: function(account) { 'use strict'; var $purchaseSelect = $('.dropdown-input.purchases', accountUI.$contentBlock); if (!$.purchaselimit) { $.purchaselimit = 10; } $('span', $purchaseSelect).text(mega.icu.format(l[469], $.purchaselimit)); $('.purchase10-', $purchaseSelect).text(mega.icu.format(l[469], 10)); $('.purchase100-', $purchaseSelect).text(mega.icu.format(l[469], 100)); $('.purchase250-', $purchaseSelect).text(mega.icu.format(l[469], 250)); M.account.purchases.sort(function(a, b) { if (a[1] < b[1]) { return 1; } else { return -1; } }); $('.data-table.purchases tr', accountUI.$contentBlock).remove(); var html = '<tr><th>' + l[476] + '</th><th>' + l[475] + '</th><th>' + l[477] + '</th><th>' + l[478] + '</th></tr>'; if (account.purchases.length) { // Render every purchase made into Purchase History on Account page $(account.purchases).each(function(index, purchaseTransaction) { if (index === $.purchaselimit) { return false;// Break the loop } // Set payment method const paymentMethodId = purchaseTransaction[4]; const paymentMethod = pro.getPaymentGatewayName(paymentMethodId).displayName; // Set Date/Time, Item (plan purchased), Amount, Payment Method const dateTime = time2date(purchaseTransaction[1]); const price = formatCurrency(purchaseTransaction[2], 'EUR', 'narrowSymbol'); const proNum = purchaseTransaction[5]; let planIcon; const numOfMonths = purchaseTransaction[6]; const monthWording = numOfMonths === 1 ? l[931] : l[6788]; const item = `${pro.getProPlanName(proNum)} (${numOfMonths} ${monthWording})`; if (proNum === pro.ACCOUNT_LEVEL_PRO_LITE) { planIcon = 'icon-crest-lite'; } else if (proNum === pro.ACCOUNT_LEVEL_BUSINESS) { planIcon = 'icon-crest-business'; } else if (proNum === pro.ACCOUNT_LEVEL_PRO_FLEXI) { planIcon = 'icon-crest-pro-flexi'; } else { planIcon = 'icon-crest-pro-' + proNum; } // Render table row html += '<tr>' + '<td><div class="label-with-icon">' + '<i class="sprite-fm-uni ' + planIcon + '"></i>' + '<span> ' + item + '</span>' + '</div></td>' + '<td><span>' + dateTime + '</span></td>' + '<td><span>' + escapeHTML(price) + '</span></td>' + '<td><span>' + paymentMethod + '</span></td>' + '</tr>'; }); } else { html += '<tr><td colspan="4" class="data-table-empty"><span>' + l[20140] + '</span></td></tr>'; } $('.data-table.purchases', accountUI.$contentBlock).safeHTML(html); }, renderTransaction: function(account) { 'use strict'; var $transactionSelect = $('.dropdown-input.transactions', accountUI.$contentBlock); if (!$.transactionlimit) { $.transactionlimit = 10; } $('span', $transactionSelect).text(mega.icu.format(l[471], $.transactionlimit)); $('.transaction10-', $transactionSelect).text(mega.icu.format(l[471], 10)); $('.transaction100-', $transactionSelect).text(mega.icu.format(l[471], 100)); $('.transaction250-', $transactionSelect).text(mega.icu.format(l[471], 250)); M.account.transactions.sort(function(a, b) { if (a[1] < b[1]) { return 1; } else { return -1; } }); $('.data-table.transactions tr', accountUI.$contentBlock).remove(); var html = '<tr><th>' + l[475] + '</th><th>' + l[484] + '</th><th>' + l[485] + '</th><th>' + l[486] + '</th></tr>'; if (account.transactions.length) { var intl = mega.intl.number; $(account.transactions).each(function(i, el) { if (i === $.transactionlimit) { return false; } var credit = ''; var debit = ''; if (el[2] > 0) { credit = '<span class="green-label">€' + escapeHTML(intl.format(el[2])) + '</span>'; } else { debit = '<span class="red-label">€' + escapeHTML(intl.format(el[2])) + '</span>'; } html += '<tr><td>' + time2date(el[1]) + '</td><td>' + htmlentities(el[0]) + '</td><td>' + credit + '</td><td>' + debit + '</td></tr>'; }); } else { html += '<tr><td colspan="4" class="data-table-empty">' + l[20140] + '</td></tr>'; } $('.data-table.transactions', accountUI.$contentBlock).safeHTML(html); }, bindEvents: function() { 'use strict'; var $planSection = $('.fm-account-plan', accountUI.$contentBlock); var $planSelects = $('.dropdown-input', $planSection); // Bind Dropdowns events bindDropdownEvents($planSelects); $('.mega-input-dropdown .option', $planSection).rebind('click.accountSection', function() { var c = $(this).attr('class') ? $(this).attr('class') : ''; if (c.indexOf('purchase10-') > -1) { $.purchaselimit = 10; } else if (c.indexOf('purchase100-') > -1) { $.purchaselimit = 100; } else if (c.indexOf('purchase250-') > -1) { $.purchaselimit = 250; } if (c.indexOf('transaction10-') > -1) { $.transactionlimit = 10; } else if (c.indexOf('transaction100-') > -1) { $.transactionlimit = 100; } else if (c.indexOf('transaction250-') > -1) { $.transactionlimit = 250; } accountUI(); }); } } }; accountUI.notifications = { helpURL: 'chats-meetings/meetings/enable-notification-browser-system-permission', permissions: { granted: 'granted', denied: 'denied', pending: 'default' }, init: function() { 'use strict'; this.render(); this.handleChatNotifications(); }, render: function() { 'use strict'; // Ensure the loading dialog stays open till enotif is finished. loadingDialog.show('enotif'); // New setting need to force cloud and contacts notification available. if (!mega.notif.has('enabled', 'cloud')) { mega.notif.set('enabled', 'cloud'); } if (!mega.notif.has('enabled', 'contacts')) { mega.notif.set('enabled', 'contacts'); } // Handle account notification switches const { notif } = mega; const $notificationContent = $('.fm-account-notifications', accountUI.$contentBlock); const $NToggleAll = $('.account-notification .mega-switch.toggle-all', $notificationContent); const $NToggle = $('.account-notification .switch-container .mega-switch', $notificationContent); // Toggle individual notifications for (let i = $NToggle.length; i--;) { const el = $NToggle[i]; const sectionEl = $(el).closest('.switch-container'); const section = accountUI.notifications.getSectionName(sectionEl); accountUI.inputs.switch.init( `#${el.id}`, sectionEl, notif.has(el.getAttribute('name'), section), val => { notif[val ? 'set' : 'unset'](el.getAttribute('name'), section); if (val) { $NToggleAll.addClass('toggle-on'); if (section === 'chat') { this.renderNotification(); } } else { ($NToggle.hasClass('toggle-on') ? $.fn.addClass : $.fn.removeClass) .apply($NToggleAll, ['toggle-on']); } $NToggleAll.trigger('update.accessibility'); } ); } // Toggle All Notifications accountUI.inputs.switch.init( '#' + $NToggleAll[0].id, $NToggleAll.parent(), $NToggle.hasClass('toggle-on'), function(val) { $NToggle.each(function() { var $this = $(this); var $section = $this.closest('.switch-container'); var sectionName = accountUI.notifications.getSectionName($section); var notifChange = val ? mega.notif.set : mega.notif.unset; notifChange($this.attr('name'), sectionName); (val ? $.fn.addClass : $.fn.removeClass).apply($this, ['toggle-on']); $this.trigger('update.accessibility'); }); } ); // Hide achievements toggle if achievements not an option for this user. if (!M.account.maf) { $('#enotif-achievements').closest('.switch-container').remove(); } // Handle email notification switches. var $EToggleAll = $('.email-notification .mega-switch.toggle-all', $notificationContent); var $EToggle = $('.email-notification .switch-container .mega-switch', $notificationContent); mega.enotif.all().then(function(enotifStates) { // Toggle Individual Emails $EToggle.each(function() { var $this = $(this); var $section = $this.closest('.switch-container'); var emailId = $this.attr('name'); accountUI.inputs.switch.init( '#' + this.id, $section, !enotifStates[emailId], function(val) { mega.enotif.setState(emailId, !val); (val || $EToggle.hasClass('toggle-on') ? $.fn.addClass : $.fn.removeClass) .apply($EToggleAll, ['toggle-on']); $EToggleAll.trigger('update.accessibility'); } ); }); // All Email Notifications Switch accountUI.inputs.switch.init( '#' + $EToggleAll[0].id, $EToggleAll.closest('.settings-sub-section'), $EToggle.hasClass('toggle-on'), function(val) { mega.enotif.setAllState(!val); (val ? $.fn.addClass : $.fn.removeClass).apply($EToggle, ['toggle-on']); $EToggle.trigger('update.accessibility'); } ); if (accountUI.plan.paymentCard.validateUser(M.account)) { $('.switch-container.card-exp-switch', $notificationContent).removeClass('hidden'); } // Hide the loading screen. loadingDialog.hide('enotif'); }); }, getSectionName: function($section) { 'use strict'; var section = String($section.attr('class')).split(" ").filter(function(c) { return ({ 'chat': 1, 'contacts': 1, 'cloud-drive': 1 })[c]; }); return String(section).split('-').shift(); }, renderNotification() { 'use strict'; return new Notification(l.notification_granted_title, { body: l.notification_granted_body }); }, onPermissionsGranted() { 'use strict'; msgDialog( 'info', '', l.notifications_permissions_granted_title, l.notifications_permissions_granted_info .replace( '[A]', `<a href="${l.mega_help_host}/${this.helpURL}" target="_blank" class="clickurl">` ) .replace('[/A]', '</a>') ); this.renderNotification(); }, requestPermission() { 'use strict'; Notification.requestPermission() .then(permission => { if (permission === this.permissions.granted) { this.onPermissionsGranted(); } mBroadcaster.sendMessage('meetings:notificationPermissions', permission); }) .then(() => this.handleChatNotifications()) .catch(ex => d && console.warn(`Failed to retrieve permissions: ${ex}`)); }, handleChatNotifications() { 'use strict'; const $container = $('.switch-container.chat', accountUI.$contentBlock); const $banner = $('.chat-permissions-banner', $container); const $body = $('.versioning-body-text', $banner); if (mega.notif.has('enabled', 'chat')) { const { permission } = Notification; const { granted, denied, pending } = this.permissions; Object.values(this.permissions).forEach(p => $banner.removeClass(`permission--${p}`)); // Toggle the inline browser permissions banner based // on the current browser permissions state switch (true) { case permission === granted: $body.safeHTML( l.notification_settings_granted .replace( '[A]', `<a href="${l.mega_help_host}/${this.helpURL}" target="_blank" class="clickurl notif-help"> ` ) .replace('[/A]', '</a>') ); $banner.addClass(`permission--${granted}`); break; case permission === denied: $body.safeHTML( l.notifications_permissions_denied_info .replace( '[A]', `<a href="${l.mega_help_host}/${this.helpURL}" target="_blank" class="clickurl notif-help"> ` ) .replace('[/A]', '</a>') ); $banner.addClass(`permission--${denied} warning-template`); break; default: $body.safeHTML( l.notification_settings_pending .replace( '[1]', `<a href="${l.mega_help_host}/${this.helpURL}" target="_blank" class="clickurl notif-help"> ` ) .replace('[/1]', '</a>') .replace('[2]', '<a href="#" class="request-notification-permissions">') .replace('[/2]', '</a>') ); $('.request-notification-permissions', $banner).rebind('click', () => this.requestPermission()); $banner.addClass(`permission--${pending}`); break; } return $banner.removeClass('hidden'); } // Don't display the browser permissions banner if // `Chat notifications` are disabled return $banner.addClass('hidden'); } }; accountUI.security = { init: function() { "use strict"; // Change Password accountChangePassword.init(); // Change Email if (!u_attr.b || u_attr.b.m) { $('.fm-account-security.fm-account-sections .data-block.change-email').removeClass('hidden'); $('.content-panel.account .acc-setting-menu-change-em').removeClass('hidden'); accountChangeEmail.init(); } else { $('.fm-account-security.fm-account-sections .data-block.change-email').addClass('hidden'); $('.content-panel.account .acc-setting-menu-change-em').addClass('hidden'); } // Recovery Key this.recoveryKey.bindEvents(); // Metadata this.metadata.render(); // Session this.session.render(); this.session.bindEvents(); // 2fa twofactor.account.init(); }, recoveryKey: { bindEvents: function() { 'use strict'; // Button on main Account page to backup their master key $('.fm-account-security .backup-master-key').rebind('click', function() { M.showRecoveryKeyDialog(2); }); } }, metadata: { render: function() { 'use strict'; accountUI.inputs.switch.init( '.dbDropOnLogout', $('.personal-data-container'), fmconfig.dbDropOnLogout, function(val) { mega.config.setn('dbDropOnLogout', val); }); // Initialise the Download personal data button on the /fm/account/security page gdprDownload.initDownloadDataButton('personal-data-container'); } }, session: { /** * Rendering session history table. * With session data from M.account.sessions, render table for session history */ render: function() { "use strict"; var $securitySection = $('.fm-account-security', accountUI.$contentBlock); var $sessionSelect = $('.dropdown-input.sessions', $securitySection); if (d) { console.log('Render session history'); } if (!$.sessionlimit) { $.sessionlimit = 10; } $('span', $sessionSelect).text(mega.icu.format(l[472], $.sessionlimit)); $('.session10-', $sessionSelect).text(mega.icu.format(l[472], 10)); $('.session100-', $sessionSelect).text(mega.icu.format(l[472], 100)); $('.session250-', $sessionSelect).text(mega.icu.format(l[472], 250)); M.account.sessions.sort(function(a, b) { if (a[7] !== b[7]) { return a[7] > b[7] ? -1 : 1; } if (a[5] !== b[5]) { return a[5] > b[5] ? -1 : 1; } return a[0] < b[0] ? 1 : -1; }); $('#sessions-table-container', $securitySection).empty(); var html = '<table width="100%" border="0" cellspacing="0" cellpadding="0" class="data-table sessions">' + '<tr><th>' + l[19303] + '</th><th>' + l[480] + '</th><th>' + l[481] + '</th><th>' + l[482] + '</th>' + '<th class="no-border session-status">' + l[7664] + '</th>' + '<th class="no-border logout-column"> </th></tr>'; var numActiveSessions = 0; for (i = 0; i < M.account.sessions.length; i++) { var session = M.account.sessions[i]; var currentSession = session[5]; var activeSession = session[7]; // If the current session or active then increment count if (currentSession || activeSession) { numActiveSessions++; } if (i >= $.sessionlimit) { continue; } html += this.getHtml(session); } $('#sessions-table-container', $securitySection).safeHTML(html + '</table>'); // Don't show button to close other sessions if there's only the current session if (numActiveSessions === 1) { $('.fm-close-all-sessions', $securitySection).addClass('hidden'); } else { $('.fm-close-all-sessions', $securitySection).removeClass('hidden'); } }, bindEvents: function() { 'use strict'; var $securitySection = $('.fm-account-security', accountUI.$contentBlock); var $sessionSelect = $('.dropdown-input.sessions', $securitySection); // Bind Dropdowns events bindDropdownEvents($sessionSelect); $('.fm-close-all-sessions', $securitySection).rebind('click.accountSection', function() { msgDialog('confirmation', '', l[18513], false, function(e) { if (e) { loadingDialog.show(); var $activeSessionsRows = $('.active-session-txt', $securitySection).parents('tr'); // Expire all sessions but not the current one api_req({a: 'usr', ko: 1}, { callback: function() { M.account = null; /* clear account cache */ $('.settings-logout', $activeSessionsRows).remove(); $('.active-session-txt', $activeSessionsRows) .removeClass('active-session-txt').addClass('expired-session-txt').text(l[25016]); $('.fm-close-all-sessions', $securitySection).addClass('hidden'); loadingDialog.hide(); } }); } }); }); $('.settings-logout', $securitySection).rebind('click.accountSection', function() { var $this = $(this).parents('tr'); var sessionId = $this.attr('class'); if (sessionId === 'current') { mLogout(); } else { loadingDialog.show(); /* usr - user session remove * remove a session Id from the current user, * usually other than the current session */ api_req({a: 'usr', s: [sessionId]}, { callback: function() { M.account = null; /* clear account cache */ $this.find('.settings-logout').remove(); $this.find('.active-session-txt').removeClass('active-session-txt') .addClass('expired-session-txt').text(l[25016]); loadingDialog.hide(); } }); } }); $('.mega-input-dropdown .option', $securitySection).rebind('click.accountSection', function() { var c = $(this).attr('class') ? $(this).attr('class') : ''; if (c.indexOf('session10-') > -1) { $.sessionlimit = 10; } else if (c.indexOf('session100-') > -1) { $.sessionlimit = 100; } else if (c.indexOf('session250-') > -1) { $.sessionlimit = 250; } accountUI(); }); }, /** * Get html of one session data for session history table. * @param {Object} el a session data from M.account.sessions * @return {String} html * When draw session hitory table make html for each session data */ getHtml: function(el) { "use strict"; var currentSession = el[5]; var activeSession = el[7]; var userAgent = el[2]; var dateTime = htmlentities(time2date(el[0])); var browser = browserdetails(userAgent); var browserName = browser.nameTrans; var ipAddress = htmlentities(el[3]); var country = countrydetails(el[4]); var sessionId = el[6]; var status = '<span class="status-label green">' + l[7665] + '</span>'; // Current // Show if using an extension e.g. "Firefox on Linux (+Extension)" if (browser.isExtension) { browserName += ' (+' + l[7683] + ')'; } // If not the current session if (!currentSession) { if (activeSession) { status = '<span class="status-label blue">' + l[23754] + '</span>'; // Logged-in } else { status = '<span class="status-label">' + l[25016] + '</span>'; // Expired } } // If unknown country code use question mark png if (!country.icon || country.icon === '??.png') { country.icon = 'ud.png'; } // Generate row html var html = '<tr class="' + (currentSession ? "current" : sessionId) + '">' + '<td><div class="label-with-icon"><img title="' + escapeHTML(userAgent.replace(/\s*megext/i, '')) + '" src="' + staticpath + 'images/browser-icons/' + browser.icon + '" /><span title="' + htmlentities(browserName) + '">' + htmlentities(browserName) + '</span></div></td>' + '<td><span class="break-word" title="' + ipAddress + '">' + ipAddress + '</span></td>' + '<td><div class="label-with-icon"><img alt="" src="' + staticpath + 'images/flags/' + country.icon + '" title="' + htmlentities(country.name) + '" /><span>' + htmlentities(country.name) + '</span></div></td>' + '<td><span>' + dateTime + '</span></td>' + '<td>' + status + '</td>'; // If the session is active show logout button if (activeSession) { html += '<td>' + '<button class="mega-button small top-login-button settings-logout">' + '<div><i class="sprite-fm-mono icon-logout"></i></div><span>' + l[967] + '</span>' + '</button></td></tr>'; } else { html += '<td> </td></tr>'; } return html; }, /** * Update Session History table html. * If there is any new session history found (or forced), re-render session history table. * @param {Boolean} force force update the table. */ update: function(force) { "use strict"; if (page === 'fm/account/security') { // if first item in sessions list is not match existing Dom list, it need update. if (d) { console.log('Updating session history table'); } M.refreshSessionList(function() { var fSession = M.account.sessions[0]; var domList = document.querySelectorAll('.data-table.sessions tr'); // update table when it has new active session or forced if (fSession && (($(domList[1]).hasClass('current') && !fSession[5]) || !$(domList[1]).hasClass(fSession[6])) || force) { if (d) { console.log('Update session history table'); } accountUI.security.session.render(); accountUI.security.session.bindEvents(); } }); } } } }; accountUI.fileManagement = { init: function(account) { 'use strict'; // File versioning this.versioning.render(); this.versioning.bindEvents(); // Rubbish cleaning schedule this.rubsched.render(account); this.rubsched.bindEvents(account); // User Interface this.userInterface.render(); // Hide Recents this.hideRecents.render(); // Drag and Drop this.dragAndDrop.render(); // Delete confirmation this.delConfirm.render(); // Password reminder dialog this.passReminder.render(); // Chat related dialogs (multiple option but share attribute) this.chatDialogs.render(); // Pro expiry this.proExpiry.render(); // Public Links this.publicLinks.render(); }, versioning: { render: function() { 'use strict'; // Update versioning info var setVersioningAttr = function(val) { showToast('settings', l[16168]); val = val === 1 ? 0 : 1; mega.attr.set('dv', val, -2, true).done(function() { fileversioning.dvState = val; }); }; fileversioning.updateVersionInfo(); accountUI.inputs.switch.init( '#versioning-status', $('#versioning-status', accountUI.$contentBlock).parent(), !fileversioning.dvState, setVersioningAttr, function(val) { var promise = new MegaPromise(); if (val) { msgDialog('confirmation', l[882], l[17595], false, function(e) { if (e) { promise.resolve(); } else { promise.reject(); } }); } else { promise.resolve(); } return promise; }); }, bindEvents: function() { 'use strict'; $('#delete-all-versions', accountUI.$contentBlock).rebind('click', function() { if (!$(this).hasClass('disabled')) { if (M.isInvalidUserStatus()) { return; } msgDialog('remove', l[1003], l[17581], l[1007], function(e) { if (e) { loadingDialog.show(); var req = {a: 'dv'}; api_req(req, { callback: function(res) { if (res === 0) { M.accountData(function() { fileversioning.updateVersionInfo(); }, false, true); } } }); } }); } }); } }, rubsched: { render: function(account) { 'use strict'; if (d) { console.log('Render rubbish bin schedule'); } var $rubschedParent = $('#rubsched', accountUI.$contentBlock).parent(); var $rubschedGreenNoti = $('.rub-grn-noti', accountUI.$contentBlock); var $rubschedOptions = $('.rubsched-options', accountUI.$contentBlock); var initRubschedSwitch = function(defaultValue) { accountUI.inputs.switch.init( '#rubsched', $('#rubsched', accountUI.$contentBlock).parent(), defaultValue, function(val) { if (val) { $('#rubsched', accountUI.$contentBlock).closest('.slide-in-out').removeClass('closed'); $rubschedParent.addClass('border'); if (!fmconfig.rubsched) { var defValue = u_attr.p ? 90 : 30; var defOption = 14; mega.config.setn('rubsched', defOption + ":" + defValue); $('#rad' + defOption + '_opt', accountUI.$contentBlock).val(defValue); } } else { mega.config.setn('rubsched', 0); $('#rubsched', accountUI.$contentBlock).closest('.slide-in-out').addClass('closed'); $rubschedParent.removeClass('border'); } }); }; if (u_attr.flags.ssrs > 0) { // Server side scheduler - new $rubschedOptions.removeClass('hidden'); $('.rubschedopt', accountUI.$contentBlock).addClass('hidden'); $('.rubschedopt-none', accountUI.$contentBlock).addClass('hidden'); var value = account.ssrs ? account.ssrs : (u_attr.p ? 90 : 30); var rad14_optString = mega.icu.format(l.clear_rub_bin_days, value); var rad14_optArray = rad14_optString.split(/\[A]|\[\/A]/); $('#rad14_opt', accountUI.$contentBlock).val(rad14_optArray[1]); $('#rad14_opt_txt_1', accountUI.$contentBlock).text(rad14_optArray[0]); $('#rad14_opt_txt_2', accountUI.$contentBlock).text(rad14_optArray[2]); if (!value) { $rubschedOptions.addClass('hidden'); } // show/hide on/off switches if (u_attr.p) { $rubschedParent.removeClass('hidden'); $rubschedGreenNoti.addClass('hidden'); $('.rubbish-desc', accountUI.$contentBlock).text(l[18685]).removeClass('hidden'); $('.account.rubbish-cleaning .settings-right-block', accountUI.$contentBlock) .addClass('slide-in-out'); if (account.ssrs) { $rubschedParent.addClass('border').parent().removeClass('closed'); } else { $rubschedParent.removeClass('border').parent().addClass('closed'); } initRubschedSwitch(account.ssrs); } else { $rubschedParent.addClass('hidden'); $rubschedGreenNoti.removeClass('hidden'); $('.rubbish-desc', accountUI.$contentBlock).text(l[18686]).removeClass('hidden'); $('.account.rubbish-cleaning .settings-right-block', accountUI.$contentBlock) .removeClass('slide-in-out'); } } else { // Client side scheduler - old initRubschedSwitch(fmconfig.rubsched); if (u_attr.p) { $rubschedGreenNoti.addClass('hidden'); } else { $rubschedGreenNoti.removeClass('hidden'); } if (fmconfig.rubsched) { $rubschedParent.addClass('border').parent().removeClass('closed'); $rubschedOptions.removeClass('hidden'); $('.rubschedopt', accountUI.$contentBlock).removeClass('hidden'); var opt = String(fmconfig.rubsched).split(':'); $('#rad' + opt[0] + '_opt', accountUI.$contentBlock).val(opt[1]); accountUI.inputs.radio.init( '.rubschedopt', $('.rubschedopt', accountUI.$contentBlock).parent(), opt[0], function (val) { mega.config.setn('rubsched', val + ":" + $('#rad' + val + '_opt').val()); } ); } else { $rubschedParent.removeClass('border').parent().addClass('closed'); } } }, bindEvents: function() { 'use strict'; $('.rubsched_textopt', accountUI.$contentBlock).rebind('click.rs blur.rs keypress.rs', function(e) { // Do not save value until user leave input or click Enter button if (e.which && e.which !== 13) { return; } var curVal = parseInt($(this).val()) | 0; var maxVal; if (this.id === 'rad14_opt') { // For days option var minVal = 7; maxVal = u_attr.p ? Math.pow(2, 53) : 30; curVal = Math.min(Math.max(curVal, minVal), maxVal); var rad14_optString = mega.icu.format(l.clear_rub_bin_days, curVal); var rad14_optArray = rad14_optString.split(/\[A]|\[\/A]/); curVal = rad14_optArray[1]; $('#rad14_opt_txt_1', accountUI.$contentBlock).text(rad14_optArray[0]); $('#rad14_opt_txt_2', accountUI.$contentBlock).text(rad14_optArray[2]); } if (this.id === 'rad15_opt') { // For size option // Max value cannot be over current account's total storage space. maxVal = account.space / Math.pow(1024, 3); curVal = Math.min(curVal, maxVal); } $(this).val(curVal); var id = String(this.id).split('_')[0]; mega.config.setn('rubsched', id.substr(3) + ':' + curVal); }); } }, userInterface: { _initOption: function(name) { 'use strict'; var selector = '.' + name; accountUI.inputs.radio.init(selector, $(selector).parent(), fmconfig[name] | 0, function(val) { mega.config.setn(name, parseInt(val) | 0, l[16168]); } ); }, render: function() { 'use strict'; this._initOption('uisorting'); this._initOption('uiviewmode'); } }, hideRecents: { render: function() { 'use strict'; accountUI.inputs.switch.init( '#hide-recents', $('#hide-recents', accountUI.$contentBlock).parent(), !mega.config.get('showRecents'), (val) => mega.config.setn('showRecents', val ? undefined : 1)); } }, dragAndDrop: { render: function() { 'use strict'; accountUI.inputs.switch.init( '#ulddd', $('#ulddd', accountUI.$contentBlock).parent(), !mega.config.get('ulddd'), function(val) { mega.config.setn('ulddd', val ? undefined : 1); }); } }, delConfirm: { render: function() { 'use strict'; accountUI.inputs.switch.init( '#skipDelWarning', $('#skipDelWarning', accountUI.$contentBlock).parent(), !mega.config.get('skipDelWarning'), val => mega.config.setn('skipDelWarning', val ? undefined : 1) ); } }, passReminder: { render: function() { 'use strict'; accountUI.inputs.switch.init( '#prd', $('#prd', accountUI.$contentBlock).parent(), !mega.ui.passwordReminderDialog.passwordReminderAttribute.dontShowAgain, val => { mega.ui.passwordReminderDialog.passwordReminderAttribute.dontShowAgain = val ^ 1; showToast('settings', l[16168]); }); } }, chatDialogs: { render: function() { 'use strict'; const $switches = $('.dialog-options .chat-dialog', accountUI.$contentBlock); const rawVal = mega.config.get('xcod'); const _set = (id, val, subtype) => { if (val) { mega.config.setn('xcod', mega.config.get(id) & ~(1 << subtype)); } else { mega.config.setn('xcod', mega.config.get(id) | 1 << subtype); } }; for (let i = $switches.length; i--;) { const elm = $switches[i]; const [id, subtype] = elm.id.split('-'); const currVal = rawVal >> subtype & 1; accountUI.inputs.switch.init( '.mega-switch', $(elm).parent(), !currVal, val => _set(id, val, subtype) ); } } }, proExpiry: { render: async function() { 'use strict'; accountUI.inputs.switch.init( '#hideProExpired', $('#hideProExpired', accountUI.$contentBlock).parent(), (await Promise.resolve(mega.attr.get(u_handle, 'hideProExpired', false, true)).catch(() => []))[0] ^ 1, val => { mega.attr.set('hideProExpired', val ? '0' : '1', false, true); showToast('settings', l[16168]); }); } }, publicLinks: { render: function() { 'use strict'; var warnplinkId = '#nowarnpl'; accountUI.inputs.switch.init( warnplinkId, $(warnplinkId, accountUI.$contentBlock).parent(), !mega.config.get('nowarnpl'), val => mega.config.setn('nowarnpl', val ^ 1) ); } }, }; accountUI.transfers = { init: function(account) { 'use strict'; this.$page = $('.fm-account-sections.fm-account-transfers', accountUI.$contentBlock); // Upload and Download - Bandwidth this.uploadAndDownload.bandwidth.render(account); // Upload and Download - Upload this.uploadAndDownload.upload.render(); // Upload and Download - Download this.uploadAndDownload.download.render(); // Transfer Tools - Megasync this.transferTools.megasync.render(); // Download folder setting for PaleMoon ext this.addDownloadFolderSetting(); }, uploadAndDownload: { bandwidth: { render: function(account) { 'use strict'; // LITE/PRO account if (u_attr.p && !u_attr.b) { var bandwidthLimit = Math.round(account.servbw_limit | 0); var $slider = $('#bandwidth-slider').slider({ min: 0, max: 100, range: 'min', value: bandwidthLimit, change: function(e, ui) { if (M.currentdirid === 'account/transfers') { bandwidthLimit = ui.value; if (parseInt($.bandwidthLimit) !== bandwidthLimit) { var done = delay.bind(null, 'bandwidthLimit', function() { api_req({"a": "up", "srvratio": Math.round(bandwidthLimit)}); if ($.bandwidthLimit !== undefined) { showToast('settings', l[16168]); } $.bandwidthLimit = bandwidthLimit; }, 700); if (bandwidthLimit > 99) { msgDialog('warningb:!' + l[776], l[882], l[12689], 0, function(e) { if (e) { done(); } else { $('.slider-percentage') .safeHTML(l.transfer_quota_pct.replace('%1', formatPercentage(0))); $('.slider-percentage span').removeClass('bold warn'); $('#bandwidth-slider').slider('value', 0); } }); } else { done(); } } } }, slide: function(e, ui) { $('.slider-percentage', accountUI.$contentBlock) .safeHTML(l.transfer_quota_pct.replace('%1', formatPercentage(ui.value / 100))); if (ui.value > 90) { $('.slider-percentage span', accountUI.$contentBlock).addClass('warn bold'); } else { $('.slider-percentage span', accountUI.$contentBlock).removeClass('bold warn'); } } }); $('.ui-slider-handle', $slider).addClass('sprite-fm-mono icon-arrow-left ' + 'sprite-fm-mono-after icon-arrow-right-after'); $('.slider-percentage', accountUI.$contentBlock) .safeHTML(l.transfer_quota_pct.replace('%1', formatPercentage(bandwidthLimit / 100))); $('.bandwith-settings', accountUI.$contentBlock).removeClass('disabled').addClass('border'); $('.slider-percentage-bl', accountUI.$contentBlock).removeClass('hidden'); $('.band-grn-noti', accountUI.$contentBlock).addClass('hidden'); } // Business account else if (u_attr.b) { $('.bandwith-settings', accountUI.$contentBlock).addClass('hidden'); $('.slider-percentage-bl', accountUI.$contentBlock).addClass('hidden'); $('.band-grn-noti', accountUI.$contentBlock).addClass('hidden'); } } }, upload: { render: function() { 'use strict'; var $uploadSettings = $('.upload-settings', accountUI.$contentBlock); // Parallel upload slider var $slider = $('#slider-range-max', $uploadSettings).slider({ min: 1, max: 6, range: "min", value: fmconfig.ul_maxSlots || 4, change: function(e, ui) { if (M.currentdirid === 'account/transfers' && ui.value !== fmconfig.ul_maxSlots) { mega.config.setn('ul_maxSlots', ui.value); ulQueue.setSize(fmconfig.ul_maxSlots); } }, slide: function(e, ui) { $('.numbers.active', $uploadSettings).removeClass('active'); $('.numbers:nth-child(' + ui.value + ')', $uploadSettings) .addClass('active'); } }); $('.ui-slider-handle', $slider).addClass('sprite-fm-mono icon-arrow-left ' + 'sprite-fm-mono-after icon-arrow-right-after'); $('.numbers.active', $uploadSettings).removeClass('active'); $(' .numbers:nth-child(' + $slider.slider('value') + ')', $uploadSettings) .addClass('active'); }, }, download: { render: function() { 'use strict'; var $downloadSettings = $('.download-settings', accountUI.$contentBlock); // Parallel download slider var $slider = $('#slider-range-max2', $downloadSettings).slider({ min: 1, max: 6, range: "min", value: fmconfig.dl_maxSlots || 4, change: function(e, ui) { if (M.currentdirid === 'account/transfers' && ui.value !== fmconfig.dl_maxSlots) { mega.config.setn('dl_maxSlots', ui.value); dlQueue.setSize(fmconfig.dl_maxSlots); } }, slide: function(e, ui) { $('.numbers.active', $downloadSettings).removeClass('active'); $('.numbers:nth-child(' + ui.value + ')', $downloadSettings) .addClass('active'); } }); $('.ui-slider-handle', $slider).addClass('sprite-fm-mono icon-arrow-left ' + 'sprite-fm-mono-after icon-arrow-right-after'); $('.numbers.active', $downloadSettings).removeClass('active'); $('.numbers:nth-child(' + $slider.slider('value') + ')', $downloadSettings) .addClass('active'); } } }, transferTools: { megasync: { render : function() { 'use strict'; var $section = $('.transfer-tools', accountUI.transfers.$page); accountUI.inputs.switch.init( '#dlThroughMEGAsync', $('#dlThroughMEGAsync', accountUI.$contentBlock).parent(), fmconfig.dlThroughMEGAsync, function(val) { mega.config.setn('dlThroughMEGAsync', val); if (val) { megasync.periodicCheck(); } else { window.useMegaSync = 4; } }); megasync.isInstalled((err, is) => { if (!err && is) { $('.mega-banner', $section).addClass('hidden'); } else { $('.mega-banner', $section).removeClass('hidden'); } }); } } }, addDownloadFolderSetting: function() { 'use strict'; if (is_chrome_firefox && !$('#acc_dls_folder', accountUI.$contentBlock).length) { $('.fm-account-transfers').safeAppend( '<div class="account data-block">' + '<div class="settings-left-block">' + '<div id="acc_dls_folder">' + '<div class="fm-account-header">Downloads folder:</div></div></div>' + '<div class="settings-right-block"><div class="settings-sub-section">' + '<input type="button" value="Browse..." style="-moz-appearance:' + 'button;margin-right:12px;cursor:pointer" />' + '</div></div></div>'); var fld = mozGetDownloadsFolder(); $('#acc_dls_folder', accountUI.$contentBlock).safeAppend($('<span/>').text(fld && fld.path)); $('#acc_dls_folder input', accountUI.$contentBlock).click(function() { var fs = mozFilePicker(0, 2); if (fs) { mozSetDownloadsFolder(fs); $(this).next().text(fs.path); } }); } } }; accountUI.contactAndChat = { init: function(autoaway, autoawaylock, autoawaytimeout, persist, persistlock, lastSeen) { 'use strict'; if (window.megaChatIsDisabled) { console.error('Mega Chat is disabled, cannot proceed to Contact and Chat settings'); return; } var self = this; if (!megaChatIsReady) { // If chat is not ready waiting for chat_initialized broadcaster. loadingDialog.show(); var args = toArray.apply(null, arguments); mBroadcaster.once('chat_initialized', function() { self.init.apply(self, args); }); return true; } loadingDialog.hide(); var presenceInt = megaChat.plugins.presencedIntegration; if (!presenceInt || !presenceInt.userPresence) { setTimeout(function() { throw new Error('presenceInt is not ready...'); }); return true; } presenceInt.rebind('settingsUIUpdated.settings', function() { self.init.apply(self, toArray.apply(null, arguments).slice(1)); }); // Only call this if the call of this function is the first one, made by fm.js -> accountUI if (autoaway === undefined) { presenceInt.userPresence.updateui(); return true; } this.status.render(presenceInt, autoaway, autoawaylock, autoawaytimeout, persist, persistlock, lastSeen); this.status.bindEvents(presenceInt, autoawaytimeout); this.chatList.render(); this.richURL.render(); this.dnd.render(); }, status: { AWAY_REFS: { hours: 'autoaway-hours', minutes: 'autoaway-minutes' }, render: function(presenceInt, autoaway, autoawaylock, autoawaytimeout, persist, persistlock, lastSeen) { 'use strict'; // Chat var $sectionContainerChat = $('.fm-account-contact-chats', accountUI.$contentBlock); // Status appearance radio buttons accountUI.inputs.radio.init( '.chatstatus', $('.chatstatus').parent(), presenceInt.getPresence(u_handle), function(newVal) { presenceInt.setPresence(parseInt(newVal)); showToast('settings', l[16168]); }); // Last seen switch accountUI.inputs.switch.init( '#last-seen', $sectionContainerChat, lastSeen, function(val) { presenceInt.userPresence.ui_enableLastSeen(Boolean(val)); showToast('settings', l[16168]); }); if (autoawaytimeout !== false) { // Auto-away switch accountUI.inputs.switch.init( '#auto-away-switch', $sectionContainerChat, autoaway, function(val) { presenceInt.userPresence.ui_setautoaway(Boolean(val)); showToast('settings', l[16168]); }); // Prevent changes to autoaway if autoawaylock is set if (autoawaylock === true) { $('#auto-away-switch', $sectionContainerChat).addClass('disabled') .parent().addClass('hidden'); } else { $('#auto-away-switch', $sectionContainerChat).removeClass('disabled') .parent().removeClass('hidden'); } // Auto-away input box const [hours, minutes] = [Math.floor(autoawaytimeout / 3600), autoawaytimeout % 3600 / 60]; const strArray = l.set_autoaway.split('[X]'); $('#autoaway_txt_1', $sectionContainerChat).text(strArray[0]); // `Set status to Away after` $(`input#${this.AWAY_REFS.hours}`, $sectionContainerChat).val(hours || ''); $('#autoaway_txt_2', $sectionContainerChat).text(mega.icu.format(l.plural_hour, hours)); $(`input#${this.AWAY_REFS.minutes}`, $sectionContainerChat).val(minutes || ''); $('#autoaway_txt_3', $sectionContainerChat).text(mega.icu.format(l.plural_minute, minutes)); $('#autoaway_txt_4', $sectionContainerChat).text(strArray[1]); // `of inactivity.` // Always editable for user comfort - accountUI.controls.enableElement( $(`input#${this.AWAY_REFS.hours}, input#${this.AWAY_REFS.minutes}`, $sectionContainerChat) ); // Persist switch accountUI.inputs.switch.init( '#persist-presence-switch', $sectionContainerChat, persist, function(val) { presenceInt.userPresence.ui_setpersist(Boolean(val)); showToast('settings', l[16168]); }); // Prevent changes to autoaway if autoawaylock is set if (persistlock === true) { $('#persist-presence-switch', $sectionContainerChat).addClass('disabled') .parent().addClass('hidden'); } else { $('#persist-presence-switch', $sectionContainerChat).removeClass('disabled') .parent().removeClass('hidden'); } } }, bindEvents: function(presenceInt, autoawaytimeout) { 'use strict'; if (autoawaytimeout !== false) { const { AWAY_REFS } = this; $(`input#${Object.values(AWAY_REFS).join(', input#')}`).rebind('change.dashboard', () => presenceInt.userPresence.ui_setautoaway( true, Object.values(AWAY_REFS) .map(ref => { const el = document.getElementById(ref); let [value, max] = (({ value, max }) => [Math.max(0, value | 0), max | 0])(el); if (value > max) { el.value = value = max; } // hours || minutes -> seconds return el.id === AWAY_REFS.hours ? value * 3600 : value * 60; }) // hours + minutes -> seconds || default to 300 seconds as min .reduce((a, b) => a + b) || 300 ) ); } }, }, chatList: { render: function() { 'use strict'; const curr = mega.config.get('showHideChat'); accountUI.inputs.radioCard.init( '.card', $('.card', accountUI.$contentBlock).parent(), typeof curr === 'undefined' ? 0 : 1, (val) => { mega.config.setn('showHideChat', val); } ); } }, richURL: { render: function() { 'use strict'; if (typeof RichpreviewsFilter === 'undefined') { return; } // Auto-away switch const { previewGenerationConfirmation, confirmationDoConfirm, confirmationDoNever } = RichpreviewsFilter; accountUI.inputs.switch.init( '#richpreviews', $('#richpreviews').parent(), // previewGenerationConfirmation -> -1 (unset, default) || true || false previewGenerationConfirmation && previewGenerationConfirmation > 0, val => { if (val){ confirmationDoConfirm(); } else { confirmationDoNever(); } showToast('settings', l[16168]); } ); } }, dnd: { /** * Cached references for common DOM elements */ DOM: { container: '.fm-account-main', toggle: '#push-settings-toggle', dialog: '.push-settings-dialog', status: '.push-settings-status', }, /** * @see PushNotificationSettings.GROUPS */ group: 'CHAT', /** * hasDnd * @description Get the current push notification setting * @returns {Boolean} */ hasDnd: function() { 'use strict'; return ( pushNotificationSettings && pushNotificationSettings.getDnd(this.group) || pushNotificationSettings.getDnd(this.group) === 0 ); }, /** * getTimeString * @description Returns human readable and formatted string based on the * current push notification setting (timestamp) * @example `Notification will be silent until XX:XX` * @returns {String} */ getTimeString: function() { 'use strict'; var dnd = pushNotificationSettings.getDnd(this.group); if (dnd) { return ( // `Notifications will be silent until %s` l[23540].replace('%s', '<span>' + toLocaleTime(dnd) + '</span>') ); } return ' '; }, /** * renderStatus * @param hasDnd Boolean the push notification setting status * @returns {*} */ renderStatus: function(hasDnd) { 'use strict'; var $status = $(this.DOM.status, this.DOM.container); return hasDnd ? $status.safeHTML(this.getTimeString()).removeClass('hidden') : $status.addClass('hidden'); }, /** * setInitialState * @description Invoked immediately upon loading the module, sets the initial state -- conditionally * sets the toggle state, renders formatted timestamp */ setInitialState: function() { 'use strict'; if (this.hasDnd()) { var dnd = pushNotificationSettings.getDnd(this.group); if (dnd && dnd < unixtime()) { pushNotificationSettings.disableDnd(this.group); return; } $(this.DOM.toggle, this.DOM.container).addClass('toggle-on').trigger('update.accessibility'); this.renderStatus(true); } }, /** * setMorningOption * @description Handles the `Until tomorrow morning, 08:00` / `Until this morning, 08:00` option. */ setMorningOption: function() { 'use strict'; var container = '.radio-txt.morning-option'; var $label = $('span', container); var $radio = $('input', container); // 00:01 ~ 07:59 -> `Until this morning, 08:00` // 08:00 ~ 00:00 -> `Until tomorrow morning, 08:00` var targetTomorrow = (new Date().getHours()) >= 8; var date = new Date(); // Start of the day -> 08:00 date.setHours(0, 1, 0, 0); date.setHours(date.getHours() + 8); if (targetTomorrow) { // +1 day if we target `tomorrow morning` date.setDate(date.getDate() + 1); } var difference = Math.abs(date - new Date()); var minutesUntil = Math.floor(difference / 1000 / 60); $label.safeHTML( targetTomorrow ? l[23671] || 'Until tomorrow morning, 08:00' : l[23670] || 'Until this morning, 08:00' ); $radio.val(minutesUntil); }, /** * handleToggle * @description Handles the toggle switch -- conditionally adds or removes the toggle active state, * disables or sets the `Until I Turn It On Again` default setting * @param ev Object the event object */ handleToggle: function(ev) { 'use strict'; var hasDnd = this.hasDnd(); var group = this.group; if (hasDnd) { pushNotificationSettings.disableDnd(group); this.renderStatus(false); $(ev.currentTarget).removeClass('toggle-on').trigger('update.accessibility'); showToast('settings', l[16168]); } else { this.handleDialogOpen(); } }, /** * handleDialogOpen * @description * Handles the dialog toggle, incl. attaches additional handler that sets the given setting if any is selected */ handleDialogOpen: function() { 'use strict'; var self = this; var $dialog = $(this.DOM.dialog); var time = unixtime(); this.setMorningOption(); M.safeShowDialog('push-settings-dialog', $dialog); // Init radio button UI. accountUI.inputs.radio.init('.custom-radio', $dialog, ''); // Bind the `Done` specific event handling $('.push-settings-done', $dialog).rebind('click.dndUpdate', function() { var $radio = $('input[type="radio"]:checked', $dialog); var value = parseInt($radio.val(), 10); pushNotificationSettings.setDnd(self.group, value === 0 ? 0 : time + value * 60); $(self.DOM.toggle, self.DOM.container).addClass('toggle-on').trigger('update.accessibility'); closeDialog(); self.renderStatus(true); showToast('settings', l[16168]); }); }, /** * bindEvents * @description * Bind the initial event handlers, excl. the `Done` found within the dialog */ bindEvents: function() { 'use strict'; $(this.DOM.toggle, this.DOM.container).rebind('click.dndToggleSwitch', this.handleToggle.bind(this)); $('button.js-close, .push-settings-close', this.DOM.dialog).rebind('click.dndDialogClose', closeDialog); }, /** * render * @description * Initial render, invoked upon mounting the module */ render: function() { 'use strict'; this.setInitialState(); this.bindEvents(); } }, delayRender: function(presenceInt, autoaway) { 'use strict'; var self = this; if (!megaChatIsReady) { if (megaChatIsDisabled) { console.error('Mega Chat is disabled, cannot proceed to Contact and Chat settings'); } else { // If chat is not ready waiting for chat_initialized broadcaster. loadingDialog.show(); mBroadcaster.once('chat_initialized', self.delayRender.bind(self, presenceInt, autoaway)); } return true; } loadingDialog.hide(); if (!presenceInt || !presenceInt.userPresence) { setTimeout(function() { throw new Error('presenceInt is not ready...'); }); return true; // ^ FIXME too..! } // Only call this if the call of this function is the first one, made by fm.js -> accountUI if (autoaway === undefined) { presenceInt.rebind('settingsUIUpdated.settings', function( e, autoaway, autoawaylock, autoawaytimeout, persist, persistlock, lastSeen ) { self.init(autoaway, autoawaylock, autoawaytimeout, persist, persistlock, lastSeen); }); presenceInt.userPresence.updateui(); return true; } if (typeof (megaChat) !== 'undefined' && typeof(presenceInt) !== 'undefined') { presenceInt.rebind('settingsUIUpdated.settings', function( e, autoaway, autoawaylock, autoawaytimeout, persist, persistlock, lastSeen ) { self.init(autoaway, autoawaylock, autoawaytimeout, persist, persistlock, lastSeen); }); } }, }; accountUI.reseller = { init: function(account) { 'use strict'; if (M.account.reseller) { this.voucher.render(account); this.voucher.bindEvents(); } }, voucher: { render: function(account) { 'use strict'; var $resellerSection = $('.fm-account-reseller', accountUI.$contentBlock); var $vouchersSelect = $('.dropdown-input.vouchers', $resellerSection); if (!$.voucherlimit) { $.voucherlimit = 10; } var email = 'resellers@mega.nz'; $('.resellerbuy').attr('href', 'mailto:' + email) .find('span').text(l[9106].replace('%1', email)); // Use 'All' or 'Last 10/100/250' for the dropdown text const buttonText = $.voucherlimit === 'all' ? l[7557] : mega.icu.format(l[466], $.voucherlimit); $('span', $vouchersSelect).text(buttonText); $('.balance span', $resellerSection).safeHTML('@@ € ', account.balance[0][0]); $('.voucher10-', $vouchersSelect).text(mega.icu.format(l[466], 10)); $('.voucher100-', $vouchersSelect).text(mega.icu.format(l[466], 100)); $('.voucher250-', $vouchersSelect).text(mega.icu.format(l[466], 250)); // Sort vouchers by most recently created at the top M.account.vouchers.sort(function(a, b) { if (a['date'] < b['date']) { return 1; } else { return -1; } }); $('.data-table.vouchers tr', $resellerSection).remove(); var html = '<tr><th>' + l[475] + '</th><th>' + l[7714] + '</th><th>' + l[477] + '</th><th>' + l[488] + '</th></tr>'; $(account.vouchers).each(function(i, el) { // Only show the last 10, 100, 250 or if the limit is not set show all vouchers if (($.voucherlimit !== 'all') && (i >= $.voucherlimit)) { return false; } var status = l[489]; if (el.redeemed > 0 && el.cancelled === 0 && el.revoked === 0) { status = l[490] + ' ' + time2date(el.redeemed); } else if (el.revoked > 0 && el.cancelled > 0) { status = l[491] + ' ' + time2date(el.revoked); } else if (el.cancelled > 0) { status = l[492] + ' ' + time2date(el.cancelled); } var voucherLink = 'https://mega.nz/#voucher' + htmlentities(el.code); html += '<tr><td><span>' + time2date(el.date) + '</span></td>' + '<td class="selectable"><span class="break-word">' + voucherLink + '</span></td>' + '<td><span>€ ' + htmlentities(el.amount) + '</span></td>' + '<td><span>' + status + '</span></td></tr>'; }); $('.data-table.vouchers', $resellerSection).safeHTML(html); $('.vouchertype .dropdown-scroll', $resellerSection).text(''); $('.vouchertype > span', $resellerSection).text(l[6875]); var prices = []; for (var i = 0; i < M.account.prices.length; i++) { if (M.account.prices[i]) { prices.push(M.account.prices[i][0]); } } prices.sort(function(a, b) { return (a - b); }); var voucheroptions = ''; for (var j = 0; j < prices.length; j++) { voucheroptions += '<div class="option" data-value="' + htmlentities(prices[j]) + '">€' + htmlentities(prices[j]) + ' voucher</div>'; } $('.vouchertype .dropdown-scroll', $resellerSection) .safeHTML(voucheroptions); }, bindEvents: function() { 'use strict'; var $resellerSection = $('.fm-account-reseller', accountUI.$contentBlock); var $voucherSelect = $('.vouchers.dropdown-input', $resellerSection); var $voucherTypeSelect = $('.vouchertype.dropdown-input', $resellerSection); // Bind Dropdowns events bindDropdownEvents($voucherSelect); bindDropdownEvents($voucherTypeSelect); $('.vouchercreate', $resellerSection).rebind('click.voucherCreateClick', function() { var vouchertype = $('.option[data-state="active"]', $voucherTypeSelect) .attr('data-value'); var voucheramount = parseInt($('#account-voucheramount', $resellerSection).val()); var proceed = false; for (var i in M.account.prices) { if (M.account.prices[i][0] === vouchertype) { proceed = true; } } if (!proceed) { msgDialog('warninga', l[135], 'Please select the voucher type.'); return false; } if (!voucheramount) { msgDialog('warninga', l[135], 'Please enter a valid voucher amount.'); return false; } if (vouchertype === '19.99') { vouchertype = '19.991'; } loadingDialog.show(); api_req({a: 'uavi', d: vouchertype, n: voucheramount, c: 'EUR'}, { callback: function() { M.account.lastupdate = 0; accountUI(); } }); }); $('.option', $voucherSelect).rebind('click.accountSection', function() { var $this = $(this); var c = $this.attr('class') ? $this.attr('class') : ''; if (c.indexOf('voucher10-') > -1) { $.voucherlimit = 10; } else if (c.indexOf('voucher100-') > -1) { $.voucherlimit = 100; } else if (c.indexOf('voucher250-') > -1) { $.voucherlimit = 250; } else if (c.indexOf('voucherAll-') > -1) { $.voucherlimit = 'all'; } accountUI(); }); } } }; accountUI.calls = { init: function() { 'use strict'; this.emptyGroupCall.render(); this.callNotifications.render(); }, emptyGroupCall: { render: function() { 'use strict'; const switchSelector = '#callemptytout'; // undefined === 2min wait, 0 === 2min wait, 1 === 24hour wait const curr = mega.config.get('callemptytout'); accountUI.inputs.switch.init( switchSelector, $(switchSelector).parent(), typeof curr === 'undefined' ? 1 : Math.abs(curr - 1), val => { mega.config.setn('callemptytout', Math.abs(val - 1)); eventlog(99758, JSON.stringify([Math.abs(val - 1)])); } ); } }, callNotifications: { render: function() { 'use strict'; const switchSelector = '#callinout'; const curr = mega.config.get('callinout'); accountUI.inputs.switch.init( switchSelector, $(switchSelector).parent(), typeof curr === 'undefined' ? 1 : Math.abs(curr - 1), val => { mega.config.setn('callinout', Math.abs(val - 1)); eventlog(99759, JSON.stringify([Math.abs(val - 1)])); } ); } } }; accountUI.vpn = { init: function() { 'use strict'; this.vpnPage = this.vpnPage || new VpnPage(); this.vpnPage.show(); } }; /** * Functionality for the My Account page, Security section to change the user's password */ var accountChangePassword = { /** * Initialise the change password functionality */ init:function() { 'use strict'; this.resetForm(); this.initPasswordKeyupHandler(); this.initChangePasswordButton(); }, /** * Reset the text inputs if coming back to the page */ resetForm: function() { 'use strict'; $('#account-new-password').val('').trigger('blur').trigger('input'); $('#account-confirm-password').val('').trigger('blur'); $('.account-pass-lines').removeClass('good1 good2 good3 good4 good5'); }, /** * Initialise the handler to change the password strength indicator while typing the password */ initPasswordKeyupHandler: function() { 'use strict'; var $newPasswordField = $('#account-new-password'); var $changePasswordButton = $('.account.change-password .save-container'); var bindStrengthChecker = function() { $newPasswordField.rebind('keyup.pwdchg input.pwdchg change.pwdchg', function() { // Estimate the password strength var password = $.trim($(this).val()); var passwordLength = password.length; if (passwordLength === 0) { $changePasswordButton.addClass('closed'); } else { $changePasswordButton.removeClass('closed'); } }); if ($newPasswordField.val().length) { // Reset strength after re-rendering. $newPasswordField.trigger('keyup.pwdchg'); } }; bindStrengthChecker(); }, /** * Initalise the change password button to verify the password (and 2FA if active) then change the password */ initChangePasswordButton: function() { 'use strict'; // Cache selectors var $newPasswordField = $('#account-new-password'); var $newPasswordConfirmField = $('#account-confirm-password'); var $changePasswordButton = $('.account .change-password-button'); var $passwordStrengthBar = $('.account-pass-lines'); $changePasswordButton.rebind('click', function() { if ($(this).hasClass('disabled')) { return false; } var password = $newPasswordField.val(); var confirmPassword = $newPasswordConfirmField.val(); // Check if the entered passwords are valid or strong enough var passwordValidationResult = security.isValidPassword(password, confirmPassword); // If bad result if (passwordValidationResult !== true) { // Show error message msgDialog('warninga', l[135], passwordValidationResult, false, function() { $newPasswordField.val(''); $newPasswordConfirmField.val(''); $newPasswordField.trigger('input').trigger('focus'); $newPasswordConfirmField.trigger('blur'); }); // Return early to prevent password change return false; } // Trigger save password on browser with correct email accountChangePassword.emulateFormSubmission(password); // Proceed to change the password accountChangePassword.changePassword(password); }); }, /** * Emulate form submission to trigger correctly behaved password manager update. * @param {String} password The new password */ emulateFormSubmission: function(password) { 'use strict'; var form = document.createElement("form"); form.className = 'hidden'; var elem1 = document.createElement("input"); var elem2 = document.createElement("input"); var elemBtn = document.createElement("input"); elem1.value = u_attr.email; elem1.type = 'email'; form.appendChild(elem1); elem2.value = password; elem2.type = 'password'; form.appendChild(elem2); elemBtn.type = 'submit'; form.appendChild(elemBtn); document.body.appendChild(form); $(form).on('submit', function() { return false; }); elemBtn.click(); document.body.removeChild(form); }, /** * Change the user's password * @param {String} newPassword The new password */ changePassword: function(newPassword) { 'use strict'; loadingDialog.show(); // Check their current Account Authentication Version before proceeding security.changePassword.checkAccountVersion((accountAuthVersion) => { // Check if 2FA is enabled on their account twofactor.isEnabledForAccount((result) => { loadingDialog.hide(); // If 2FA is enabled on their account if (result) { // Show the verify 2FA dialog to collect the user's PIN twofactor.verifyActionDialog.init((twoFactorPin) => { accountChangePassword.continueChangePassword(accountAuthVersion, newPassword, twoFactorPin); }); } else { // Otherwise change the password without 2FA accountChangePassword.continueChangePassword(accountAuthVersion, newPassword, null); } }); }); }, /** * Continue to change the user's password (e.g. immediately or after they've entered their 2FA PIN * @param {Number} accountAuthVersion The current Account Authentication Version i.e. 1 or 2 * @param {String} newPassword The new password * @param {String|null} twoFactorPin The 2FA PIN code or null if not applicable * @returns {void} */ continueChangePassword: function(accountAuthVersion, newPassword, twoFactorPin) { 'use strict'; const checkPassPromise = security.changePassword.isPasswordTheSame($.trim(newPassword), accountAuthVersion); checkPassPromise.fail((status) => { if (status === 1) { msgDialog('info', '', l[22126]); // You have entered your current password, please enter a new password } else { msgDialog('info', '', l[200]); // Oops, something went wrong } this.resetForm(); }); checkPassPromise.done(() => { if (accountAuthVersion === 1) { security.changePassword.oldMethod( newPassword, twoFactorPin, accountChangePassword.completeChangePassword ); } if (accountAuthVersion === 2) { security.changePassword.newMethod( newPassword, twoFactorPin, accountChangePassword.completeChangePassword ); } }); }, /** * Update the UI after attempting to change the password and let the user know if it failed or was successful * @param {Number} result The result from the change password API request */ completeChangePassword: function(result) { 'use strict'; loadingDialog.hide(); // If something went wrong with the 2FA PIN if (result === EFAILED || result === EEXPIRED) { msgDialog('warninga', l[135], l[19216]); } // If something else went wrong, show an error else if (typeof result === 'number' && result < 0) { msgDialog('warninga', l[135], l[47]); } else { // Success showToast('settings', l[725]); } // Clear password fields $('#account-new-password').val(''); $('#account-confirm-password').val(''); // Update the account page accountUI(); } }; /** * Functionality for the My Account page, Security section to change the user's email */ var accountChangeEmail = { /** * Initialise the change email functionality */ init:function() { 'use strict'; this.resetForm(); this.initEmailKeyupHandler(); this.initChangeEmailButton(); }, /** * Reset the text inputs if coming back to the page */ resetForm: function() { 'use strict'; // Reset change email fields after change $('#current-email').val(u_attr.email).blur(); $('#account-email').val(''); $('.fm-account-change-email').addClass('hidden'); }, /** * Initialise the handler to show an information message while typing the email */ initEmailKeyupHandler: function() { 'use strict'; // Cache selectors var $newEmail = $('#account-email'); var $emailInfoMessage = $('.fm-account-change-email'); var $changeEmailButton = $('.account .change-email-button'); // On text entry in the new email text field $newEmail.rebind('keyup', function() { const newEmailValue = $.trim($newEmail.val()).toLowerCase(); if (newEmailValue && u_attr.email.toLowerCase() !== newEmailValue) { // Show information message $emailInfoMessage.slideDown(); $changeEmailButton.closest('.save-container').removeClass('closed'); } else { // Show information message $emailInfoMessage.slideUp(); $changeEmailButton.closest('.save-container').addClass('closed'); if (u_attr.email.toLowerCase() === newEmailValue) { $newEmail.megaInputsShowError(l.m_change_email_same); } } $.tresizer(); }); }, /** * Initalise the change email button to verify the email, get the 2FA code (if active) then change the email */ initChangeEmailButton: function() { 'use strict'; // Cache selectors var $changeEmailButton = $('.account .change-email-button'); var $newEmail = $('#account-email'); // On Change Email button click $changeEmailButton.rebind('click', function() { if ($(this).hasClass('disabled')) { return false; } // Get the new email address var newEmailRaw = $newEmail.val(); var newEmail = $.trim(newEmailRaw).toLowerCase(); // If not a valid email, show an error if (!isValidEmail(newEmail)) { $newEmail.megaInputsShowError(l[1513]); return false; } // If there is text in the email field and it doesn't match the existing one if (newEmail !== '' && u_attr.email.toLowerCase() !== newEmail) { loadingDialog.show(); // Check if 2FA is enabled on their account twofactor.isEnabledForAccount(function(result) { loadingDialog.hide(); // If 2FA is enabled if (result) { // Show the verify 2FA dialog to collect the user's PIN twofactor.verifyActionDialog.init(function(twoFactorPin) { accountChangeEmail.continueChangeEmail(newEmail, twoFactorPin); }); } else { accountChangeEmail.continueChangeEmail(newEmail, null); } }); } }); }, /** * Initiate the change email request to the API * @param {String} newEmail The new email * @param {String|null} twoFactorPin The 2FA PIN code or null if not applicable */ continueChangeEmail: function(newEmail, twoFactorPin) { 'use strict'; loadingDialog.show(); // Prepare the request var requestParams = { a: 'se', // Set Email aa: 'a', e: newEmail, // The new email address i: requesti // Last request ID }; // If the 2FA PIN was entered, send it with the request if (twoFactorPin !== null) { requestParams.mfa = twoFactorPin; } // Change of email request api_req(requestParams, { callback: function(result) { loadingDialog.hide(); // If something went wrong with the 2FA PIN if (result === EFAILED || result === EEXPIRED) { msgDialog('warninga', l[135], l[19216]); } // If they have already requested a confirmation link for that email address, show an error else if (result === -12) { return msgDialog('warninga', l[135], l[7717]); } // EACCESS, the email address is already in use or current user is invalid. (less likely). else if (typeof result === 'number' && result === -11) { return msgDialog('warninga', l[135], l[19562]); } // If something else went wrong, show an error else if (typeof result === 'number' && result < 0) { msgDialog('warninga', l[135], l[47]); } else { // Success fm_showoverlay(); $('.awaiting-confirmation').removeClass('hidden'); localStorage.new_email = newEmail; } } }); } }; (function _dialogs(global) { 'use strict'; /* jshint -W074 */ // @private pointer to global fm-picker-dialog var $dialog = false; // @private reference to active dialog section var section = 'cloud-drive'; // @private shared nodes metadata var shares = Object.create(null); if (d) { window.mcshares = shares; } // ------------------------------------------------------------------------ // ---- Private Functions ------------------------------------------------- /** * Find shared folders marked read-only and disable it in dialog. * @private */ var disableReadOnlySharedFolders = function() { var $ro = $('.fm-picker-dialog-tree-panel.shared-with-me .dialog-content-block span[id^="mctreea_"]'); var targets = $.selected || []; if (!$ro.length) { if ($('.fm-picker-dialog-button.shared-with-me', $dialog).hasClass('active')) { // disable import btn $('.dialog-picker-button', $dialog).addClass('disabled'); } } $ro.each(function(i, v) { var h = $(v).attr('id').replace('mctreea_', ''); var s = shares[h] = Object.create(null); var n = M.d[h]; while (n && !n.su) { n = M.d[n.p]; } if (n) { s.share = n; s.owner = n.su; s.level = n.r; for (i = targets.length; i--;) { if (M.isCircular(n.h, targets[i])) { s.circular = true; break; } } } if (!n || !n.r) { $(v).addClass('disabled'); } }); }; /** * Disable circular references and read-only shared folders. * @private */ var disableFolders = function() { $('*[id^="mctreea_"]').removeClass('disabled'); if ($.moveDialog) { M.disableCircularTargets('#mctreea_'); } else if ($.selectFolderDialog && $.fileRequestNew) { const getIdAndDisableDescendants = (elem) => { const handle = String($(elem).attr('id')).replace('mctreea_', ''); if (handle) { M.disableDescendantFolders(handle, '#mctreea_'); } }; const $allFolders = $('*[id^="mctreea_"]', $dialog); const $sharedAndFileRequestFolders = $('*[id^="mctreea_"]', $dialog) .children('.file-request-folder, .shared-folder'); // All parent file request and shared folder $sharedAndFileRequestFolders.closest('.nw-fm-tree-item') .addClass('disabled'); // Filter shared folder and disable descendants const filteredSharedFolders = $sharedAndFileRequestFolders.filter('.shared-folder') .closest('.nw-fm-tree-item'); if (filteredSharedFolders.length) { for (let i = 0; i < filteredSharedFolders.length; i++) { getIdAndDisableDescendants(filteredSharedFolders[i]); } } // Check all linked folders and disable descendants const filteredLinkedFolders = $allFolders .filter('.linked') .addClass('disabled'); if (filteredLinkedFolders.length) { for (let i = 0; i < filteredLinkedFolders.length; i++) { getIdAndDisableDescendants(filteredLinkedFolders[i]); } } } else if (!$.copyToUpload) { var sel = $.selected || []; for (var i = sel.length; i--;) { $('#mctreea_' + String(sel[i]).replace(/[^\w-]/g, '')).addClass('disabled'); } } disableReadOnlySharedFolders(); }; /** * Retrieve array of non-circular nodes * @param {Array} [selectedNodes] * @returns {Array} * @private */ var getNonCircularNodes = function(selectedNodes) { var r = []; if ($.mcselected) { selectedNodes = selectedNodes || $.selected || []; for (var i = selectedNodes.length; i--;) { if ($.moveDialog && M.isCircular(selectedNodes[i], $.mcselected)) { continue; // Ignore circular targets if move dialog is active } if (selectedNodes[i] === $.mcselected) { continue; // If the source node is equal to target node } r.push(selectedNodes[i]); } } return r; }; /** * Retrieves a list of currently selected target chats * @private */ var getSelectedChats = function() { var chats = $('.nw-contact-item.selected', $dialog).attrs('id'); chats = chats.map(function(c) { return String(c).replace('cpy-dlg-chat-itm-spn-', ''); }); return chats; }; /** * Set the dialog button state to either disabled or enabled * @param {Object} $btn The jQuery's node or selector * @private */ var setDialogButtonState = function($btn) { $btn = $($btn); if (section === 'conversations') { if (getSelectedChats().length) { $btn.removeClass('disabled'); } else { $btn.addClass('disabled'); } } else if (!$.mcselected) { $btn.addClass('disabled'); } else if ($.copyDialog && section === 'cloud-drive') { $btn.removeClass('disabled'); } else if ($.selectFolderDialog && section === 'cloud-drive' && $.mcselected !== M.RootID) { $btn.removeClass('disabled'); } else { var forceEnabled = $.copyToShare || $.copyToUpload || $.onImportCopyNodes || $.saveToDialog || $.nodeSaveAs; console.assert(!$.copyToShare || Object($.selected).length === 0, 'check this...'); if (!forceEnabled && !getNonCircularNodes().length) { $btn.addClass('disabled'); } else { $btn.removeClass('disabled'); } } }; /** * Select tree item node * @param {String} h The node handle */ var selectTreeItem = function(h) { onIdle(function() { if (section === 'conversations') { $('#cpy-dlg-chat-itm-spn-' + h, $dialog).trigger('click'); } else if (!$('#mctreesub_' + h, $dialog).hasClass('opened')) { $('#mctreea_' + h, $dialog).trigger('click'); } }); }; /** * Render target breadcrumb * @param {String} [aTarget] Target node handle * @private */ var setDialogBreadcrumb = function(aTarget) { let path = false; let names = Object.create(null); const titles = Object.create(null); const dialog = $dialog[0]; var autoSelect = $.copyToUpload && !$.copyToUpload[2]; if (section === 'conversations') { const chats = getSelectedChats(); if (chats.length > 1) { path = [u_handle, 'contacts']; names[u_handle] = l[23755]; } else { aTarget = chats[0] || String(aTarget || '').split('/').pop(); } if (aTarget && String(aTarget).indexOf("#") > -1) { aTarget = aTarget.split("#")[0]; } } // Update global $.mcselected with the target handle $.mcselected = aTarget && aTarget !== 'transfers' ? aTarget : undefined; path = path || M.getPath($.mcselected); titles[M.RootID] = l[164]; titles[M.RubbishID] = l[167]; titles.shares = l[5542]; titles.contacts = l[17765]; if (path.length === 1) { names = titles; } if ($.mcselected && !path.length) { // The selected node is likely not in memory, try to rely on DOM and find the ancestors var el = $dialog[0].querySelector('#mctreea_' + aTarget); if (el) { path.push(aTarget); names[aTarget] = el.querySelector('.nw-fm-tree-folder').textContent; $(el).parentsUntil('.dialog-content-block', 'ul').each(function(i, elm) { var h = String(elm.id).split('_')[1]; path.push(h); elm = dialog.querySelector('#mctreea_' + h + ' .nw-fm-tree-folder'); if (elm) { names[h] = elm.textContent; } }); } } const scope = dialog.querySelector('.fm-picker-breadcrumbs'); const dictionary = function _(handle) { // If this is gallery view, make it default to root path instead if (M.isGalleryPage(handle) || M.isAlbumsPage(0, handle)) { return _(M.RootID); } let name = names[handle] || M.getNameByHandle(handle) || ''; let id = handle; let typeClass = 'folder'; const typeClasses = { [M.RootID]: 'cloud-drive', [M.RubbishID]: 'rubbish-bin', 'contacts': 'contacts', 'shares': 'shared-with-me' }; if (handle === M.RootID) { if (!folderlink) { typeClass = typeClasses[handle]; } } else if (typeClasses[handle]) { typeClass = typeClasses[handle]; } else if (handle.length === 11) { typeClass = 'contact selectable-txt'; } if (name === 'undefined') { name = ''; } if (titles[handle]) { name = titles[handle]; } if (section === 'conversations') { name = name && megaChat.plugins.emoticonsFilter.processHtmlMessage(escapeHTML(name)) || name; } else if (typeClass === 'contact') { name = ''; } if (autoSelect) { selectTreeItem(handle); } return { name, id, typeClass }; }; M.renderBreadcrumbs(path, scope, dictionary, id => { var $elm = $('#mctreea_' + id, $dialog); if ($elm.length) { $elm.trigger('click'); } else { $('.fm-picker-dialog-button.active', $dialog).trigger('click'); } $('.breadcrumb-dropdown.active', $dialog).removeClass('active'); return false; }); const placeholder = dialog.querySelector('.summary-input.placeholder'); if (path.length) { placeholder.classList.add('correct-input'); placeholder.classList.remove('high-light'); } else { placeholder.classList.add('high-light'); placeholder.classList.remove('correct-input'); const filetypeIcon = placeholder.querySelector('.target-icon'); filetypeIcon.classList.remove('icon-chat-filled', 'icon-folder-filled', 'sprite-fm-uni', 'sprite-fm-mono'); filetypeIcon.classList.add( 'sprite-fm-mono', section === 'conversations' ? 'icon-chat-filled' : 'icon-folder-filled' ); } if ($.copyToUpload) { // auto-select entries once $.copyToUpload[2] = true; } }; /** * Set selected items... * @param {*} [single] Single item mode * @private */ var setSelectedItems = function(single) { var $icon = $('.summary-items-drop-icon', $dialog) .removeClass('icon-arrow-up drop-up icon-arrow-down drop-down hidden'); var $div = $('.summary-items', $dialog).removeClass('unfold multi').empty(); var names = Object.create(null); var items = $.selected || []; $('.summary-title.summary-selected-title', $dialog).text(l[19180]); if ($.saveToDialogNode) { items = [$.saveToDialogNode.h]; names[$.saveToDialogNode.h] = $.saveToDialogNode.name; } if ($.copyToShare) { items = []; single = true; $('.summary-target-title', $dialog).text(l[19180]); $('.summary-selected', $dialog).addClass('hidden'); } else if ($.selectFolderDialog) { $('.summary-target-title', $dialog).text(l[19181]); $('.summary-selected', $dialog).addClass('hidden'); } else { $('.summary-target-title', $dialog).text(l[19181]); if ($.onImportCopyNodes) { $('.summary-selected', $dialog).addClass('hidden'); } else { $('.summary-selected', $dialog).removeClass('hidden'); } if ($.copyToUpload) { items = $.copyToUpload[0]; for (var j = items.length; j--;) { items[j].uuid = makeUUID(); } } } if (!single) { $div.addClass('unfold'); $div.safeAppend('<div class="item-row-group"></div>'); $div = $div.find('.item-row-group'); } if ($.nodeSaveAs) { items = [$.nodeSaveAs.h]; names[$.nodeSaveAs.h] = $.nodeSaveAs.name || ''; $('.summary-title.summary-selected-title', $dialog).text(l[1764]); var rowHtml = '<div class="item-row">' + '<div class="transfer-filetype-icon file text"></div>' + '<input id="f-name-input" class="summary-ff-name" type="text" value="' + escapeHTML($.nodeSaveAs.name) + '" placeholder="' + l[17506] + '" autocomplete="off"/> ' + '</div>' + '<div class="whitespaces-input-warning"> <div class="arrow"></div> <span></span></div>' + '<div class="duplicated-input-warning"> <div class="arrow"></div> <span>' + l[17578] + '</span> </div>'; $div.safeHTML(rowHtml); const ltWSpaceWarning = new InputFloatWarning($dialog); ltWSpaceWarning.check({type: 0, name: names[$.nodeSaveAs.h], ms: 0}); $('#f-name-input', $div).rebind('keydown.saveas', function(e) { if (e.which === 13 || e.keyCode === 13) { $('.dialog-picker-button', $dialog).trigger('click'); } }); $('#f-name-input', $div).rebind('keyup.saveas', () => { ltWSpaceWarning.check({type: 0}); }); if ($.saveAsDialog) { $('#f-name-input', $dialog).focus(); } } else { for (var i = 0; i < items.length; i++) { var h = items[i]; var n = M.getNodeByHandle(h) || Object(h); var name = names[h] || M.getNameByHandle(h) || n.name; var tail = '<i class="delete-item sprite-fm-mono icon-close-component "></i>'; var summary = '<div class="summary-ff-name-ellipsis">@@</div>'; var icon = fileIcon(n); var data = n.uuid || h; if (single) { tail = '<span>(@@)</span>'; if (items.length < 2) { tail = ''; summary = '<div class="summary-ff-name">@@</div>'; } } const pluralText = mega.icu.format(l.items_other_count, items.length - 1); $div.safeAppend( '<div class="item-row" data-node="@@">' + ' <div class="transfer-filetype-icon file @@"></div>' + summary + ' ' + tail + '</div>', data, icon, name, pluralText ); if (single) { break; } } } $icon.rebind('click', function() { $div.off('click.unfold'); setSelectedItems(!single); return false; }); if (single) { if (items.length > 1) { $div.addClass('multi').rebind('click.unfold', function() { $icon.trigger('click'); return false; }); $icon.addClass('icon-arrow-down drop-down'); } else { $icon.addClass('hidden'); } } else { initPerfectScrollbar($div); $icon.addClass('icon-arrow-up drop-up'); $('.delete-item', $div).rebind('click', function() { var $row = $(this).parent(); var data = $row.attr('data-node'); $row.remove(); initPerfectScrollbar($div); if ($.copyToUpload) { for (var i = items.length; i--;) { if (items[i].uuid === data) { items.splice(i, 1); break; } } $('header h2', $dialog).text(getDialogTitle()); } else { array.remove(items, data); } if (items.length < 2) { setSelectedItems(true); } return false; }); } }; /** * Get the button label for the dialog's main action button * @returns {String} * @private */ var getActionButtonLabel = function() { if ($.mcImport) { return l[236]; // Import } if ($.chatAttachmentShare && section !== 'conversations') { return l[776]; // Save } if ($.copyToShare) { return l[1344]; // Share } if ($.copyToUpload) { return l[372]; // Upload } if (section === 'conversations') { return l[1940]; // Send } if ($.saveToDialog || $.saveAsDialog) { if ($.nodeSaveAs && !$.nodeSaveAs.h) { return l[158]; } return l[776]; // Save } if ($.moveDialog) { return l[62]; // Move } if ($.selectFolderDialog) { return l[1523]; // Select } return l[16176]; // Paste }; /** * Get the dialog title based on operation * @returns {String} * @private */ var getDialogTitle = function() { if ($.mcImport) { return l[236]; // Import } if ($.chatAttachmentShare && section !== 'conversations') { return l[776]; // Save } if ($.copyToShare) { return l[1344]; // Share } if ($.copyToUpload) { var len = $.copyToUpload[0].length; if (section === 'conversations') { return mega.icu.format(l.upload_to_conversation, len); } if (section === 'shared-with-me') { return mega.icu.format(l.upload_to_share, len); } return mega.icu.format(l.upload_to_cd, len); } if ($.saveToDialog) { return l[776]; // Save } if ($.saveAsDialog) { if ($.nodeSaveAs && !$.nodeSaveAs.h) { return l[22680]; } return l[22678]; } if (section === 'conversations') { return l[17764]; // Send to chat } if ($.moveDialog) { return l[62]; // Move } return l[63]; // Copy }; /** * Getting contacts and view them in copy dialog */ var handleConversationTabContent = function _handleConversationTabContent() { var myChats = megaChat.chats; var myContacts = M.getContactsEMails(true); // true to exclude requests (incoming and outgoing) var $conversationTab = $('.fm-picker-dialog-tree-panel.conversations'); var $conversationNoConvTab = $('.dialog-empty-block.conversations'); var $conversationTabHeader = $('.fm-picker-dialog-panel-header', $conversationTab); var $contactsContentBlock = $('.dialog-content-block', $conversationTab); var contactGeneratedList = ""; var ulListId = 'cpy-dlg-chat-' + u_handle; var addedContactsByRecent = []; var nbOfRecent = 0; var isActiveMember = false; var createContactEntry = function _createContactEntry(name, email, handle) { if (name && handle && email) { var contactElem = '<span id="cpy-dlg-chat-itm-spn-' + handle + '" class="nw-contact-item single-contact '; var contactStatus = 'offline'; if (M.d[handle] && M.d[handle].presence) { contactStatus = M.onlineStatusClass(M.d[handle].presence)[1]; } contactElem += contactStatus + '">'; contactElem += '<i class="encrypted-icon sprite-fm-uni icon-ekr"></i>'; contactElem += '<span class="nw-contact-status"></span>'; contactElem += '<span class="nw-contact-name selectable-txt">' + megaChat.plugins.emoticonsFilter.processHtmlMessage(escapeHTML(name)) + '</span>'; contactElem += '<span class="nw-contact-email">' + escapeHTML(email) + '</span>'; contactElem = '<li id="cpy-dlg-chat-itm-' + handle + '">' + contactElem + '</li>'; return contactElem; } return ''; }; var createGroupEntry = function _createGroupEntry(names, nb, handle, chatRoom) { if (names && names.length && nb && handle) { var groupElem = '<span id="cpy-dlg-chat-itm-spn-' + handle + '" class="nw-contact-item multi-contact">'; if (chatRoom && (chatRoom.type === "group" || chatRoom.type === "private")) { groupElem += '<i class="encrypted-icon sprite-fm-uni icon-ekr"></i>'; } else { groupElem += '<span class="encrypted-spacer"></span>'; } groupElem += '<i class="group-chat-icon sprite-fm-mono icon-contacts"></i>'; var namesCombine = names[0]; var k = 1; while (namesCombine.length <= 40 && k < names.length) { namesCombine += ', ' + names[k]; k++; } if (k !== names.length) { namesCombine = namesCombine.substr(0, 37); namesCombine += '...'; } groupElem += '<span class="nw-contact-name group selectable-txt">' + megaChat.plugins.emoticonsFilter.processHtmlMessage(escapeHTML(namesCombine)) + '</span>'; groupElem += '<span class="nw-contact-group">' + mega.icu.format(l[24157], nb) + '</span>'; groupElem = '<li id="cpy-dlg-chat-itm-' + handle + '">' + groupElem + '</li>'; return groupElem; } return ''; }; if (myChats && myChats.length) { isActiveMember = myChats.every(function(chat) { return chat.members[u_handle] !== undefined && chat.members[u_handle] !== -1; }); var top5 = 5; // defined in specs, top 5 contacts var sortedChats = obj_values(myChats.toJS()); sortedChats.sort(M.sortObjFn("lastActivity", -1)); for (var chati = 0; chati < sortedChats.length; chati++) { var chatRoom = sortedChats[chati]; if (chatRoom.isArchived()) { continue; } if (chatRoom.isReadOnly()) { continue; } var isValidGroupOrPubChat = false; if (chatRoom.type === 'group') { if (!$.len(chatRoom.members)) { continue; } isValidGroupOrPubChat = true; } else if ( chatRoom.type === "public" && chatRoom.membersSetFromApi && chatRoom.membersSetFromApi.members[u_handle] >= 2 ) { isValidGroupOrPubChat = true; } if (isValidGroupOrPubChat) { var gNames = []; if (chatRoom.topic) { gNames.push(chatRoom.topic); } else { ChatdIntegration._ensureContactExists(chatRoom.members); for (var grHandle in chatRoom.members) { if (gNames.length > 4) { break; } if (grHandle !== u_handle) { gNames.push(M.getNameByHandle(grHandle)); } } } if (gNames.length) { if (nbOfRecent < top5) { var gElem = createGroupEntry( gNames, Object.keys(chatRoom.members).length, chatRoom.roomId, chatRoom ); contactGeneratedList += gElem; } else { myContacts.push({ id: Object.keys(chatRoom.members).length, name: gNames[0], handle: chatRoom.roomId, isG: true, gMembers: gNames }); } nbOfRecent++; } } else if (nbOfRecent < top5) { var contactHandle; for (var ctHandle in chatRoom.members) { if (ctHandle !== u_handle) { contactHandle = ctHandle; break; } } if ( contactHandle && M.u[contactHandle] && M.u[contactHandle].c === 1 && M.u[contactHandle].m ) { addedContactsByRecent.push(contactHandle); var ctElemC = createContactEntry( M.getNameByHandle(contactHandle), M.u[contactHandle].m, contactHandle ); contactGeneratedList += ctElemC; nbOfRecent++; } } } } if (myContacts && myContacts.length) { myContacts.sort(M.sortObjFn("name", 1)); for (var a = 0; a < myContacts.length; a++) { if (addedContactsByRecent.includes(myContacts[a].handle)) { continue; } var ctElem; if (!myContacts[a].isG) { ctElem = createContactEntry(myContacts[a].name, myContacts[a].id, myContacts[a].handle); } else { ctElem = createGroupEntry( myContacts[a].gMembers, myContacts[a].id, myContacts[a].handle, megaChat.chats[myContacts[a].handle] ); } contactGeneratedList += ctElem; } } if ( myChats && myChats.length && isActiveMember || myContacts && myContacts.length ) { contactGeneratedList = '<ul id="' + ulListId + '">' + contactGeneratedList + '</ul>'; $contactsContentBlock.safeHTML(contactGeneratedList); $conversationTab.addClass('active'); $conversationNoConvTab.removeClass('active'); $conversationTabHeader.removeClass('hidden'); } else { $conversationTab.removeClass('active'); $conversationNoConvTab.addClass('active'); $conversationTabHeader.addClass('hidden'); } }; /** * Handle DOM directly, no return value. * @param {String} dialogTabClass dialog tab class name. * @param {String} parentTag tag of source element. * @param {String} htmlContent html content. * @private */ var handleDialogTabContent = function(dialogTabClass, parentTag, htmlContent) { var tabClass = '.' + dialogTabClass; var $tab = $('.fm-picker-dialog-tree-panel' + tabClass, $dialog); var html = String(htmlContent) .replace(/treea_/ig, 'mctreea_') .replace(/treesub_/ig, 'mctreesub_') .replace(/treeli_/ig, 'mctreeli_'); $('.dialog-content-block', $tab).empty().safeHTML(html); if ($('.dialog-content-block ' + parentTag, $tab).children().length) { // Items available, hide empty message $('.dialog-empty-block', $dialog).removeClass('active'); $('.fm-picker-dialog-panel-header', $tab).removeClass('hidden'); $tab.addClass('active'); // TODO check why this was only here } else { // Empty message, no items available $('.dialog-empty-block' + tabClass, $dialog).addClass('active'); $('.fm-picker-dialog-panel-header', $tab).addClass('hidden'); } }; /** * Build tree for a move/copy dialog. * @private */ var buildDialogTree = function() { var $dpa = $('.fm-picker-dialog-panel-arrows', $dialog).removeClass('hidden'); if (section === 'cloud-drive' || section === 'folder-link') { M.buildtree(M.d[M.RootID], 'fm-picker-dialog', 'cloud-drive'); const $folderContainer = $('.folder-container', $dialog); const $treePanel = $('.fm-picker-dialog-tree-panel', $folderContainer); const $cloudDrive = $treePanel.filter('.cloud-drive'); const $cloudDriveFolders = $('.dialog-content-block ul li', $cloudDrive); const $dialogPanelHeader = $('.fm-picker-dialog-panel-header', $treePanel); const $emptyCloudDrive = $('.dialog-empty-block.cloud-drive', $folderContainer); if ($cloudDriveFolders.length > 0) { $emptyCloudDrive.removeClass('active'); $cloudDrive.addClass('active'); $dialogPanelHeader.removeClass('hidden'); } } else if (section === 'shared-with-me') { M.buildtree({h: 'shares'}, 'fm-picker-dialog'); } else if (section === 'rubbish-bin') { M.buildtree({h: M.RubbishID}, 'fm-picker-dialog'); } else if (section === 'conversations') { if (window.megaChatIsReady) { // prepare Conversation Tab if needed $dpa.addClass('hidden'); handleConversationTabContent(); } else { console.error('MEGAchat is not ready'); } } if (!treesearch) { $('.fm-picker-dialog .nw-fm-tree-item').removeClass('expanded active opened selected'); $('.fm-picker-dialog ul').removeClass('opened'); } disableFolders(); onIdle(() => { initPerfectScrollbar($('.right-pane.active .dialog-tree-panel-scroll', $dialog)); // Place tooltip for long names const folderNames = $dialog[0].querySelectorAll('.nw-fm-tree-folder:not(.inbound-share)'); for (let i = folderNames.length; i--;) { const elm = folderNames[i]; if (elm.scrollWidth > elm.offsetWidth) { elm.setAttribute('data-simpletip', elm.textContent); elm.classList.add('simpletip'); } } }); }; /** * Dialogs content handler * @param {String} dialogTabClass Dialog tab class name. * @param {String} [buttonLabel] Action button label. * @private */ var handleDialogContent = function(dialogTabClass, buttonLabel) { section = dialogTabClass || 'cloud-drive'; buttonLabel = buttonLabel || getActionButtonLabel(); let title = ''; var $pickerButtons = $('.fm-picker-dialog-button', $dialog).removeClass('active'); $('.dialog-sorting-menu', $dialog).addClass('hidden'); $('.dialog-empty-block', $dialog).removeClass('active'); $('.fm-picker-dialog-tree-panel', $dialog).removeClass('active'); $('.fm-picker-dialog-panel-arrows', $dialog).removeClass('active'); $('.fm-picker-dialog-desc', $dialog).addClass('hidden'); // Hide description $dialog.removeClass('fm-picker-file-request'); $('button.js-close', $dialog).addClass('hidden'); // inherited dialog content... var html = section !== 'conversations' && $('.content-panel.' + section).html(); var $permissionSelect = $('.permissions.dropdown-input', $dialog); var $permissionIcon = $('i.permission', $permissionSelect); var $permissionOptions = $('.option', $permissionSelect); // all the different buttons // var $cloudDrive = $pickerButtons.filter('[data-section="cloud-drive"]'); var $sharedMe = $pickerButtons.filter('[data-section="shared-with-me"]'); var $conversations = $pickerButtons.filter('[data-section="conversations"]'); var $rubbishBin = $pickerButtons.filter('[data-section="rubbish-bin"]'); // Action button label $('.dialog-picker-button span', $dialog).text(buttonLabel); // if the site is initialized on the chat, $.selected may be `undefined`, // which may cause issues doing .length on it in dialogs.js, so lets define it as empty array // if is not def. $.selected = $.selected || []; $conversations.addClass('hidden'); $rubbishBin.addClass('hidden'); if ( ($.dialog === 'copy' && $.selected.length && !$.saveToDialog || $.copyToUpload) && !( // Don't allow copying incoming shared folders to chat as it is currently not functional $.dialog === 'copy' && $.selected.filter(n => !M.isFileNode(M.getNodeByHandle(n)) && M.getNodeRoot(n) === 'shares').length ) ) { $conversations.removeClass('hidden'); } const nodeRoot = M.getNodeRoot($.selected[0]); if (!u_type || $.copyToShare || $.mcImport || $.selectFolderDialog || $.saveAsDialog) { $rubbishBin.addClass('hidden'); $conversations.addClass('hidden'); } if (nodeRoot === M.RubbishID || $.copyDialog || $.moveDialog) { $rubbishBin.addClass('hidden'); } if (nodeRoot === M.RubbishID || $.copyToShare || $.selectFolderDialog || !u_type || M.currentdirid === 'devices') { $sharedMe.addClass('hidden'); } else { $sharedMe.removeClass('hidden'); } if ($.copyToUpload) { $('.fm-picker-notagain', $dialog).removeClass('hidden'); $('footer', $dialog).removeClass('dialog-bottom'); } else { $('.fm-picker-notagain', $dialog).addClass('hidden'); $('footer', $dialog).addClass('dialog-bottom'); } handleDialogTabContent(section, section === 'conversations' ? 'div' : 'ul', html); buildDialogTree(); // 'New Folder' button if (section === 'shared-with-me' || section === 'conversations') { $('.dialog-newfolder-button', $dialog).addClass('hidden'); } else { $('.dialog-newfolder-button', $dialog).removeClass('hidden'); } // Reset the value of permission and permissions list if ($permissionSelect.length > 0) { $permissionSelect.attr('data-access', 'read-only'); $permissionIcon.attr('class', 'permission sprite-fm-mono icon-read-only'); $('> span', $permissionSelect).text(l[7534]); $permissionOptions.removeClass('active'); $('.permissions .option[data-access="read-only"]', $dialog).addClass('active'); $('.permissions .option[data-state="active"]', $dialog).removeAttr('data-state'); } // If copying from contacts tab (Ie, sharing) if (!$.saveToDialog && section === 'cloud-drive' && (M.currentrootid === 'chat' || $.copyToShare)) { $('.dialog-newfolder-button', $dialog).addClass('hidden'); $permissionSelect.removeClass('hidden'); bindDropdownEvents($permissionSelect); $permissionOptions.rebind('click.selectpermission', function() { var $this = $(this); $permissionIcon.attr('class', 'permission sprite-fm-mono ' + $this.attr('data-icon')); $permissionSelect.attr('data-access', $this.attr('data-access')); }); } else if ($.selectFolderDialog) { $permissionSelect.addClass('hidden'); if ($.fileRequestNew) { $dialog.addClass('fm-picker-file-request'); $('.fm-picker-dialog-desc', $dialog) .removeClass('hidden'); $('.fm-picker-dialog-desc p', $dialog) .text(l.file_request_select_folder_desc); $('button.js-close', $dialog).removeClass('hidden'); title = l.file_request_select_folder_title; } else { title = l[16533]; } } else { $permissionSelect.addClass('hidden'); } if ($.chatAttachmentShare && section !== 'conversations') { $permissionSelect.addClass('hidden'); } // 'New contact' button if (section === 'conversations') { $('.dialog-newcontact-button', $dialog).removeClass('hidden'); } else { $('.dialog-newcontact-button', $dialog).addClass('hidden'); } // Activate tab $('.fm-picker-dialog-button[data-section="' + section + '"]', $dialog).addClass('active'); $('header h2', $dialog).text(title || getDialogTitle()); }; /** * Handle opening dialogs and content * @param {String} aTab The section/tab to activate * @param {String} [aTarget] The target folder for the operation * @param {String|Object} [aMode] Copy dialog mode (share, save, etc) */ var handleOpenDialog = function(aTab, aTarget, aMode) { // Save an snapshot of selected nodes at time of invocation, given $.hideContextMenu(); could swap // the internal list as part of cleanup performed during closing context-menus, e.g. for in-shares const preUserSelection = window.selectionManager && selectionManager.get_selected() || $.selected; onIdle(function() { /** @name $.copyDialog */ /** @name $.moveDialog */ /** @name $.selectFolderDialog */ /** @name $.saveAsDialog */ $[$.dialog + 'Dialog'] = $.dialog; if (aMode) { /** @name $.copyToShare */ /** @name $.copyToUpload */ /** @name $.saveToDialog */ $[aMode.key || aMode] = aMode.value || true; } if (preUserSelection && preUserSelection.length) { const postSelection = window.selectionManager && selectionManager.get_selected() || $.selected; if (preUserSelection !== postSelection) { $.selected = preUserSelection; if (window.selectionManager) { selectionManager.reinitialize(); } } } $('.search-bar input', $dialog).val(''); $('.search-bar.placeholder .search-icon-reset', $dialog).addClass('hidden'); handleDialogContent(typeof aTab === 'string' && aTab); if (aTab === 'conversations') { setDialogBreadcrumb(M.currentrootid === 'chat' && aTarget !== M.RootID ? aTarget : ''); } else { setDialogBreadcrumb(aTarget); } setDialogButtonState($('.dialog-picker-button', $dialog).addClass('active')); setSelectedItems(true); }); $.hideContextMenu(); $dialog.removeClass('duplicate'); console.assert($dialog, 'The dialogs subsystem is not yet initialized!...'); }; /** Checks if the user can access dialogs copy/move/share */ var isUserAllowedToOpenDialogs = function() { if (M.isInvalidUserStatus()) { return false; } return true; }; // ------------------------------------------------------------------------ // ---- Public Functions -------------------------------------------------- /** * Refresh copy/move dialog content with newly created directory. * @global */ global.refreshDialogContent = function refreshDialogContent() { var tab = $.cfsection || 'cloud-drive'; var b = $('.content-panel.' + tab).html(); // Before refresh content remember what is opened. var $openedNodes = $('ul.opened[id^="mctreesub_"]', $dialog); $.openedDialogNodes = {}; for (var i = $openedNodes.length; i--;) { var id = $openedNodes[i].id.replace('mctreesub_', ''); $.openedDialogNodes[id] = 1; } handleDialogTabContent(tab, 'ul', b); buildDialogTree(); delete $.cfsection; // safe deleting delete $.openedDialogNodes; disableFolders($.moveDialog && 'move'); initPerfectScrollbar($('.right-pane.active .dialog-tree-panel-scroll', $dialog)); }; /** * A version of the Copy dialog used in the contacts page for sharing. * @param {String} [u_id] Share to contact handle. * @global */ global.openCopyShareDialog = function openCopyShareDialog(u_id) { // Not allowed chats if (isUserAllowedToOpenDialogs()) { M.safeShowDialog('copy', function() { $.shareToContactId = u_id; handleOpenDialog('cloud-drive', false, 'copyToShare'); return $dialog; }); } return false; }; /** * A version of the Copy dialog used when uploading. * @param {Array} files The files being uploaded. * @param {Object} [emptyFolders] Empty folders to create hierarchy for. * @global */ global.openCopyUploadDialog = function openCopyUploadDialog(files, emptyFolders) { // Is allowed chats if (isUserAllowedToOpenDialogs()) { M.safeShowDialog('copy', function() { var tab = M.chat ? 'conversations' : M.currentrootid === 'shares' ? 'shared-with-me' : 'cloud-drive'; var dir = M.currentdirid === 'transfers' ? M.lastSeenCloudFolder || M.RootID : M.currentdirid; closeMsg(); handleOpenDialog(tab, dir, { key: 'copyToUpload', value: [files, emptyFolders] }); return uiCheckboxes($dialog); }); } return false; }; /** * Generic function to open the Copy dialog. * @global */ global.openCopyDialog = function openCopyDialog(activeTab, onBeforeShown) { // Is allowed chats if (isUserAllowedToOpenDialogs()) { if ($.dialog === 'onboardingDialog') { closeDialog(); } M.safeShowDialog('copy', function() { if (typeof activeTab === 'function') { onBeforeShown = activeTab; activeTab = false; } if (typeof onBeforeShown === 'function') { onBeforeShown($dialog); } handleOpenDialog(activeTab, M.RootID); return $dialog; }); } return false; }; /** * Generic function to open the Move dialog. * @global */ global.openMoveDialog = function openMoveDialog() { // Not allowed chats if (isUserAllowedToOpenDialogs()) { M.safeShowDialog('move', function() { handleOpenDialog(0, M.RootID); return $dialog; }); } return false; }; /** * A version of the Copy dialog used for "Save to" in chat. * @global */ global.openSaveToDialog = function openSaveToDialog(node, cb, activeTab) { // Not allowed chats if (isUserAllowedToOpenDialogs()) { M.safeShowDialog('copy', function() { $.saveToDialogCb = cb; $.saveToDialogNode = node; handleOpenDialog(activeTab, M.RootID, activeTab !== 'conversations' && 'saveToDialog'); return $dialog; }); } return false; }; /** * Save As dialog show * @param {Object} node The node to save AS * @param {String} content Content to be saved * @param {Function} cb a callback to be called when the user "Save" * @returns {Object} The jquery object of the dialog */ global.openSaveAsDialog = function(node, content, cb) { // Not allowed chats M.safeShowDialog('saveAs', function() { const ltWSpaceWarning = new InputFloatWarning($dialog); ltWSpaceWarning.hide(); $.saveAsCallBack = cb; $.nodeSaveAs = typeof node === 'string' ? M.d[node] : node; $.saveAsContent = content; handleOpenDialog(null, node.p || M.RootID); return $dialog; }); return false; }; /** * A version of the select a folder dialog used for "New Shared Folder" in out-shares. * @global * @returns {Object} The jquery object of the dialog */ global.openNewSharedFolderDialog = function openNewSharedFolderDialog() { // Not allowed chats if (isUserAllowedToOpenDialogs()) { M.safeShowDialog('selectFolder', function() { $.selected = []; handleOpenDialog(0, M.RootID); $.selectFolderCallback = function() { closeDialog(); $.selected = [$.mcselected]; M.openSharingDialog(); }; return $dialog; }); } return false; }; /** * A version of the select a folder dialog used for selecting target folder * @global * @param {Function} cb A callback to be called when the user "Select" * @param {String} aMode Dialog mode (copy, move, share, save, etc) * @returns {Object} The jquery object of the dialog */ global.selectFolderDialog = function selectedFolderDialog(cb, aMode) { // Not allowed chats. if (isUserAllowedToOpenDialogs()) { M.safeShowDialog(aMode || 'selectFolder', () => { handleOpenDialog(0, M.RootID); $.selectFolderCallback = cb; return $dialog; }); } return false; }; /** * A version of the select a folder dialog used for "New File Request Folder" in out-shares. * @global * @param {Object} options Additional settings for new file request dialog * @returns {Object} The jquery object of the dialog */ global.openNewFileRequestDialog = function(options) { // Not allowed chats. if (!isUserAllowedToOpenDialogs()) { return false; } const postEventHandler = options && options.post || null; const closeEventHandler = options && options.close || null; if (postEventHandler) { const aMode = options && options.mode || 'fileRequestNew'; M.safeShowDialog('selectFolder', () => { handleOpenDialog(0, options && options.selectedNodeHandle || M.RootID, aMode); $.selectFolderCallback = function() { closeDialog(); postEventHandler($.mcselected); }; if (closeEventHandler) { closeEventHandler($dialog, options && options.selectedNodeHandle); } return $dialog; }); } return false; }; mBroadcaster.addListener('fm:initialized', function copyMoveDialogs() { if (folderlink) { return false; } $dialog = $('.mega-dialog.fm-picker-dialog'); var $btn = $('.dialog-picker-button', $dialog); var $swm = $('.shared-with-me', $dialog); var dialogTooltipTimer; var treePanelHeader = document.querySelector('.fm-picker-dialog-panel-header'); $('.fm-picker-dialog-tree-panel', $dialog).each(function(i, elm) { elm.insertBefore(treePanelHeader.cloneNode(true), elm.firstElementChild); }); treePanelHeader.parentNode.removeChild(treePanelHeader); // dialog sort $dialog.find('.fm-picker-dialog-panel-header').append($('.dialog-sorting-menu.hidden').clone()); $('.fm-picker-dialog-tree-panel.conversations .fm-picker-dialog-panel-header span:first').text(l[17765]); // close breadcrumb overflow menu $dialog.rebind('click.dialog', e => { if (!e.target.closest('.breadcrumb-dropdown, .breadcrumb-dropdown-link') && $('.breadcrumb-dropdown', $dialog).hasClass('active')) { $('.breadcrumb-dropdown', $dialog).removeClass('active'); } }); $('button.js-close, .dialog-cancel-button', $dialog).rebind('click', closeDialog); $('.fm-picker-dialog-button', $dialog).rebind('click', function _(ev) { section = $(this).attr('data-section'); if (section === 'shared-with-me' && ev !== -0x3f) { $('.dialog-content-block', $dialog).empty(); $('.fm-picker-dialog-button', $dialog).removeClass('active'); $(this).addClass('active'); dbfetch.geta(Object.keys(M.c.shares || {}), new MegaPromise()) .always(function() { if (section === 'shared-with-me') { _.call(this, -0x3f); } }.bind(this)); return false; } treesearch = false; handleDialogContent(section); $('.search-bar input', $dialog).val(''); $('.search-bar.placeholder .search-icon-reset', $dialog).addClass('hidden'); $('.nw-fm-tree-item', $dialog).removeClass('selected'); if ($.copyToShare) { setDialogBreadcrumb(); } else if (section === 'cloud-drive' || section === 'folder-link') { setDialogBreadcrumb(M.RootID); } else if (section === 'rubbish-bin') { setDialogBreadcrumb(M.RubbishID); } else { setDialogBreadcrumb(); } setDialogButtonState($btn); }); /** * On click, copy dialog, dialog-sorting-menu will be shown. * Handles that valid informations about current sorting options * for selected tab of copy dialog are up to date. */ $('.fm-picker-dialog-panel-arrows', $dialog).rebind('click', function() { var $self = $(this); if (!$self.hasClass('active')) { // There are four menus for each tab: get menu for active tab var $menu = $self.siblings('.dialog-sorting-menu'); var p = $self.position(); $menu.css('left', p.left + 24 + 'px'); $menu.css('top', p.top - 8 + 'px'); // @ToDo: Make sure .by is hadeled properly once when we have chat available // Copy dialog key only var key = $.dialog[0].toUpperCase() + $.dialog.substr(1) + section; $('.dropdown-item', $menu).removeClass('active asc desc'); $('.sort-arrow', $menu).removeClass('icon-up icon-down'); const by = escapeHTML(M.sortTreePanel[key] && M.sortTreePanel[key].by || 'name'); const dir = M.sortTreePanel[key] && M.sortTreePanel[key].dir || 1; var $sortbutton = $('.dropdown-item[data-by="' + by + '"]', $menu); $sortbutton.addClass(dir > 0 ? 'asc' : 'desc').addClass('active'); $('.sort-arrow', $sortbutton).addClass(dir > 0 ? 'icon-up' : 'icon-down'); $self.addClass('active'); $dialog.find('.dialog-sorting-menu').removeClass('hidden'); } else { $self.removeClass('active'); $dialog.find('.dialog-sorting-menu').addClass('hidden'); } }); $('.dialog-sorting-menu .dropdown-item', $dialog).rebind('click', function() { var $self = $(this); // Arbitrary element data var data = $self.data(); var key = $.dialog[0].toUpperCase() + $.dialog.substr(1) + section; var $arrowIcon = $('.sort-arrow', $self).removeClass('icon-down icon-up'); var sortDir; if (data.by) { M.sortTreePanel[key].by = data.by; } $self.removeClass('asc desc'); if ($self.hasClass('active')) { M.sortTreePanel[key].dir *= -1; sortDir = M.sortTreePanel[key].dir > 0 ? 'asc' : 'desc'; $self.addClass(sortDir); } buildDialogTree(); // Disable previously selected $('.sorting-menu-item', $self.parent()).removeClass('active'); $('.sort-arrow', $self.parent()).removeClass('icon-down icon-up'); $self.addClass('active'); // Change icon $arrowIcon.addClass(sortDir === 'asc' ? 'icon-up' : 'icon-down'); // Hide menu $('.dialog-sorting-menu', $dialog).addClass('hidden'); $('.fm-picker-dialog-panel-arrows.active').removeClass('active'); }); $('.search-bar input', $dialog).rebind('keyup.dsb', function(ev) { var value = String($(this).val()).toLowerCase(); var exit = ev.keyCode === 27 || !value; if (value) { $('.search-bar.placeholder .search-icon-reset', $dialog).removeClass('hidden'); } else { $('.search-bar.placeholder .search-icon-reset', $dialog).addClass('hidden'); } if (section === 'conversations') { var $lis = $('.nw-contact-item', $dialog).parent(); if (exit) { $lis.removeClass('tree-item-on-search-hidden'); if (value) { $(this).val('').trigger("blur"); } } else { $lis.addClass('tree-item-on-search-hidden').each(function(i, elm) { var sel = ['.nw-contact-name', '.nw-contact-email']; for (i = sel.length; i--;) { var tmp = elm.querySelector(sel[i]); if (tmp) { tmp = String(tmp.textContent).toLowerCase(); if (tmp.indexOf(value) !== -1) { elm.classList.remove('tree-item-on-search-hidden'); break; } } } }); } onIdle(function() { initPerfectScrollbar($('.right-pane.active .dialog-tree-panel-scroll', $dialog)); }); } else { if (exit) { treesearch = false; if (value) { $(this).val('').trigger("blur"); } } else { treesearch = value; } delay('mctree:search', buildDialogTree); } return false; }); $('.search-bar.placeholder .search-icon-reset', $dialog).rebind('click.dsb', () => { $('.search-bar input', $dialog).val('').trigger('keyup.dsb'); }); $('.dialog-newfolder-button', $dialog).rebind('click', function() { $dialog.addClass('arrange-to-back'); $.cfpromise = new MegaPromise(); $.cftarget = $.mcselected || (section === 'cloud-drive' ? M.RootID : M.RubbishID); $.cfsection = section; createFolderDialog(); $('.mega-dialog.create-folder-dialog .create-folder-size-icon').addClass('hidden'); // Auto-select the created folder. $.cfpromise.done(function(h) { var p = Object(M.d[h]).p || $.cftarget; // Make sure parent has selected class to make it expand $('#mctreea_' + p, $dialog).addClass('selected'); selectTreeItem(p); selectTreeItem(h); }); }); $('.dialog-newcontact-button', $dialog).rebind('click', function() { closeDialog(); contactAddDialog(); }); $dialog.rebind('click', '.nw-contact-item', function() { const $this = $(this); const $scrollBlock = $('.right-pane.active .dialog-tree-panel-scroll', $dialog); if ($this.hasClass('selected')) { $this.removeClass('selected'); } else { $this.addClass('selected'); } setDialogBreadcrumb(); setDialogButtonState($btn); initPerfectScrollbar($scrollBlock); // Scroll the element into view, only needed if element triggered. scrollToElement($scrollBlock, $this); }); $dialog.rebind('click', '.nw-fm-tree-item', function(e) { var ts = treesearch; var old = $.mcselected; const $scrollBlock = $('.right-pane.active .dialog-tree-panel-scroll', $dialog); setDialogBreadcrumb(String($(this).attr('id')).replace('mctreea_', '')); treesearch = false; M.buildtree({h: $.mcselected}, 'fm-picker-dialog', section); treesearch = ts; disableFolders(); var c = $(e.target).attr('class'); // Sub-folder exist? if (c && c.indexOf('nw-fm-tree-arrow') > -1) { c = $(this).attr('class'); // Sub-folder expanded if (c && c.indexOf('expanded') > -1) { $(this).removeClass('expanded'); $('#mctreesub_' + $.mcselected).removeClass('opened'); } else { $(this).addClass('expanded'); $('#mctreesub_' + $.mcselected).addClass('opened'); } } else { c = $(this).attr('class'); if (c && c.indexOf('selected') > -1) { if (c && c.indexOf('expanded') > -1) { $(this).removeClass('expanded'); $('#mctreesub_' + $.mcselected).removeClass('opened'); } else { $(this).addClass('expanded'); $('#mctreesub_' + $.mcselected).addClass('opened'); } } } if (!$(this).is('.disabled')) { // unselect previously selected item $('.nw-fm-tree-item', $dialog).removeClass('selected'); $(this).addClass('selected'); $btn.removeClass('disabled'); } else if ($('#mctreea_' + old + ':visible').length) { setDialogBreadcrumb(old); $('#mctreea_' + old).addClass('selected'); } else { setDialogBreadcrumb(); } initPerfectScrollbar($scrollBlock); // Disable action button if there is no selected items setDialogButtonState($btn); // Set opened & expanded ancestors, only needed if element triggered. $(this).parentsUntil('.dialog-content-block', 'ul').addClass('opened') .prev('.nw-fm-tree-item').addClass('expanded'); // Scroll the element into view, only needed if element triggered. scrollToElement($scrollBlock, $(this)); // // If not copying from contacts tab (Ie, sharing) if (!(section === 'cloud-drive' && (M.currentrootid === 'chat' || $.copyToShare))) { if ($.mcselected && M.getNodeRights($.mcselected) > 0) { $('.dialog-newfolder-button', $dialog).removeClass('hidden'); } else { $('.dialog-newfolder-button', $dialog).addClass('hidden'); } } }); $swm.rebind('mouseenter', '.nw-fm-tree-item', function _try(ev) { var h = $(this).attr('id').replace('mctreea_', ''); if (ev !== 0xEFAEE && !M.c[h]) { var self = this; dbfetch.get(h).always(function() { _try.call(self, 0xEFAEE); }); return false; } var share = shares[h]; var owner = share && share.owner; var user = M.getUserByHandle(owner); if (!user) { return false; } var $item = $(this).find('.nw-fm-tree-folder'); var itemLeftPos = $item.offset().left; var itemTopPos = $item.offset().top; var $tooltip = $('.contact-preview', $dialog); var avatar = useravatar.contact(owner, '', 'div'); var note = !share.level && !share.circular && l[19340]; var displayName = user.nickname || user.name || user.m; $tooltip.find('.contacts-info.body') .safeHTML( avatar + '<div class="user-card-data no-status">' + ' <div class="user-card-name small selectable-txt">@@<span class="grey">(@@)</span></div>' + ' <div class="user-card-email selectable-txt small">@@</div>' + ' <div class="user-card-email selectable-txt small @@">@@</div>' + '</div>', displayName || '', l[8664], user.m || '', note ? 'note' : '', note || '' ); clearTimeout(dialogTooltipTimer); dialogTooltipTimer = setTimeout(function() { $tooltip.removeClass('hidden'); $tooltip.css({ 'left': itemLeftPos + $item.outerWidth() / 2 - $tooltip.outerWidth() / 2 + 'px', 'top': (itemTopPos - (note ? 120 : 75)) + 'px' }); }, 200); return false; }); $swm.rebind('mouseleave', '.nw-fm-tree-item', function() { var $tooltip = $('.contact-preview', $dialog); clearTimeout(dialogTooltipTimer); $tooltip.hide(); return false; }); // Handle conversations tab item selection $dialog.rebind('click', '.nw-conversations-item', function() { setDialogBreadcrumb(String($(this).attr('id')).replace('contact2_', '')); // unselect previously selected item $('.nw-conversations-item', $dialog).removeClass('selected'); $(this).addClass('selected'); $btn.removeClass('disabled'); // Disable action button if there is no selected items setDialogButtonState($btn); }); const handleCopyToChat = async(chats, selectedNodes) => { const files = []; const folders = []; const foreignNodes = []; for (const sel of selectedNodes) { if (M.isFileNode(sel)) { if (M.d[sel.h]) { files.push(sel); } else { foreignNodes.push(sel); } } else { const node = M.getNodeByHandle(sel); if (M.d[node.h]) { if (node.t) { folders.push(node.h); } else { files.push(node); } } else { foreignNodes.push(node); } } } if (!foreignNodes.length) { const noOpen = $.noOpenChatFromPreview; delete $.noOpenChatFromPreview; megaChat.openChatAndAttachNodes(chats, [...folders, ...files], noOpen).dump(); return; } const mcf = await M.myChatFilesFolder.get(true).catch(() => { if (d) { console.error('Failed to allocate "My chat files" folder'); } throw ENOENT; }); M.injectNodes(foreignNodes, mcf.h, res => { if (Array.isArray(res) && res.length) { const noOpen = $.noOpenChatFromPreview; delete $.noOpenChatFromPreview; megaChat.openChatAndAttachNodes(chats, [...res, ...folders, ...files], noOpen).dump(); } else if (d) { console.warn('Unable to inject nodes... no longer existing?', res); } }); }; // Handle copy/move/share button action $btn.rebind('click', function() { var chats = getSelectedChats(); var skip = !$.mcselected && section !== 'conversations'; if (skip || $(this).hasClass('disabled')) { return false; } var selectedNodes = ($.selected || []).concat(); // closeDialog would cleanup some $.* variables, so we need them cloned here var saveToDialogNode = $.saveToDialogNode; var saveToDialogCb = $.saveToDialogCb; var saveToDialog = $.saveToDialog || saveToDialogNode; var shareToContactId = $.shareToContactId; delete $.saveToDialogPromise; if ($.copyToUpload) { var data = $.copyToUpload; var target = $.mcselected; if (section === 'conversations') { target = chats.map(function(h) { if (megaChat.chats[h]) { return megaChat.chats[h].getRoomUrl().replace("fm/", ""); } else if (M.u[h]) { return 'chat/p/' + h; } else { if (d) { console.error("Chat room not found for handle:", h); } return ''; } }); } if ($('.notagain', $dialog).prop('checked')) { mega.config.setn('ulddd', 1); } closeDialog(); M.addUpload(data[0], false, data[1], target); return false; } if (typeof $.selectFolderCallback === 'function') { $.selectFolderCallback(); return false; } if ($.moveDialog) { if (section === "shared-with-me") { var $tooltip = $('.contact-preview', $dialog); clearTimeout(dialogTooltipTimer); $tooltip.hide(); } closeDialog(); M.safeMoveNodes($.mcselected); return false; } if ($.nodeSaveAs) { var $nameInput = $('#f-name-input', $dialog); var saveAsName = $nameInput.val(); var eventName = 'input.saveas'; var removeErrorStyling = function() { $nameInput.removeClass('error'); $dialog.removeClass('duplicate'); $nameInput.off(eventName); }; removeErrorStyling(); var errMsg = ''; if (!saveAsName.trim()) { errMsg = l[5744]; } else if (!M.isSafeName(saveAsName)) { errMsg = saveAsName.length > 250 ? l.LongName1 : l[24708]; } else if (duplicated(saveAsName, $.mcselected)) { errMsg = l[23219]; } if (errMsg) { $('.duplicated-input-warning span', $dialog).text(errMsg); $dialog.addClass('duplicate'); $nameInput.addClass('error'); $nameInput.rebind(eventName, function() { removeErrorStyling(); return false; }); setTimeout(() => { removeErrorStyling(); }, 2000); return false; } $nameInput.rebind(eventName, function() { removeErrorStyling(); return false; }); $nameInput.off(eventName); var nodeToSave = $.nodeSaveAs; closeDialog(); M.getStorageQuota().then(data => { if (data.isFull) { ulmanager.ulShowOverStorageQuotaDialog(); return false; } mega.fileTextEditor.saveFileAs(saveAsName, $.mcselected, $.saveAsContent, nodeToSave).done( (handle) => { if ($.saveAsCallBack) { $.selected = Array.isArray(handle) ? handle : [handle]; $.saveAsCallBack(handle); } } ); }); return false; } closeDialog(); if (saveToDialog) { saveToDialogCb(saveToDialogNode, section === 'conversations' && chats || $.mcselected); return false; } // Get active tab if (section === 'cloud-drive' || section === 'folder-link' || section === 'rubbish-bin') { // If copying from contacts tab (Ie, sharing) if ($(this).text().trim() === l[1344]) { var user = { u: shareToContactId ? shareToContactId : M.currentdirid }; var spValue = $('.permissions.dropdown-input', $dialog).attr('data-access'); if (spValue === 'read-and-write') { user.r = 1; } else if (spValue === 'full-access') { user.r = 2; } else { user.r = 0; } mega.keyMgr.setShareSnapshot($.mcselected) .then(() => { doShare($.mcselected, [user], true); }) .catch(dump); } else { M.copyNodes(selectedNodes, $.mcselected); } } else if (section === 'shared-with-me') { M.copyNodes(getNonCircularNodes(selectedNodes), $.mcselected); } else if (section === 'conversations') { if (window.megaChatIsReady) { handleCopyToChat(chats, selectedNodes).catch(dump); } else if (d) { console.error('MEGAchat is not ready'); } } delete $.onImportCopyNodes; return false; }); return 0xDEAD; }); })(self); (function _properties(global) { 'use strict'; /** * Handles node properties/info dialog contact list content * @param {Object} $dialog The properties dialog * @param {Array} users The list of users to whom we're sharing the selected nodes * @private */ function fillPropertiesContactList($dialog, users) { var MAX_CONTACTS = 5; var shareUsersHtml = ''; var $shareUsers = $dialog.find('.properties-body .properties-context-menu') .empty() .append('<div class="properties-context-arrow"></div>'); for (var i = users.length; i--;) { var user = users[i]; var userHandle = user.u || user.p; var hidden = i >= MAX_CONTACTS ? 'hidden' : ''; var status = megaChatIsReady && megaChat.getPresenceAsCssClass(user.u); shareUsersHtml += '<div class="properties-context-item ustatus ' + escapeHTML(userHandle) + ' ' + (status || '') + ' ' + hidden + '" data-handle="' + escapeHTML(userHandle) + '">' + '<div class="properties-contact-status"></div>' + '<span>' + escapeHTML(M.getNameByHandle(userHandle)) + '</span>' + '</div>'; } if (users.length > MAX_CONTACTS) { shareUsersHtml += '<div class="properties-context-item show-more">' + '<span>...' + escapeHTML(l[10663]).replace('[X]', users.length - MAX_CONTACTS) + '</span>' + '</div>'; } if (shareUsersHtml !== '') { $shareUsers.append(shareUsersHtml); } } function _propertiesDialog(action) { const update = action === 3; const close = !update && action; const $dialog = $('.mega-dialog.properties-dialog', '.mega-dialog-container'); const $icon = $('.properties-file-icon', $dialog); $(document).off('MegaCloseDialog.Properties'); if (close) { delete $.propertiesDialog; if (close !== 2) { closeDialog(); } else { fm_hideoverlay(); } $('.contact-list-icon').removeClass('active'); $('.properties-context-menu').fadeOut(200); if ($.hideContextMenu) { $.hideContextMenu(); } return true; } $dialog.removeClass('multiple folders-only two-elements shared shared-with-me'); $dialog.removeClass('read-only read-and-write full-access taken-down undecryptable'); $dialog.removeClass('hidden-context versioning'); $('.properties-elements-counter span').text(''); var users = null; var filecnt = 0; var foldercnt = 0; var size = 0; var sfilecnt = 0; var sfoldercnt = 0; var vsize = 0; var svfilecnt = 0; var n; var versioningFlag = false; var hasValid = false; var icons = []; const selected = []; for (var i = $.selected.length; i--;) { const node = M.getNodeByHandle($.selected[i]); if (!node) { if (d) { console.log('propertiesDialog: invalid node', $.selected[i]); } continue; } n = node; hasValid = true; icons.push(fileIcon(n)); selected.push($.selected[i]); if (n.t) { size += n.tb;// - (n.tvb || 0); sfilecnt += n.tf;// - (n.tvf || 0); sfoldercnt += n.td; foldercnt++; vsize += n.tvb || 0; svfilecnt += n.tvf || 0; } else { filecnt++; size += n.s; vsize += n.tvb || 0; svfilecnt += n.tvf || 0; } } if (!hasValid) { // $.selected had no valid nodes! propertiesDialog(1); return msgDialog('warninga', l[882], l[24196]); } if (selected.length > 1) { n = Object.create(null); // empty n [multiple selection] } if (n.tvf) { $dialog.addClass('versioning'); versioningFlag = true; } if ($.dialog === 'onboardingDialog') { closeDialog(); } M.safeShowDialog('properties', function() { $.propertiesDialog = 'properties'; // If it is download page or // node is not owned by current user on chat // (possible old shared file and no longer exist on cloud-drive, or shared by other user in the chat room), // don't display path if (page === 'download' || M.chat && n.u !== u_handle || !n.h && !M.d[M.currentdirid]) { $('.properties-breadcrumb', $dialog).addClass('hidden'); } else { // on idle so we can call renderPathBreadcrumbs only once the info dialog is rendered. onIdle(() => { // we pass the filehandle, so it is available if we search on files on search M.renderPathBreadcrumbs(n.h, true); }); } return $dialog; }); var exportLink = new mega.Share.ExportLink({}); var isTakenDown = exportLink.isTakenDown(selected); var isUndecrypted = missingkeys[n.h]; var notificationText = ''; var p = {}; if (filecnt + foldercnt === 1) { // one item if (isTakenDown || isUndecrypted) { if (isTakenDown) { $dialog.addClass('taken-down'); notificationText = l[7703] + '\n'; } if (isUndecrypted) { $dialog.addClass('undecryptable'); if (n.t) { notificationText += l[8595]; } else { notificationText += l[8602]; } } showToast('clipboard', notificationText); } var star = ''; const rootHandle = M.getNodeRoot(n.h); if (n.fav && !folderlink && rootHandle !== M.RubbishID) { star = ' sprite-fm-mono icon-favourite-filled'; } $dialog.find('.file-status-icon').attr('class', 'file-status-icon ' + star); if (icons.includes('outgoing')) { $dialog.addClass('shared'); } if (typeof n.r === "number") { var zclass = "read-only"; if (n.r === 1) { zclass = "read-and-write"; } else if (n.r === 2) { zclass = "full-access"; } $dialog.addClass('shared shared-with-me ' + zclass); } var user = Object(M.d[n.su || n.p]); if (d) { console.log('propertiesDialog', n, user); } p.t6 = ''; p.t7 = ''; if (filecnt) { p.t3 = l[5605]; p.t5 = ' second'; if (n.mtime) { p.t6 = l[22129]; p.t7 = htmlentities(time2date(n.mtime)); } } else { p.t3 = l[22130]; p.t5 = ''; } p.t1 = l[1764]; // Hide context menu button if (n.h === M.RootID || slideshowid || n.h === M.RubbishID) { $dialog.addClass('hidden-context'); } if (isUndecrypted) { p.t2 = htmlentities(l[8649]); } else if (mega.backupCenter.selectedSync && mega.backupCenter.selectedSync.nodeHandle === n.h && mega.backupCenter.selectedSync.localName) { p.t2 = htmlentities(mega.backupCenter.selectedSync.localName); } else if (n.name) { p.t2 = htmlentities(n.name); } else if (n.h === M.RootID) { p.t2 = htmlentities(l[164]); } else if (n.h === M.InboxID) { p.t2 = htmlentities(l.restricted_folder_button); } else if (n.h === M.RubbishID) { p.t2 = htmlentities(l[167]); } if (page.substr(0, 7) === 'fm/chat') { $dialog.addClass('hidden-context'); } p.t4 = versioningFlag ? bytesToSize(size + vsize) : bytesToSize(size); p.t9 = n.ts && htmlentities(time2date(n.ts)) || ''; p.t8 = p.t9 ? l[22143] : ''; p.t12 = ' second'; p.t13 = l[22144]; p.t14 = mega.icu.format(l.version_count, svfilecnt); p.t15 = l[22145]; p.t16 = bytesToSize(size); p.t17 = ' second'; p.t18 = l[22146]; p.t19 = bytesToSize(vsize); if (foldercnt) { p.t6 = l[22147]; p.t7 = fm_contains(sfilecnt, sfoldercnt, true); p.t15 = l[22148]; if ($dialog.hasClass('shared')) { users = M.getSharingUsers(selected, true); // In case that user doesn't share with other // Do NOT show contact informations in property dialog if (!users.length) { p.hideContacts = true; } else { p.t8 = l[5611]; p.t9 = mega.icu.format(l.contact_count, users.length); p.t11 = n.ts ? htmlentities(time2date(n.ts)) : ''; p.t10 = p.t11 ? l[6084] : ''; $('.properties-elements-counter span').text(typeof n.r === "number" ? '' : users.length); fillPropertiesContactList($dialog, users); } } if ($dialog.hasClass('shared-with-me')) { p.t3 = l[5612]; var rights = l[55]; if (n.r === 1) { rights = l[56]; } else if (n.r === 2) { rights = l[57]; } p.t4 = rights; p.t6 = l[22157]; p.t7 = htmlentities(M.getNameByHandle(user.h)); p.t8 = l[22130]; p.t9 = versioningFlag ? bytesToSize(size + vsize) : bytesToSize(size); p.t10 = l[22147]; p.t11 = fm_contains(sfilecnt, sfoldercnt, true); } } if (filecnt && versioningFlag && M.currentrootid !== M.RubbishID) { p.t14 = '<a id="previousversions">' + p.t14 + '</a>'; } } else { $dialog.addClass('multiple folders-only'); p.t1 = ''; p.t2 = '<b>' + fm_contains(filecnt, foldercnt) + '</b>'; p.t3 = l[22130]; p.t4 = versioningFlag ? bytesToSize(size + vsize) : bytesToSize(size); if (foldercnt) { p.t5 = ''; p.t6 = l[22147]; p.t7 = fm_contains(sfilecnt + filecnt, sfoldercnt + foldercnt, true); } else { p.t5 = ' second'; } p.t8 = l[22149]; p.t9 = l[1025]; p.t12 = ''; p.t13 = l[22144]; p.t14 = mega.icu.format(l.version_count, svfilecnt); p.t15 = l[22148]; p.t16 = bytesToSize(size); p.t17 = ''; p.t18 = l[22146]; p.t19 = bytesToSize(vsize); } var vhtml = versioningFlag ? '<div class="properties-float-bl' + p.t12 + '"><span class="properties-small-gray">' + p.t13 + '</span>' + '<span class="propreties-dark-txt">' + p.t14 + '</span></div>' + '<div class="properties-float-bl"><span class="properties-small-gray">' + p.t15 + '</span>' + '<span class="propreties-dark-txt">' + p.t16 + '</span></div>' + '<div class="properties-float-bl' + p.t17 + '"><span class="properties-small-gray">' + p.t18 + '</span>' + '<span class="propreties-dark-txt">' + p.t19 + '</span></div>' : ''; var singlenodeinfohtml = '<div class="properties-float-bl' + p.t5 + '">' + '<span class="properties-small-gray">' + p.t6 + '</span>' + '<span class="propreties-dark-txt t7">' + p.t7 + '</span></div>'; var shareinfohtml = (typeof p.t10 === 'undefined' && typeof p.t11 === 'undefined') ? '' : '<div class="properties-float-bl"><div class="properties-small-gray t10">' + p.t10 + '</div>' + '<div class="propreties-dark-txt t11">' + p.t11 + '</div></div></div>'; var html = '<div class="properties-name-container"><div class="properties-small-gray">' + p.t1 + '</div>' + '<div class="properties-name-block"><div class="propreties-dark-txt">' + p.t2 + '</div>' + '</div></div>' + `<div class="properties-breadcrumb"><div class="properties-small-gray path">${l.path_lbl}</div>` + '<div class="fm-breadcrumbs-wrapper info">' + '<div class="crumb-overflow-link dropdown">' + '<a class="breadcrumb-dropdown-link info-dlg">' + '<i class="menu-icon sprite-fm-mono icon-options icon24"></i>' + '</a>' + '<i class="sprite-fm-mono icon-arrow-right icon16"></i>' + '</div>' + '<div class="fm-breadcrumbs-block info"></div>' + '<div class="breadcrumb-dropdown"></div>' + '</div>' + '</div>' + '<div class="properties-items"><div class="properties-float-bl"><span class="properties-small-gray">' + p.t3 + '</span>' + '<span class="propreties-dark-txt">' + p.t4 + '</span></div>' + vhtml + singlenodeinfohtml + '<div class="properties-float-bl">' + (n.h === M.RootID || n.h === M.RubbishID || n.h === M.InboxID ? '<div class="contact-list-icon sprite-fm-mono icon-info-filled"></div>' + '</div>' + shareinfohtml : '<div class="properties-small-gray">' + p.t8 + '</div><div class="propreties-dark-txt contact-list">' + '<span>' + p.t9 + '</span>' + '<div class="contact-list-icon sprite-fm-mono icon-info-filled"></div>' + '</div></div>' + shareinfohtml); $('.properties-txt-pad').safeHTML(html); if ($dialog.hasClass('shared-with-me')) { $('.properties-txt-pad').find('.contact-list-icon').remove(); } $('.properties-body', $dialog).rebind('click', function() { // Clicking anywhere in the dialog will close the context-menu, if open var $fsi = $('.file-settings-icon', $dialog); if ($fsi.hasClass('active')) { $fsi.click(); } // Clicking anywhere in the dialog would close the path breadcrumb dropdown if exists and open const $pathBreadcrumb = $('.breadcrumb-dropdown', $dialog); if ($pathBreadcrumb && $pathBreadcrumb.hasClass('active')) { $pathBreadcrumb.removeClass('active'); } }); if ((filecnt === 1) && (foldercnt === 0)) { $('#previousversions').rebind('click', function(ev) { if (M.currentrootid !== M.RubbishID) { if (slideshowid) { slideshow(n.h, 1); } fileversioning.fileVersioningDialog(n.h); closeDialog(); } }); } $('button.js-close', $dialog).rebind('click', _propertiesDialog); var __fsi_close = function() { $dialog.find('.file-settings-icon').removeClass('active'); $('.dropdown.body').removeClass('arrange-to-front'); $('.properties-dialog').removeClass('arrange-to-back'); $('.mega-dialog').removeClass('arrange-to-front'); $.hideContextMenu(); }; $dialog.find('.file-settings-icon').rebind('click context', function(e) { if (!$(this).hasClass('active')) { e.preventDefault(); e.stopPropagation(); $(this).addClass('active'); // $('.mega-dialog').addClass('arrange-to-front'); // $('.properties-dialog').addClass('arrange-to-back'); $('.dropdown.body').addClass('arrange-to-front'); e.currentTarget = $('#' + n.h); if (!e.currentTarget.length) { e.currentTarget = $('#treea_' + n.h); } e.calculatePosition = true; $.selected = [n.h]; M.contextMenuUI(e, n.h.length === 11 ? 5 : 1); } else { __fsi_close(); } return false; }); $(document).rebind('MegaCloseDialog.Properties', __fsi_close); if (p.hideContacts) { $('.properties-txt-pad .contact-list-icon', $dialog).addClass('hidden'); } if ($dialog.hasClass('shared')) { $('.contact-list-icon').rebind('click', function() { if (!$(this).hasClass('active')) { $(this).addClass('active'); var $pcm = $('.properties-context-menu'); var position = $(this).position(); $pcm.css({ 'left': position.left + 16 + 'px', 'top': position.top - $pcm.outerHeight() - 8 + 'px', 'transform': 'translateX(-50%)', }); $pcm.fadeIn(200); } else { $(this).removeClass('active'); $('.properties-context-menu').fadeOut(200); } return false; }); $('.properties-dialog').rebind('click', function() { var $list = $('.contact-list-icon'); if ($list.hasClass('active')) { $list.removeClass('active'); $('.properties-context-menu').fadeOut(200); } }); $('.properties-context-item').rebind('click', function() { $('.contact-list-icon').removeClass('active'); $('.properties-context-menu').fadeOut(200); loadSubPage('fm/' + $(this).data('handle')); return false; }); // Expands properties-context-menu so rest of contacts can be shown // By default only 5 contacts is shown $('.properties-context-item.show-more').rebind('click', function() { // $('.properties-context-menu').fadeOut(200); $('.properties-dialog .properties-context-item') .remove('.show-more') .removeClass('hidden');// un-hide rest of contacts var $cli = $('.contact-list-icon'); var position = $cli.position(); $('.properties-context-menu').css({ 'left': position.left + 16 + 'px', 'top': position.top - $('.properties-context-menu').outerHeight() - 8 + 'px', 'transform': 'translateX(-50%)', }); // $('.properties-context-menu').fadeIn(200); return false;// Prevent bubbling }); } $icon.text(''); if (filecnt + foldercnt === 1) { mCreateElement('i', { 'class': icons[0] }, $icon[0]); } else { if (filecnt + foldercnt === 2) { $dialog.addClass('two-elements'); } $('.properties-elements-counter span', $dialog).text(filecnt + foldercnt); var iconsTypes = []; for (var j = 0; j < icons.length; j++) { var ico = icons[j]; if (!iconsTypes.includes(ico)) { if (!ico.includes('folder')) { $dialog.removeClass('folders-only'); } iconsTypes.push(ico); } } if (icons.length === 2) { $dialog.addClass('two-elements'); } for (var k = 0; k < icons.length; k++) { if (filecnt && foldercnt || iconsTypes.length > 1) { mCreateElement('i', { 'class': filecnt ? 'generic' : 'folder' }, $icon[0]); } else { mCreateElement('i', { 'class': escapeHTML(iconsTypes[0]) }, $icon[0]); } if (k === 2) { break; } } } } /** * Open properties dialog for the selected node(s) * @param {Number|Boolean} [close] Whether it should be rather closed. * @returns {*|MegaPromise} */ global.propertiesDialog = function propertiesDialog(close) { if (close) { _propertiesDialog(close); } else { var shares = []; var nodes = ($.selected || []) .filter(function(h) { if (String(h).length === 11 && M.c[h]) { shares = shares.concat(Object.keys(M.c[h])); } else if (!M.getNodeByHandle(h)) { return true; } }); nodes = nodes.concat(shares); var promise = dbfetch.geta(nodes); promise.always(function() { _propertiesDialog(); }); return promise; } }; })(self); /** * bindDropdownEvents Bind custom select event * * @param {Selector} $select Class .dropdown elements selector * @param {String} saveOption Addition option for account page only. Allows to show "Show changes" notification * @param {String} classname/id of content block for dropdown aligment */ function bindDropdownEvents($select, saveOption, contentBlock) { 'use strict'; var $dropdownItem = $('.option', $select); var $contentBlock = contentBlock ? $(contentBlock) : $('body'); var $hiddenInput = $('.hidden-input', $select); // hidden input for keyboard search if (!$hiddenInput.length) { // Skip tab action for hidden input by tabindex="-1" $select.safeAppend('<input class="hidden-input" tabindex="-1" autocomplete="disabled">'); $hiddenInput = $('input.hidden-input', $select); } $select.rebind('click.inputDropdown', function(e) { var $this = $(this); var $dropdown = $('.mega-input-dropdown', $this); var $hiddenInput = $('.hidden-input', $this); var $target = $(e.target); var closeDropdown = function() { $this.removeClass('active'); $dropdown.addClass('hidden').removeAttr('style'); $contentBlock.unbind('mousedown.closeInputDropdown'); $hiddenInput.trigger('blur'); }; if ($this.hasClass('disabled')) { return false; } else if (!$this.hasClass('active')) { var horizontalOffset; var verticalOffset; var dropdownLeftPos; var dropdownBottPos; var dropdownHeight; var dropdownWidth; var contentBlockHeight; var contentBlockWidth; var $activeDropdownItem = $('.option[data-state="active"]', $dropdown); // Show select dropdown $('.mega-input.dropdown-input.active', 'body').removeClass('active'); $('.active .mega-input-dropdown', 'body').addClass('hidden'); $this.addClass('active'); $('.option.active', $this).removeClass('active'); $activeDropdownItem.addClass('active'); $dropdown.removeClass('hidden'); // For case select is located under overflow none element, to avoid it is hidden under overflow if ($this.closest('.ps').length) { $dropdown.css('min-width', $this.width() + 18); $dropdown.position({ of: $this, my: 'left-9 top-7', at: 'left top', collision: 'flipfit' }); } $hiddenInput.trigger('focus'); // Dropdown position relative to the window horizontalOffset = $dropdown.offset().left - $contentBlock.offset().left; verticalOffset = $dropdown.offset().top - $contentBlock.offset().top; contentBlockHeight = $contentBlock.outerHeight(); contentBlockWidth = $contentBlock.outerWidth(); dropdownHeight = $dropdown.outerHeight(); dropdownWidth = $dropdown.outerWidth(); dropdownBottPos = contentBlockHeight - (verticalOffset + dropdownHeight); dropdownLeftPos = contentBlockWidth - (horizontalOffset + dropdownWidth); if (contentBlockHeight < (dropdownHeight + 20)) { $dropdown.css({ 'margin-top': '-' + (verticalOffset - 10) + 'px', 'height': (contentBlockHeight - 20) + 'px' }); } else if (dropdownBottPos < 10) { $dropdown.css({ 'margin-top': '-' + (10 - dropdownBottPos) + 'px' }); } if (dropdownLeftPos < 20) { $dropdown.css({ 'margin-left': '-' + (10 - dropdownLeftPos) + 'px' }); } $contentBlock.rebind('mousedown.closeInputDropdown', e => { var $target = $(e.target); if (!$this.has($target).length && !$this.is($target)) { closeDropdown(); } }); var $scrollBlock = $('.dropdown-scroll', $this); // Dropdown scrolling initialization if ($scrollBlock.length) { if ($scrollBlock.is('.ps')) { $scrollBlock.scrollTop(0); Ps.destroy($scrollBlock[0]); } Ps.initialize($scrollBlock[0]); if ($activeDropdownItem.length) { $scrollBlock.scrollTop($activeDropdownItem.position().top); } } $hiddenInput.trigger('focus'); } else if (($target.closest('.option').length || $target.is('.option')) && !($target.hasClass('disabled') || $target.closest('.option').hasClass('disabled'))) { closeDropdown(); } }); $dropdownItem.rebind('click.inputDropdown', function() { var $this = $(this); $select.removeClass('error'); // Select dropdown item $('.option', $select).removeClass('active').removeAttr('data-state'); $this.addClass('active').attr('data-state', 'active'); $('> span', $select).text($this.text()); $('> span', $select).removeClass('placeholder'); $this.trigger('change'); if (saveOption) { var nameLen = String($('#account-firstname').val() || '').trim().length; // Save changes for account page if (nameLen) { $('.save-block', $this.closest('.settings-right-block')).removeClass('hidden'); } } }); $dropdownItem.rebind('mouseenter.inputDropdown', function() { var $this = $(this); // If contents width is bigger than size of dropdown if (this.offsetWidth < this.scrollWidth) { $this.addClass('simpletip').attr('data-simpletip', $this.text()); } }); // Typing search and arrow key up and down features for dropdowns $hiddenInput.rebind('keyup.inputDropdown', function(e) { var charCode = e.which || e.keyCode; // ff var $filteredItem = {}; if ((charCode > 64 && charCode < 91) || (charCode > 96 && charCode < 123)) { var inputValue = $hiddenInput.val(); $filteredItem = $dropdownItem.filter(function() { return $(this).text().slice(0, inputValue.length).toLowerCase() === inputValue.toLowerCase(); }).first(); } else { e.preventDefault(); e.stopPropagation(); const $activeOption = $('.option.active', $select); const $current = $activeOption.length ? $activeOption : $('.option:not(.template)', $select).first(); var $prev = $current.prev('.option:not(.template)'); var $next = $current.next('.option:not(.template)'); if (charCode === 38 && $prev.length) { // Up key $filteredItem = $prev; } else if (charCode === 40 && $next.length) { // Down key $filteredItem = $next; } else if (charCode === 13) {// Enter $current.trigger('click'); } } if ($filteredItem.length) { const $dropdownScroll = $('.dropdown-scroll', this); const $scrollBlock = $dropdownScroll.length ? $dropdownScroll : $('.dropdown-scroll', $(this).closest('.dropdown-input')); $('.option.active', $select).removeClass('active'); $filteredItem.addClass('active'); if ($scrollBlock.length) { $scrollBlock.scrollTop($scrollBlock.scrollTop() + $filteredItem.position().top); } } }); $hiddenInput.rebind('keydown.inputDropdown', function() { var $this = $(this); delay('dropbox:clearHidden', () => { // Combination language bug fixs for MacOS. $this.val('').trigger('blur').trigger('focus'); }, 750); }); // End of typing search for dropdowns } /** * addToMultiInputDropDownList * * Add item from token.input plugin drop down list. * * @param {String} dialog, The class name. * @param {Array} item An array of JSON objects e.g. { id, name }. * */ function addToMultiInputDropDownList(dialog, item) { 'use strict'; if (dialog) { $(dialog).tokenInput("addToDDL", item); } } /** * removeFromMultiInputDDL * * Remove item from token.input plugin drop down list. * * @param {String} dialog, The class name. * @param {Array} item An array of JSON objects e.g. { id, name }. * */ function removeFromMultiInputDDL(dialog, item) { 'use strict'; if (dialog) { $(dialog).tokenInput("removeFromDDL", item); } } /** * Set mega dropdown value * * @param {$} $container parent or element selctor * @param {string|function} selectedOptionCallback value or callback to get selected option element * * @returns {void} */ function setDropdownValue($container, selectedOptionCallback) { 'use strict'; const $megaInputDropdown = getDropdownMegaInput($container); if (!$megaInputDropdown || !$megaInputDropdown.length) { return; } const $dropdownInput = $('.mega-input-dropdown', $megaInputDropdown); const $dropdownInputOptions = $('.option', $dropdownInput); if (!$dropdownInputOptions.length) { return; } let $selectedOption = typeof selectedOptionCallback === 'function' ? selectedOptionCallback($dropdownInput, $dropdownInputOptions) : $(`.option[data-value="${selectedOptionCallback}"]`, $dropdownInput); const $dropdownSpanValueLabel = $('span:first', $megaInputDropdown); const $dropdownHiddenInput = $('.hidden-input', $megaInputDropdown); // Clear selected value; $dropdownInputOptions .removeClass('active') .removeAttr('data-state'); if (!$selectedOption.length) { $selectedOption = $dropdownInputOptions.first(); // If no match then select first by default? } if (!$selectedOption.length) { // Abnormal situation, dropdown may not be initialized return; } $selectedOption .addClass('active') .attr('data-state', 'active'); $dropdownSpanValueLabel.text($selectedOption.text()); $dropdownHiddenInput.trigger('focus'); } function getDropdownMegaInput($container) { 'use strict'; if (!$container || !$container.length) { return; } const inputDropdownClass = 'dropdown-input'; const megaInputClass = 'mega-input'; const dropdownClass = `.${megaInputClass}.${inputDropdownClass}`; let $megaInputDropdown = $container; if (!$megaInputDropdown.hasClass(megaInputClass) && !$megaInputDropdown.hasClass(inputDropdownClass)) { $megaInputDropdown = $container.closest(dropdownClass); } return $megaInputDropdown; } function createDropdown($container, options) { 'use strict'; const $megaInputDropdown = getDropdownMegaInput($container); if (!$megaInputDropdown || !$megaInputDropdown.length) { return; } const __prepareDropdownLabel = ($megaInputDropdown, options) => { const $dropdownTitle = $('.mega-input-title', $megaInputDropdown); const $dropdownInput = $('.mega-input-dropdown', $megaInputDropdown); let $dropdownSpanValueLabel = $('span:first', $megaInputDropdown); if (!$dropdownSpanValueLabel.length) { $dropdownSpanValueLabel = $('<span/>', {}); if ($dropdownTitle.length) { $dropdownSpanValueLabel.insertAfter($dropdownTitle); } else { $dropdownSpanValueLabel.insertBefore($dropdownInput); } } if (typeof options.placeholder === 'string') { $dropdownSpanValueLabel.text(options.placeholder); } }; const __prepareDropdownOptions = ($megaInputDropdown, $dropdownInput, options) => { const $dropdownInputScroll = $('.dropdown-scroll', $dropdownInput); if ($dropdownInputScroll.length) { $dropdownInputScroll.empty(); // clear list } const selectedOption = options.selected; const selectedOptionCallback = typeof selectedOption === 'function' ? selectedOption : null; const postActionCallback = typeof options.postAction === 'function' ? options.postAction : null; const optionList = options.items; if (typeof optionList !== 'object') { return; } let $selectedOption = null; for (const option in optionList) { if (!optionList.hasOwnProperty(option)) { continue; } const value = optionList[option]; const index = option; const attr = { 'data-value': index }; const $dropdownOptionItem = $( '<div/>', { class: 'option' } ); $dropdownOptionItem .attr(attr) .text(value); if (selectedOption) { const result = selectedOptionCallback ? selectedOptionCallback($dropdownOptionItem, value, index) : selectedOption === index; if (result) { $dropdownOptionItem .addClass('active') .attr('data-state', 'active'); $selectedOption = $dropdownOptionItem; } } if (postActionCallback) { postActionCallback($dropdownOptionItem, value, index, optionList); } $dropdownOptionItem.appendTo($dropdownInputScroll); } const $dropdownSpanValueLabel = $('span:first', $megaInputDropdown); const $dropdownHiddenInput = $('.hidden-input', $megaInputDropdown); if (!$selectedOption || !$selectedOption.length) { // Abnormal situation, dropdown may not be initialized return; } $dropdownSpanValueLabel.text($selectedOption.text()); $dropdownHiddenInput.trigger('focus'); }; __prepareDropdownLabel($megaInputDropdown, options); const $dropdownInput = $('.mega-input-dropdown', $megaInputDropdown); __prepareDropdownOptions($megaInputDropdown, $dropdownInput, options); } function getDropdownValue($container, attributeName) { 'use strict'; const defaultValue = ''; const $megaInputDropdown = getDropdownMegaInput($container); if (!$megaInputDropdown || !$megaInputDropdown.length) { return defaultValue; } const $dropdownInput = $('.mega-input-dropdown ', $megaInputDropdown); if (!$dropdownInput.length) { return defaultValue; } const $dropdownInputSelectedOption = $(`.option[data-state="active"]`, $dropdownInput); if (!$dropdownInputSelectedOption.length) { return defaultValue; } if (!attributeName) { return $dropdownInputSelectedOption.attr('data-value') || defaultValue; } return $dropdownInputSelectedOption.attr(`data-${attributeName}`) || $dropdownInputSelectedOption.attr('data-value') || defaultValue; } /** * Functionality for the Notifications popup * * 1) On page load, fetch the latest x number of notifications. If there are any new ones, these should show a * number e.g. (3) in the red circle to indicate there are new notifications. * 2) When they click the notifications icon, show the popup and whatever notifications the user has. * 3) On action packet receive, put the notification at the top of the queue and update the red circle to indicate a * new notification. Next time the popup opens this will show the new notification and old ones. */ var notify = { /** The current notifications **/ notifications: [], /** Number of notifications to fetch in the 'c=100' API request. This is reduced to 50 for fast rendering. */ numOfNotifications: 50, /** Locally cached emails and pending contact emails */ userEmails: Object.create(null), /** jQuery objects for faster lookup */ $popup: null, $popupIcon: null, $popupNum: null, /** A flag for if the initial loading of notifications is complete */ initialLoadComplete: false, /** A list of already rendered pending contact request IDs (multiple can exist with reminders) */ renderedContactRequests: [], /** Temp list of accepted contact requests */ acceptedContactRequests: [], /** * Initialise the notifications system */ init: function() { // Cache lookups notify.$popup = $('.js-notification-popup'); notify.$popupIcon = $('.top-head .top-icon.notification'); notify.$popupNum = $('.js-notification-num'); // Init event handler to open popup notify.initNotifyIconClickHandler(); // Recount the notifications and display red tooltip because they opened a new page within Mega notify.countAndShowNewNotifications(); }, /** * Get the most recent 100 notifications from the API */ getInitialNotifications: function() { // Clear notifications before fetching (sometimes this needs to be done if re-logging in) notify.notifications = []; // Call API to fetch the most recent notifications api_req('c=' + notify.numOfNotifications, { callback: function(result) { // Check it wasn't a negative number error response if (typeof result !== 'object') { return false; } // Get the current UNIX timestamp and the last time delta (the last time the user saw a notification) var currentTime = unixtime(); var lastTimeDelta = (result.ltd) ? result.ltd : 0; var notifications = result.c; var pendingContactUsers = result.u; // Add pending contact users notify.addUserEmails(pendingContactUsers); // Loop through the notifications if (notifications) { for (var i = 0; i < notifications.length; i++) { // Check that the user has enabled notifications of this type or skip it if (notify.isUnwantedNotification(notifications[i])) { continue; } var notification = notifications[i]; // The full notification object var id = makeid(10); // Make random ID var type = notification.t; // Type of notification e.g. share var timeDelta = notification.td; // Seconds since the notification occurred var seen = (timeDelta >= lastTimeDelta); // If the notification time delta is older than the last time the user saw the notification then it is read var timestamp = currentTime - timeDelta; // Timestamp of the notification var userHandle = notification.u; // User handle e.g. new share from this user if (!userHandle && notification.t === 'ipc') { // incoming pending contact userHandle = notification.p; } // Add notifications to list notify.notifications.push({ data: notification, // The full notification object id: id, seen: seen, timeDelta: timeDelta, timestamp: timestamp, type: type, userHandle: userHandle }); } } // After the first SC request all subsequent requests can generate notifications notify.initialLoadComplete = true; // Show the notifications notify.countAndShowNewNotifications(); // If the popup is already open (they opened it while the notifications were being fetched) then render // the notifications. If the popup is not open, then clicking the icon will render the notifications. if (!notify.$popup.hasClass('hidden')) { notify.renderNotifications(); } } }, 3); // Channel 3 }, /** * Adds a notification from an Action Packet * @param {Object} actionPacket The action packet object */ notifyFromActionPacket: function(actionPacket) { // We should not show notifications if we haven't yet done the initial notifications load yet if (!notify.initialLoadComplete || notify.isUnwantedNotification(actionPacket)) { return false; } // Construct the notification object var newNotification = { data: actionPacket, // The action packet id: makeid(10), // Make random ID seen: false, // New notification, so mark as unread timeDelta: 0, // Time since notification was sent timestamp: unixtime(), // Get the current timestamps in seconds type: actionPacket.a, // Type of notification e.g. share userHandle: actionPacket.u || actionPacket.ou // User handle e.g. new share from this user }; if (actionPacket.a === 'dshare' && actionPacket.orig && actionPacket.orig !== u_handle) { newNotification.userHandle = actionPacket.orig; } // If the user handle is not known to the local state we need to fetch the email from the API. This happens in // some sharing scenarios where a user is part of a share then another user adds files to the share but they // are not contacts with that other user so the local state has no information about them and would display a // broken notification if the email is not known. if ( newNotification.type === 'put' && !this.getUserEmailByTheirHandle(newNotification.userHandle, undefined, true) ) { console.assert(newNotification.userHandle && newNotification.userHandle.length === 11); // Once the email is fetched it will re-call the notifyFromActionPacket function with the same actionPacket notify.fetchUserEmailFromApi(newNotification.userHandle, actionPacket); return false; } // Combines the current new notification with the previous one if it meets certain criteria notify.combineNewNotificationWithPrevious(newNotification); // Show the new notification icon notify.countAndShowNewNotifications(); // If the popup is open, re-render the notifications to show the latest one if (!notify.$popup.hasClass('hidden')) { notify.renderNotifications(); } }, /** * Fetches the user's email address from the API based on the user handle * @param {String} userHandle The user handle to fetch the email address for e.g. 555wupYjkMU * @param {Object} actionPacket An action packet which will be resent to the notifyFromActionPacket function after * the user's email has been returned. An example 'put' action packet for testing is: * {"a":"put","n":"U8oHEL7Q","u":"555wupYjkMU","f":[{"h":"F5QQSDJR","t":0}]} */ fetchUserEmailFromApi: function(userHandle, actionPacket) { 'use strict'; // Make User Get Email (uge) request to get the user's email address from the user handle M.req({a: 'uge', 'u': userHandle}).done(function(result) { // Update the local state with the user's email notify.userEmails[userHandle] = result; // Re-call the notify function with the action packet now that the email has been stored notify.notifyFromActionPacket(actionPacket); }); }, /** * Check whether we should omit a notification. * @param {Object} notification * @returns {Boolean} */ isUnwantedNotification: function(notification) { var action; if (notification.dn) { return true; } switch (notification.a || notification.t) { case 'put': case 'share': case 'dshare': if (!mega.notif.has('cloud_enabled')) { return true; } break; case 'c': case 'ipc': case 'upci': case 'upco': if (!mega.notif.has('contacts_enabled')) { return true; } break; case 'd': if (notification.v) { return true; } break; } switch (notification.a || notification.t) { case 'put': if (!mega.notif.has('cloud_newfiles')) { return true; } break; case 'share': if (!mega.notif.has('cloud_newshare')) { return true; } break; case 'dshare': if (!mega.notif.has('cloud_delshare')) { return true; } break; case 'ipc': if (!mega.notif.has('contacts_fcrin')) { return true; } break; case 'c': action = (typeof notification.c !== 'undefined') ? notification.c : notification.u[0].c; if ((action === 0 && !mega.notif.has('contacts_fcrdel')) || (action === 1 && !mega.notif.has('contacts_fcracpt'))) { return true; } break; case 'upco': action = (typeof notification.s !== 'undefined') ? notification.s : notification.u[0].s; if (action === 2 && !mega.notif.has('contacts_fcracpt')) { return true; } default: break; } return false; }, /** * For incoming action packets, this combines the current new notification with the previous one if it meets * certain criteria. To be combined: * - There must be a previous notification to actually combine with * - It must be a new folder/file added to a share * - The user must be the same (same user handle) * - The previous notification must be less than 5 minutes old, and * - The new notification must be added to the same folder as the previous one. * An example put node: * {"a":"put","n":"U8oHEL7Q","u":"555wupYjkMU","f":[{"h":"F5QQSDJR","t":0}]} * @param {Object} currentNotification The current notification object */ combineNewNotificationWithPrevious: function(currentNotification) { 'use strict'; // If there are no previous notifications, nothing can be combined, // so add it to start of the list without modification and exit // Get the previous notification (list is already sorted by most recent at the top) var previousNotification = notify.notifications[0]; if (!previousNotification) { notify.notifications.unshift(currentNotification); return false; } // if prev+curr notifications are not from the same type if (currentNotification.type !== previousNotification.type) { notify.notifications.unshift(currentNotification); return false; } // we only for now combine "put" and "d" (del) if (currentNotification.type !== 'put' && currentNotification.type !== 'd') { notify.notifications.unshift(currentNotification); return false; } // If the current notification is not from the same user it cannot be combined // so add it to start of the list without modification and exit if (previousNotification.userHandle !== currentNotification.userHandle) { notify.notifications.unshift(currentNotification); return false; } // If time difference is older than 5 minutes it's a separate event and not worth combining, // so add it to start of the list without modification and exit if (previousNotification.timestamp - currentNotification.timestamp > 300) { notify.notifications.unshift(currentNotification); return false; } // Get details about the current notification var currentNotificationParentHandle = currentNotification.data.n; var currentNotificationNodes = currentNotification.data.f; // Get details about the previous notification var previousNotificationParentHandle = previousNotification.data.n; var previousNotificationNodes = previousNotification.data.f; if (currentNotification.type === 'put') { // If parent folders are not the same, they cannot be combined, so // add it to start of the list without modification and exit if (currentNotificationParentHandle !== previousNotificationParentHandle) { notify.notifications.unshift(currentNotification); return false; } // Combine the folder/file nodes from the current notification to the previous one var combinedNotificationNodes = previousNotificationNodes.concat(currentNotificationNodes); // Replace the current notification's nodes with the combined nodes currentNotification.data.f = combinedNotificationNodes; } else { // it's 'd' if (!Array.isArray(previousNotificationParentHandle)) { previousNotificationParentHandle = [previousNotificationParentHandle]; } var deletedCombinedNodes = previousNotificationParentHandle.concat(currentNotificationParentHandle); currentNotification.data.n = deletedCombinedNodes; } // Remove the previous notification and add the current notification with combined nodes from the previous notify.notifications.shift(); notify.notifications.unshift(currentNotification); }, /** * Counts the new notifications and shows the number of new notifications in a red circle */ countAndShowNewNotifications: function() { var newNotifications = 0; var $popup = $(notify.$popupNum); // Loop through the notifications for (var i = 0; i < notify.notifications.length; i++) { // If it hasn't been seen yet increment the count if (notify.notifications[i].seen === false) { // Don't count chat notifications until chat has loaded and can verify them. if (notify.notifications[i].type === 'mcsmp') { const { data } = notify.notifications[i]; if (megaChatIsReady) { const res = notify.getScheduledNotifOrReject(data); if ( res !== false && (res === 0 || !(res.mode === ScheduleMetaChange.MODE.CREATED && data.ou === u_handle)) ) { newNotifications++; } } } else { newNotifications++; } } } // If there is a new notification, show the red circle with the number of notifications in it if (newNotifications >= 1) { $popup.removeClass('hidden').text(newNotifications); $(document.body).trigger('onMegaNotification', newNotifications); } else { // Otherwise hide it $popup.addClass('hidden').text(newNotifications); $(document.body).trigger('onMegaNotification', false); } // Update page title megatitle(); }, /** * Marks all notifications so far as seen, this will hide the red circle * and also make sure on reload these notifications are not new anymore * If this is triggered by local, send `sla` request * * @param {Boolean} [remote] Optional. Show this function triggered by remote action packet. */ markAllNotificationsAsSeen: function(remote) { 'use strict'; // Loop through the notifications and mark them as seen (read) for (var i = 0; i < notify.notifications.length; i++) { notify.notifications[i].seen = true; } // Hide red circle with number of new notifications notify.$popupNum.addClass('hidden'); notify.$popupNum.html(0); // Update page title megatitle(); // Send 'set last acknowledged' API request to inform it which notifications have been seen // up to this point then they won't show these notifications as new next time they are fetched if (!remote) { api_req({ a: 'sla', i: requesti }); } }, /** * Open the notifications popup when clicking the notifications icon */ initNotifyIconClickHandler: function() { // Add delegated event for when the notifications icon is clicked $('.top-head').off('click', '.top-icon.notification'); $('.top-head').on('click', '.top-icon.notification', function() { // If the popup is already open, then close it if (!notify.$popup.hasClass('hidden')) { notify.closePopup(); } else { // Otherwise open the popup notify.renderNotifications(); } }); $('.js-topbarnotification').rebind('click', function() { let $elem = $(this).parent(); if ($elem.hasClass('show')) { notify.closePopup(); } else { $elem.addClass('show'); notify.renderNotifications(); } }); }, /** * Closes the popup. If the popup is currently open and a) the user clicks onto a new page within Mega or b) clicks * outside of the popup then this will mark the notifications as read. If the popup is not open, then functions * like $.hideTopMenu will try to hide any popups that may be open, but in this scenario we don't want to mark the * notifications as seen/read, we want the number of new notifications to remain in the red tooltip. */ closePopup: function() { 'use strict'; if (notify.$popup !== null && this.$popup.closest('.js-dropdown-notification').hasClass('show')) { this.$popup.closest('.js-dropdown-notification').removeClass('show'); notify.markAllNotificationsAsSeen(); } }, /** * Sort the notifications so the most recent ones appear first in the popup */ sortNotificationsByMostRecent: function() { notify.notifications.sort(function(notificationA, notificationB) { if (notificationA.timestamp > notificationB.timestamp) { return -1; } else if (notificationA.timestamp < notificationB.timestamp) { return 1; } else { return 0; } }); }, /** * Populates the user emails into a list which can be looked up later for incoming * notifications where there is no known contact handle e.g. pending shares/contacts * @param {Array} pendingContactUsers An array of objects (with user handle and email) for the pending contacts */ addUserEmails: function(pendingContactUsers) { 'use strict'; // Add the pending contact email addresses if (Array.isArray(pendingContactUsers)) { for (var i = pendingContactUsers.length; i--;) { var userHandle = pendingContactUsers[i].u; var userEmail = pendingContactUsers[i].m; notify.userEmails[userHandle] = userEmail; } } }, /** * Retrieve the email associated to a user by his/their handle or from the optional notification data * @param {String} userHandle the user handle to fetch the email for * @param {Object} [data] Optional the notification data * @param {boolean} [skipFetch] Optional to not use this function to sync the user data * @returns {string|false} The email if found or false if fetching data (or skipped by skipFetch) */ getUserEmailByTheirHandle: function(userHandle, data, skipFetch) { 'use strict'; if (typeof userHandle !== 'string' || userHandle.length !== 11) { return l[7381]; } if (typeof this.userEmails[userHandle] === 'string' && this.userEmails[userHandle]) { // Previously found and not an empty string. return this.userEmails[userHandle]; } const userEmail = M.getUserByHandle(userHandle).m; if (userEmail) { // Found in M.u return userEmail; } if (data && data.m) { // Found on notification data return data.m; } if (skipFetch || (this.userEmails[userHandle] instanceof Promise)) { return false; } // Fetch data from API M.setUser(userHandle); const promises = [ M.syncUsersFullname(userHandle, undefined, new MegaPromise()), M.syncContactEmail(userHandle, new MegaPromise(), true) ]; this.userEmails[userHandle] = Promise.allSettled(promises) .then(() => { this.userEmails[userHandle] = M.getUserByHandle(userHandle).m; }); return false; }, /** * To do: render the notifications in the popup */ renderNotifications: function() { 'use strict'; // Get the number of notifications var numOfNotifications = notify.notifications.length; var allNotificationsHtml = ''; // If no notifications, show empty if (notify.initialLoadComplete && numOfNotifications === 0) { notify.$popup.removeClass('loading'); notify.$popup.addClass('empty'); return false; } else if (!notify.initialLoadComplete) { return false; } // Sort the notifications notify.sortNotificationsByMostRecent(); // Reset rendered contact requests so the Accept button will show again notify.renderedContactRequests = []; // Cache the template selector var $template = this.$popup.find('.notification-item.template'); // Remove existing notifications and so they are re-rendered this.$popup.find('.notification-item:not(.template)').remove(); // Loop through all the notifications for (var i = 0; i < numOfNotifications; i++) { // Get the notification data and clone the notification template in /html/top.html var notification = notify.notifications[i]; var $notificationHtml = $template.clone(); // Update template $notificationHtml = notify.updateTemplate($notificationHtml, notification); // Skip this notification if it's not one that is recognised if ($notificationHtml === false) { continue; } // Build the html allNotificationsHtml += $notificationHtml.prop('outerHTML'); } // If all notifications are not recognised, show empty if (allNotificationsHtml === "") { notify.$popup.removeClass('loading'); notify.$popup.addClass('empty'); return false; } // Update the list of notifications notify.$popup.find('.notification-scr-list').append(allNotificationsHtml); notify.$popup.removeClass('empty loading'); // Add scrolling for the notifications Soon(() => { initPerfectScrollbar($('.notification-scroll', notify.$popup)); }); // Add click handlers for various notifications notify.initFullContactClickHandler(); notify.initShareClickHandler(); notify.initTakedownClickHandler(); notify.initPaymentClickHandler(); notify.initPaymentReminderClickHandler(); notify.initAcceptContactClickHandler(); notify.initSettingsClickHander(); notify.initScheduledClickHandler(); }, /** * When the other user has accepted the contact request and the 'Contact relationship established' notification * appears, make this is clickable so they can go to the contact's page to verify fingerprints or start chatting. */ initFullContactClickHandler: function() { // Add click handler for the 'Contact relationship established' notification this.$popup.find('.nt-contact-accepted').rebind('click', function() { // Redirect to the contact's page only if it's still a contact if (M.c.contacts && $(this).attr('data-contact-handle') in M.c.contacts) { loadSubPage('fm/chat/contacts/' + $(this).attr('data-contact-handle')); notify.closePopup(); } else { msgDialog('info', '', l[20427]); } }); clickURLs(); $('a.clickurl', this.$popup).rebind('click.notif', () => notify.closePopup()); }, /** * On click of a share or new files/folders notification, go to that share */ initShareClickHandler: function() { // Select the notifications with shares or new files/folders this.$popup.find('.notification-item.nt-incoming-share, .notification-item.nt-new-files').rebind('click', function() { // Get the folder ID from the HTML5 data attribute const $this = $(this); const folderId = $this.attr('data-folder-id'); const notificationID = $this.attr('id'); // Mark all notifications as seen and close the popup // (because they clicked on a notification within the popup) notify.closePopup(); // Open the folder M.openFolder(folderId) .always(() => { const notificationData = notify.notifications.find(elem => elem.id === notificationID); $.selected = notificationData.data.f && notificationData.data.f.map((elem) => elem.h); reselect(true); }); }); }, /** * On click of a takedown or restore notice, go to the parent folder */ initTakedownClickHandler: function() { // Select the notifications with shares or new files/folders this.$popup.find('.nt-takedown-notification, .nt-takedown-reinstated-notification').rebind('click', function() { // Get the folder ID from the HTML5 data attribute var folderOrFileId = $(this).attr('data-folder-or-file-id'); var parentFolderId = M.getNodeByHandle(folderOrFileId).p; // Mark all notifications as seen and close the popup // (because they clicked on a notification within the popup) notify.closePopup(); if (parentFolderId) { // Open the folder M.openFolder(parentFolderId) .always(function() { reselect(true); }); } }); }, /** * If they click on a payment notification, then redirect them to the Account History page */ initPaymentClickHandler: function() { // On payment notification click this.$popup.find('.notification-item.nt-payment-notification').rebind('click', function() { // Mark all notifications as seen (because they clicked on a notification within the popup) notify.closePopup(); var $target = $('.data-block.account-balance'); // Redirect to payment history loadSubPage('fm/account/plan'); mBroadcaster.once('settingPageReady', function () { const $scrollBlock = $('.fm-right-account-block.ps'); if ($scrollBlock.length) { $scrollBlock.scrollTop( $target.offset().top - $scrollBlock.offset().top + $scrollBlock.scrollTop() ); } }); }); }, /** * If they click on a payment reminder notification, then redirect them to the Pro page */ initPaymentReminderClickHandler: function() { // On payment reminder notification click this.$popup.find('.notification-item.nt-payment-reminder-notification').rebind('click', function() { // Mark all notifications as seen and close the popup // (because they clicked on a notification within the popup) notify.closePopup(); // Redirect to pro page loadSubPage('pro'); }); }, /** * If the click on Accept for a contact request, accept the contact */ initAcceptContactClickHandler: function() { // Add click handler to Accept button this.$popup.find('.notification-item .notifications-button.accept').rebind('click', function() { var $this = $(this); var pendingContactId = $this.attr('data-pending-contact-id'); // Send the User Pending Contact Action (upca) API 2.0 request to accept the request M.acceptPendingContactRequest(pendingContactId).fail(() => { notify.acceptedContactRequests.splice(notify.acceptedContactRequests.indexOf(pendingContactId), 1); }); // Show the Accepted icon and text $this.closest('.notification-item').addClass('accepted'); notify.acceptedContactRequests.push(pendingContactId); // Mark all notifications as seen and close the popup // (because they clicked on a notification within the popup) notify.closePopup(); }); }, /** * Load the notification settings page * @return {undefined} */ initSettingsClickHander: function() { 'use strict'; $('button.settings', this.$popup).rebind('click.notifications', () => { notify.closePopup(); loadSubPage('fm/account/notifications'); }); }, initScheduledClickHandler: () => { 'use strict'; $('.nt-schedule-meet', this.$popup).rebind('click.notifications', e => { const chatId = $(e.currentTarget).attr('data-chatid'); if (chatId) { notify.closePopup(); loadSubPage(`fm/chat/${chatId}`); if ($(e.currentTarget).attr('data-desc') === '1') { delay(`showSchedDescDialog-${chatId}`, () => { megaChat.chats[chatId].trigger('openSchedDescDialog'); }, 1500); } } }); }, /** * Main function to update each notification with relevant style and details * @param {Object} $notificationHtml The jQuery clone of the HTML notification template * @param {Object} notification The notification object * @returns {Object} */ updateTemplate: function($notificationHtml, notification) { // Remove the template class $notificationHtml.removeClass('template'); var date = time2last(notification.timestamp); var data = notification.data; var userHandle = notification.userHandle; var customIconNotifications = ['psts', 'pses', 'ph']; // Payment & Takedown notification types var userEmail = l[7381]; // Unknown var avatar = ''; // If a contact action packet if (typeof userHandle !== 'string') { if (Array.isArray(userHandle)) { userHandle = userHandle[0] || false; } if (typeof userHandle !== 'object' && data) { userHandle = Array.isArray(data.u) && data.u[0] || data; } userEmail = userHandle.m || userEmail; userHandle = userHandle.u || userHandle.ou; } // Use the email address in the notification/action packet if the contact doesn't exist locally // or if it was populated partially locally (i.e from chat, without email) // or if the notification is closed account notification, M.u cannot be exist, so just using attached email. if (userEmail === l[7381]) { let email = data && data.m; if (userHandle) { email = this.getUserEmailByTheirHandle(userHandle, data); if (!email) { return false; // Fetching user attributes... } } userEmail = email || userEmail; } // If the notification is not one of the custom ones, generate an avatar from the user information if (customIconNotifications.indexOf(notification.type) === -1) { // Generate avatar from the user handle which will load their profile pic if they are already a contact if (typeof M.u[userHandle] !== 'undefined') { avatar = useravatar.contact(userHandle); } // If it failed to generate an avatar from the user handle, or we haven't generated one yet use the email // address. With the new v2.0 API for pending contacts, the user handle will usually not be available as // they are not a full contact yet. if (avatar === '') { avatar = useravatar.contact(userEmail); } // Add the avatar HTML and show it $notificationHtml.find('.notification-avatar').removeClass('hidden').prepend(avatar); } else { // Hide the notification avatar code, the specific notification will render the icon $notificationHtml.find('.notification-icon').removeClass('hidden'); } // Get the user's name if we have it, otherwise use their email var displayNameOrEmail = notify.getDisplayName(userEmail); // Update common template variables $notificationHtml.attr('id', notification.id); $notificationHtml.find('.notification-date').text(date); $notificationHtml.find('.notification-username').text(displayNameOrEmail); // Add read status if (notification.seen) { $notificationHtml.addClass('read'); } // Populate other information based on each type of notification switch (notification.type) { case 'ipc': return notify.renderIncomingPendingContact($notificationHtml, notification); case 'c': return notify.renderContactChange($notificationHtml, notification); case 'upci': return notify.renderUpdatedPendingContactIncoming($notificationHtml, notification); case 'upco': return notify.renderUpdatedPendingContactOutgoing($notificationHtml, notification); case 'share': return notify.renderNewShare($notificationHtml, notification, userEmail); case 'd': return notify.renderRemovedSharedNode($notificationHtml, notification); case 'dshare': return notify.renderDeletedShare($notificationHtml, userEmail, notification); case 'put': return notify.renderNewSharedNodes($notificationHtml, notification, userEmail); case 'psts': return notify.renderPayment($notificationHtml, notification); case 'pses': return notify.renderPaymentReminder($notificationHtml, notification); case 'ph': return notify.renderTakedown($notificationHtml, notification); case 'mcsmp': { if (!window.megaChat || !window.megaChat.is_initialized) { return false; } return notify.renderScheduled($notificationHtml, notification); } default: return false; // If it's a notification type we do not recognise yet } }, /** * Render pending contact requests * @param {Object} $notificationHtml jQuery object of the notification template HTML * @param {Object} notification * @returns {Object} The HTML to be rendered for the notification */ renderIncomingPendingContact: function($notificationHtml, notification) { var pendingContactId = notification.data.p; var mostRecentNotification = true; let isAccepted = false; var className = ''; var title = ''; // Check if a newer contact request for this user has already been rendered (notifications are sorted by timestamp) for (var i = 0, length = notify.renderedContactRequests.length; i < length; i++) { // If this contact request has already been rendered, don't render the current notification with buttons if (pendingContactId === notify.renderedContactRequests[i]) { mostRecentNotification = false; } } if (notify.acceptedContactRequests.includes(pendingContactId)) { isAccepted = true; } // If this is the most recent contact request from this user if (mostRecentNotification && !isAccepted) { // If this IPC notification also exists in the state if (typeof M.ipc[pendingContactId] === 'object') { // Show the Accept button $notificationHtml.find('.notification-request-buttons').removeClass('hidden'); } // Set a flag so the buttons are not rendered again on older notifications notify.renderedContactRequests.push(pendingContactId); } // If the other user deleted their contact request to the current user if (typeof notification.data.dts !== 'undefined') { className = 'nt-contact-deleted'; title = l[7151]; // Cancelled their contact request } // If the other user sent a reminder about their contact request else if (typeof notification.data.rts !== 'undefined') { className = 'nt-contact-request'; title = l[7150]; // Reminder: you have a contact request } else { // Creates notification with 'Sent you a contact request' and 'Accept' button className = 'nt-contact-request'; title = l[5851]; } // Populate other template information $notificationHtml.addClass(className); if (notification.data && notification.data.m) { const user = M.getUserByEmail(notification.data.m); if (user && user.c === 1 && user.h) { $notificationHtml.addClass('clickurl').attr('href', `/fm/chat/contacts/${user.h}`); } else if (isAccepted) { // In-flight request so user.h is unknown. Send to contacts on click $notificationHtml.addClass('clickurl').attr('href', `/fm/chat/contacts`); } } $notificationHtml.find('.notification-info').text(title); $notificationHtml.find('.notifications-button.accept').attr('data-pending-contact-id', pendingContactId); return $notificationHtml; }, /** * Renders notifications related to contact changes * @param {Object} $notificationHtml jQuery object of the notification template HTML * @param {Object} notification * @returns {Object} The HTML to be rendered for the notification */ renderContactChange: function($notificationHtml, notification) { // Get data from initial c=50 notification fetch or action packet var action = (typeof notification.data.c !== 'undefined') ? notification.data.c : notification.data.u[0].c; var userHandle = (Array.isArray(notification.userHandle)) ? notification.data.ou || notification.userHandle[0].u : notification.userHandle; var className = ''; var title = ''; // If the user deleted the request if (action === 0) { className = 'nt-contact-deleted'; title = l[7146]; // Deleted you as a contact } else if (action === 1) { className = 'nt-contact-accepted'; title = l.notification_contact_accepted; // Accepted your contact request if (u_attr.b && !u_attr.b.m && u_attr.b.mu && u_attr.b.mu[0] === userHandle) { title = l.admin_sub_contacts; // your admin and you are now contacts. } // Add a data attribute for the click handler $notificationHtml.attr('data-contact-handle', userHandle); $notificationHtml.addClass('clickable'); } else if (action === 2) { className = 'nt-contact-deleted'; title = l[7144]; // Account has been deleted/deactivated } else if (action === 3) { className = 'nt-contact-request-blocked'; title = l[7143]; // Blocked you as a contact } // Populate other template information $notificationHtml.addClass(className); $notificationHtml.find('.notification-info').text(title); return $notificationHtml; }, /** * Renders Updated Pending Contact (Incoming) notifications * @param {Object} $notificationHtml jQuery object of the notification template HTML * @param {Object} notification * @returns {Object} The HTML to be rendered for the notification */ renderUpdatedPendingContactIncoming: function($notificationHtml, notification) { // The action 's' will only be available if initial fetch of notifications, 'u[0].s' is used if action packet var action = (typeof notification.data.s !== 'undefined') ? notification.data.s : notification.data.u[0].s; var className = ''; var title = ''; if (action === 1) { className = 'nt-contact-request-ignored'; title = l[7149]; // You ignored a contact request } else if (action === 2) { className = 'nt-contact-accepted'; title = l[7148]; // You accepted a contact request } else if (action === 3) { className = 'nt-contact-request-denied'; title = l[7147]; // You denied a contact request } // Populate other template information $notificationHtml.addClass(className); $notificationHtml.find('.notification-info').text(title); return $notificationHtml; }, /** * Renders Updated Pending Contact (Outgoing) notifications * @param {Object} $notificationHtml jQuery object of the notification template HTML * @param {Object} notification * @returns {Object} The HTML to be rendered for the notification */ renderUpdatedPendingContactOutgoing: function($notificationHtml, notification) { // The action 's' will only be available if initial fetch of notifications, 'u[0].s' is used if action packet var action = (typeof notification.data.s !== 'undefined') ? notification.data.s : notification.data.u[0].s; var className = ''; var title = ''; // Display message depending on action if (action === 2) { className = 'nt-contact-accepted'; title = l[5852]; // Accepted your contact request } else if (action === 3) { className = 'nt-contact-request-denied'; title = l[5853]; // Denied your contact request } // Populate other template information $notificationHtml.addClass(className); $notificationHtml.find('.notification-info').text(title); return $notificationHtml; }, /** * Render new share notification * @param {Object} $notificationHtml jQuery object of the notification template HTML * @param {Object} notification * @param {String} email The email address * @returns {Object} The HTML to be rendered for the notification */ renderNewShare: function($notificationHtml, notification, email) { var title = ''; var folderId = notification.data.n; // If the email exists use language string 'New shared folder from [X]' if (email) { title = l[824].replace('[X]', email); } else { // Otherwise use string 'New shared folder' title = l[825]; } // Populate other template information $notificationHtml.addClass('nt-incoming-share'); $notificationHtml.addClass('clickable'); $notificationHtml.find('.notification-info').text(title); $notificationHtml.attr('data-folder-id', folderId); return $notificationHtml; }, /** * Render removed share node notification * @param {Object} $notificationHtml jQuery object of the notification template HTML * @returns {Object} The HTML to be rendered for the notification */ renderRemovedSharedNode: function($notificationHtml, notification) { var itemsNumber = 0; var title = ''; if (Array.isArray(notification.data.n)) { itemsNumber = notification.data.n.length; } else { itemsNumber = 1; } title = mega.icu.format(l[8913], itemsNumber); // Populate other template information $notificationHtml.addClass('nt-revocation-of-incoming'); $notificationHtml.find('.notification-info').text(title); return $notificationHtml; }, /** * Render a deleted share notification * @param {Object} $notificationHtml jQuery object of the notification template HTML * @param {String} email The email address * @param {Object} notification notification object * @returns {Object} The HTML to be rendered for the notification */ renderDeletedShare: function ($notificationHtml, email, notification) { var title = ''; var notificationOwner; var notificationTarget; var notificationOrginating; // first we are parsing an action packet. if (notification.data.orig) { notificationOwner = notification.data.u; notificationTarget = notification.data.rece; notificationOrginating = notification.data.orig; } else { // otherwise we are parsing 'c' api response (initial notifications request) notificationOwner = notification.data.o; notificationOrginating = notification.data.u; if (notificationOwner === u_handle) { if (notificationOrginating === u_handle) { console.error('receiving a wrong notification, this notification shouldnt be sent to me', notification ); } else { notificationTarget = notificationOrginating; } } else { notificationTarget = u_handle; } // if we are dealing with old notification which doesnt support the new data if (!notificationOwner || notificationOwner === -1) { // fall back to the old not correct notification notificationOwner = notificationOrginating; } // receiving old action packet // .rece without .orig if (notification.data.rece) { notificationOwner = notificationOrginating; } } var sharingRemovedByReciver = notificationOrginating !== notificationOwner; if (!sharingRemovedByReciver) { // If the email exists use string 'Access to folders shared by [X] was removed' if (email) { title = l[7879].replace('[X]', email); } else { // Otherwise use string 'Access to folders was removed.' title = l[7880]; } } else { var folderName = M.getNameByHandle(notification.data.n) || ''; var removerEmail = notify.getUserEmailByTheirHandle(notificationTarget); if (removerEmail) { title = l[19153].replace('{0}', removerEmail).replace('{1}', folderName); } else { title = l[19154].replace('{0}', folderName); } } // Populate other template information $notificationHtml.addClass('nt-revocation-of-incoming'); $notificationHtml.find('.notification-info').text(title); return $notificationHtml; }, /** * Render a notification for when another user has added files/folders into an already shared folder. * This condenses all the files and folders that were shared into a single notification. * @param {Object} $notificationHtml jQuery object of the notification template HTML * @param {Object} notification * @param {String} email The email address * @returns {Object} The HTML to be rendered for the notification */ renderNewSharedNodes: function($notificationHtml, notification, email) { var nodes = notification.data.f; var fileCount = 0; var folderCount = 0; var folderId = notification.data.n; var notificationText = ''; var title = ''; // Count the number of new files and folders for (var node in nodes) { // Skip if not own property if (!nodes.hasOwnProperty(node)) { continue; } // If folder, increment if (nodes[node].t) { folderCount++; } else { // Otherwise is file fileCount++; } } // Get wording for the number of files and folders added const folderText = mega.icu.format(l.folder_count, folderCount); const fileText = mega.icu.format(l.file_count, fileCount); // Set wording of the title if (folderCount >= 1 && fileCount >= 1) { title = email ? mega.icu.format(l.user_item_added_count, folderCount + fileCount).replace('[X]', email) : mega.icu.format(l.item_added_count, folderCount + fileCount); } else if (folderCount > 0) { title = email ? l[836].replace('[X]', email).replace('[DATA]', folderText) : mega.icu.format(l.folder_added_count, folderCount); } else if (fileCount > 0) { title = email ? l[836].replace('[X]', email).replace('[DATA]', fileText) : mega.icu.format(l.file_added_count, fileCount); } // Populate other template information $notificationHtml.addClass('nt-new-files'); $notificationHtml.addClass('clickable'); $notificationHtml.find('.notification-info').text(title); $notificationHtml.attr('data-folder-id', folderId); return $notificationHtml; }, /** * Process payment notification sent from payment provider e.g. Bitcoin. * @param {Object} $notificationHtml jQuery object of the notification template HTML * @param {Object} notification The notification object * @returns {Object} The HTML to be rendered for the notification */ renderPayment: function($notificationHtml, notification) { var proLevel = notification.data.p; var proPlan = pro.getProPlanName(proLevel); var success = (notification.data.r === 's') ? true : false; var header = l[1230]; // Payment info var title = ''; // Change wording depending on success or failure if (success) { title = l[7142].replace('%1', proPlan); // Your payment for the PRO III plan was received. } else { title = l[7141].replace('%1', proPlan); // Your payment for the PRO II plan was unsuccessful. } // Populate other template information $notificationHtml.addClass('nt-payment-notification'); $notificationHtml.addClass('clickable'); $notificationHtml.find('.notification-info').text(title); $notificationHtml.find('.notification-username').text(header); // Use 'Payment info' instead of an email return $notificationHtml; }, /** * Process payment reminder notification to remind them their PRO plan is due for renewal. * Example PSES (Pro Status Expiring Soon) packet: {"a":"pses", "ts":expirestimestamp}. * @param {Object} $notificationHtml jQuery object of the notification template HTML * @param {Object} notification The notification object * @returns {Object|false} The HTML to be rendered for the notification */ renderPaymentReminder: function($notificationHtml, notification) { // Find the time difference between the current time and the plan expiry time var currentTimestamp = unixtime(); var expiringTimestamp = notification.data.ts; var secondsDifference = (expiringTimestamp - currentTimestamp); // If the notification is still in the future if (secondsDifference > 0) { // Calculate day/days remaining var days = Math.floor(secondsDifference / 86400); // PRO membership plan expiring soon // Your PRO membership plan will expire in 1 day/x days. var header = l[8598]; var title; if (days === 0) { title = l[25041]; } else { title = mega.icu.format(l[8597], days); } // Populate other template information $notificationHtml.addClass('nt-payment-reminder-notification clickable'); $notificationHtml.find('.notification-username').text(header); $notificationHtml.find('.notification-info').addClass('red').text(title); return $notificationHtml; } // Don't show any notification if the time has passed return false; }, /** * Processes a takedown notice or counter-notice to restore the file. * @param {Object} $notificationHtml jQuery object of the notification template HTML * @param {Object} notification The notification object * @returns {Object|false} The HTML to be rendered for the notification */ renderTakedown: function($notificationHtml, notification) { var header = ''; var title = ''; var cssClass = ''; var handle = notification.data.h; var node = M.d[handle] || {}; var name = (node.name) ? '(' + notify.shortenNodeName(node.name) + ')' : ''; // Takedown notice // Your publicly shared %1 (%2) has been taken down. if (notification.data.down === 1) { header = l[8521]; title = (node.t === 0) ? l.publicly_shared_file_taken_down.replace('%1', name) : l.publicly_shared_folder_taken_down.replace('%1', name); cssClass = 'nt-takedown-notification'; } // Takedown reinstated // Your taken down %1 (%2) has been reinstated. else if (notification.data.down === 0) { header = l[8524]; title = (node.t === 0) ? l.taken_down_file_reinstated.replace('%1', name) : l.taken_down_folder_reinstated.replace('%1', name); cssClass = 'nt-takedown-reinstated-notification'; } else { // Not applicable so don't return anything or it will show a blank notification return false; } // Populate other template information $notificationHtml.addClass(cssClass); $notificationHtml.addClass('clickable'); $notificationHtml.find('.notification-info').text(title); $notificationHtml.find('.notification-username').text(header); $notificationHtml.attr('data-folder-or-file-id', handle); return $notificationHtml; }, getScheduledNotifOrReject(data) { 'use strict'; const chatRoom = megaChat.chats[data.cid]; if (!chatRoom) { return false; } if (data && data.cs && Array.isArray(data.cs.c) && $.len(data.cs) === 1 && data.cs.c[1] === 0) { // Only change we see is that the meeting is no longer cancelled so ignore it. return false; } const meta = megaChat.plugins.meetingsManager.getFormattingMeta(data.id, data, chatRoom); if (meta.ap || meta.gone) { // If the ap flag is set the occurrence state is not known. // A future render should be able to get past this step when all occurrences are known // If the gone flag is set then the occurrence no longer exists so skip it. return meta.gone ? false : 0; } const { MODE } = ScheduleMetaChange; if (meta.occurrence && meta.mode === MODE.CANCELLED && !$.len(meta.timeRules)) { const meeting = megaChat.plugins.meetingsManager.getMeetingOrOccurrenceParent(meta.handle); if (!meeting) { return false; } const occurrences = meeting.getOccurrencesById(meta.handle); if (!occurrences) { return false; } meta.timeRules.startTime = Math.floor(occurrences[0].start / 1000); meta.timeRules.endTime = Math.floor(occurrences[0].end / 1000); } return meta; }, renderScheduled: function($notificationHtml, notification) { 'use strict'; const { data } = notification; const meta = this.getScheduledNotifOrReject(data); if (!meta) { return false; } const chatRoom = megaChat.chats[data.cid]; let now; let prev; const { MODE } = ScheduleMetaChange; if (meta.mode === MODE.CREATED && data.ou === u_handle) { return false; } if (meta.timeRules.startTime && meta.timeRules.endTime) { const prevMode = meta.mode; if (!meta.recurring && prevMode !== MODE.CANCELLED) { // Fake the mode for one-off meetings to get the correct time string. meta.mode = MODE.EDITED; } [now, prev] = megaChat.plugins.meetingsManager.getOccurrenceStrings(meta); meta.mode = prevMode; } const $notifBody = $('.notification-scheduled-body', $notificationHtml); $notifBody.removeClass('hidden'); const $notifLabel = $('.notification-content .notification-info', $notificationHtml).eq(0); const { NOTIF_TITLES } = megaChat.plugins.meetingsManager; let showTitle = true; const titleSelect = (core) => { const occurrenceKey = meta.occurrence ? 'occur' : 'all'; if (meta.mode === MODE.CREATED) { return core.inv; } else if (meta.mode === MODE.EDITED) { let string = ''; let diffCounter = 0; if ( meta.prevTiming && ( meta.timeRules.startTime !== meta.prevTiming.startTime || meta.timeRules.endTime !== meta.prevTiming.endTime || ( meta.recurring && !megaChat.plugins.meetingsManager.areMetaObjectsSame(meta.timeRules, meta.prevTiming) ) ) || (meta.occurrence && meta.mode !== MODE.CANCELLED) ) { string = core.time[occurrenceKey]; diffCounter++; } if (meta.topicChange) { string = core.name.update.replace('%1', meta.oldTopic).replace('%s', meta.topic); diffCounter++; showTitle = false; } if (meta.description) { string = core.desc.update; diffCounter++; $notificationHtml.attr('data-desc', diffCounter); } if (meta.converted) { string = core.convert; } else if (diffCounter > 1) { string = core.multi; now = false; prev = false; showTitle = true; } return string; } return core.cancel[occurrenceKey]; }; if (meta.mode === MODE.CREATED) { let email = this.getUserEmailByTheirHandle(data.ou); if (email) { const avatar = useravatar.contact(email); if (avatar) { const $avatar = $('.notification-avatar', $notificationHtml).removeClass('hidden'); $avatar.empty(); $avatar.safePrepend(avatar); } email = this.getDisplayName(email); $('.notification-username', $notificationHtml).text(email); } } $notifLabel.text(titleSelect(meta.recurring ? NOTIF_TITLES.recur : NOTIF_TITLES.once)); const $title = $('.notification-scheduled-title', $notifBody); if (showTitle) { $title.text(chatRoom.topic).removeClass('hidden'); } const $prev = $('.notification-scheduled-prev', $notifBody); const $new = $('.notification-scheduled-occurrence', $notifBody); if (prev && !(meta.occurrence && meta.mode === MODE.CANCELLED)) { $prev.removeClass('hidden'); $('s', $prev).text(prev); } if (now) { $new.removeClass('hidden'); $new.text(now); } $notificationHtml.addClass('nt-schedule-meet').attr('data-chatid', chatRoom.chatId); return $notificationHtml; }, /** * Truncates long file or folder names to 30 characters * @param {String} name The file or folder name * @returns {String} Returns a string similar to 'reallylongfilename...' */ shortenNodeName: function(name) { if (name.length > 30) { name = name.substr(0, 30) + '...'; } return htmlentities(name); }, /** * Gets a display name for the notification. If available it will use the user or contact's name. * If the name is unavailable (e.g. a new contact request) then it will use the email address. * @param {String} email The email address e.g. ed@fredom.press * @returns {String} Returns the name and email as a string e.g. "Ed Snowden (ed@fredom.press)" or just the email */ getDisplayName: function(email) { // Use the email by default var displayName = email; // Search through contacts for the email address if (M && M.u) { M.u.forEach(function(contact) { var contactEmail = contact.m; var contactHandle = contact.u; // If the email is found if (contactEmail === email) { // If the nickname is available use: Nickname if (M.u[contactHandle].nickname !== '') { displayName = nicknames.getNickname(contactHandle); } else { // Otherwise use: FirstName LastName (Email) displayName = (M.u[contactHandle].firstName + ' ' + M.u[contactHandle].lastName).trim() + ' (' + email + ')'; } // Exit foreach loop return true; } }); } // Escape and return return displayName; } }; (function(window) { /** * Functions from Underscore.js 1.4.4 * http://underscorejs.org * (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. * Underscore may be freely distributed under the MIT license. */ var _ = { bind: function _Bind(ctx) { return (function(){}).bind.apply(ctx, [].slice.call(arguments, 1)); }, bindAll: function _bindAll(obj) { [].slice.call(arguments, 1).forEach(function(f) { obj[f] = _.bind(obj[f], obj); }); }, each: function _each(obj, iterator, context) { obj.forEach(iterator, context); }, filter: function _filter(obj, iterator, context) { return [].slice.call(obj).filter(iterator, context); }, first: function _first(array, n, guard) { if (array == null) { return void 0; } return (n != null) && !guard ? array.slice(0, n) : array[0]; }, has: function _has(obj, prop) { return obj.hasOwnProperty(prop); }, isFunction: function _isFunction(func) { return typeof func === 'function'; }, isRegExp: function _isRegExp(obj) { return Object.prototype.toString.call(obj) === '[object RegExp]'; } }; /** * Avatar Picker * https://bitbucket.org/atlassianlabs/avatar-picker/src * A combination of the JS source files required for the avatar picker to work. * Built with command: * cat canvas-cropper.js <(echo) client-file-handler.js <(echo) client-file-reader.js <(echo) drag-drop-file-target.js <(echo) upload-interceptor.js <(echo) image-explorer.js <(echo) image-upload-and-crop.js > avatar-picker.js */ window.CanvasCropper = (function(){ function CanvasCropper(width, height){ if (!CanvasCropper.isSupported()) { throw new Error("This browser doesn't support CanvasCropper."); } return this.init.apply(this, arguments); } var supportsCanvas = (function() { var canvas = document.createElement('canvas'); return (typeof canvas.getContext === 'function') && canvas.getContext('2d'); }()); CanvasCropper.isSupported = function() { return supportsCanvas; }; CanvasCropper.prototype.defaults = { outputFormat: 'image/jpeg', backgroundFillColor: undefined }; CanvasCropper.prototype.init = function(width, height, opts) { this.width = width; this.height = height || width; //Allow single param for square crop this.options = $.extend({}, this.defaults, opts); this.canvas = $('<canvas/>') .attr('width', this.width) .attr('height', this.height) [0]; return this; }; CanvasCropper.prototype.cropToDataURI = function(image, sourceX, sourceY, cropWidth, cropHeight) { return this .crop(image, sourceX, sourceY, cropWidth, cropHeight) .getDataURI(this.options.outputFormat); }; CanvasCropper.prototype.crop = function(image, sourceX, sourceY, cropWidth, cropHeight) { var context = this.canvas.getContext('2d'), targetX = 0, targetY = 0, targetWidth = this.width, targetHeight = this.height; context.clearRect(targetX, targetY, targetWidth, targetHeight); if (this.options.backgroundFillColor) { context.fillStyle = this.options.backgroundFillColor; context.fillRect(targetX, targetY, targetWidth, targetHeight); } /* *** Negative sourceX or sourceY *** context.drawImage can't accept negative values for source co-ordinates, but what you probably meant is you want to do something like the below |-------------------| | | | CROP AREA | | | | |----------|----------------| | | | | | | | IMAGE | | | | | |-------------------| | | | | | | | | | |---------------------------| We need to do a couple of things to make that work. 1. Set the target position to the proportional location of the source position 2. Set source co-ordinates to 0 */ if (sourceX < 0) { targetX = Math.round((Math.abs(sourceX) / cropWidth) * targetWidth); sourceX = 0; } if (sourceY < 0) { targetY = Math.round((Math.abs(sourceY) / cropHeight) * targetHeight); sourceY = 0; } /* *** source co-ordinate + cropSize > image size *** context.drawImage can't accept a source co-ordinate and a crop size where their sum is greater than the image size. Again, below is probably what you wanted to achieve. |---------------------------| | | | IMAGE | | | | | | |-----------|-------| | | | | | | X | | | | | | |---------------|-----------| | | | | CROP AREA | | | |-------------------| We need to do a couple of things to make that work also. 1. Work out the size of the actual image area to be cropped (X). 2. Get the proportional size of the target based on the above 3. Set the crop size to the actual crop size. */ if (sourceX + cropWidth > image.naturalWidth) { var newCropWidth = image.naturalWidth - sourceX; targetWidth *= newCropWidth / cropWidth; cropWidth = newCropWidth; } if (sourceY + cropHeight > image.naturalHeight) { var newCropHeight = image.naturalHeight - sourceY; targetHeight *= newCropHeight / cropHeight; cropHeight = newCropHeight; } context.drawImage( image, sourceX, sourceY, cropWidth, cropHeight, targetX, targetY, targetWidth, targetHeight ); return this; }; CanvasCropper.prototype.getDataURI = function(outputFormat) { if (outputFormat) { //TODO: Check if in array of valid mime types return this.canvas.toDataURL(outputFormat, 0.75); } else { return null; } }; return CanvasCropper; })(); window.ClientFileHandler = (function(){ function ClientFileHandler(opts){ return this.init(opts); } ClientFileHandler.typeFilters = { all: /.*/, application: /^application\/.*/, audio: /^audio\/.*/, image: /^image\/.*/, imageWeb: /^image\/(jpeg|png|gif)$/, text: /^text\/.*/, video: /^video\/.*/ }; ClientFileHandler.prototype.defaults = { fileTypeFilter: ClientFileHandler.typeFilters.all, //specify a regex or use one of the built in typeFilters fileCountLimit: Infinity, //How many files can a user upload at once? This will limit it to the first n files, fileSizeLimit: 20 * 1024 * 1024, //Maximum file size in bytes (20MB per file), onSuccess: $.noop, onError: $.noop }; ClientFileHandler.prototype.init = function(opts){ this.options = $.extend({}, this.defaults, opts); if (opts && !opts.fileSizeLimit) { this.options.fileSizeLimit = this.defaults.fileSizeLimit; } if (opts && !opts.fileCountLimit) { this.options.fileCountLimit = this.defaults.fileCountLimit; } _.bindAll(this, 'handleFiles', 'filterFiles'); return this; }; /** * Takes in an array of files, processes them, and fires the onSuccess handler if any are valid, or the onError handler * otherwise. These handlers can be specified on the options object passed to the constructor. * @param fileList array of objects like { size:Number, type:String } * @param fileSourceElem - Unused. Matches IframeUploader interface\ * @param event - event to check user drop a folder */ ClientFileHandler.prototype.handleFiles = function(fileList, fileSourceElem, event){ //Assumes any number of files > 0 is a success, else it's an error var filteredFiles = this.filterFiles(fileList, event); if (filteredFiles.valid.length > 0) { //There was at least one valid file _.isFunction(this.options.onSuccess) && this.options.onSuccess(filteredFiles.valid); } else { //there were no valid files added _.isFunction(this.options.onError) && this.options.onError(filteredFiles.invalid); } }; ClientFileHandler.prototype.filterFiles = function(fileList, event){ var fileTypeFilter = _.isRegExp(this.options.fileTypeFilter) ? this.options.fileTypeFilter : this.defaults.fileTypeFilter, fileSizeLimit = this.options.fileSizeLimit, invalid = { byType: [], bySize: [], byCount: [] }, valid = _.filter(fileList, function(file){ if (M.checkFolderDrop(event)) { invalid.byType.push(file); return false; } if (!fileTypeFilter.test(file.type)) { invalid.byType.push(file); return false; } if (file.size > fileSizeLimit) { invalid.bySize.push(file); return false; } return true; }); if (valid.length > this.options.fileCountLimit) { invalid.byCount = valid.slice(this.options.fileCountLimit); valid = valid.slice(0, this.options.fileCountLimit); } return { valid: valid, invalid: invalid }; }; return ClientFileHandler; })(); window.ClientFileReader = (function(){ var fileReaderSupport = !!(window.File && window.FileList && window.FileReader); var _readMethodMap = { ArrayBuffer : 'readAsArrayBuffer', BinaryString: 'readAsBinaryString', DataURL : 'readAsDataURL', Text : 'readAsText' }; function ClientFileReader(opts){ if (!ClientFileReader.isSupported()) { throw new Error("ClientFileReader requires FileReaderAPI support"); } return this.init(opts); } ClientFileReader.isSupported = function() { return fileReaderSupport; }; $.extend(ClientFileReader.prototype, ClientFileHandler.prototype); ClientFileReader.readMethods = { ArrayBuffer : 'ArrayBuffer', BinaryString: 'BinaryString', DataURL : 'DataURL', Text : 'Text' }; ClientFileReader.typeFilters = ClientFileHandler.typeFilters; //Expose this to the calling code ClientFileReader.prototype.defaults = $.extend({}, ClientFileHandler.prototype.defaults, { readMethod: ClientFileReader.readMethods.DataURL, onRead: $.noop }); ClientFileReader.prototype.init = function(opts) { _.bindAll(this, 'onSuccess', 'readFile'); ClientFileHandler.prototype.init.call(this, opts); this.options.onSuccess = this.onSuccess; //We don't want this to be optional. return this; }; ClientFileReader.prototype.onSuccess = function(files) { var readMethod = _.has(_readMethodMap, this.options.readMethod) ? _readMethodMap[this.options.readMethod] : undefined; if (readMethod) { _.each(files, _.bind(function(file){ var fileReader = new FileReader(); fileReader.onload = _.bind(this.readFile, this, file); //pass the file handle to allow callback access to filename, size, etc. fileReader[readMethod](file); }, this)); } }; ClientFileReader.prototype.readFile = function(file, fileReaderEvent){ _.isFunction(this.options.onRead) && this.options.onRead(fileReaderEvent.target.result, file); }; return ClientFileReader; })(); window.DragDropFileTarget = (function(){ function DragDropFileTarget(el, opts){ return this.init.apply(this, arguments); } DragDropFileTarget.prototype.getDefaults = function() { return { activeDropTargetClass: 'active-drop-target', uploadPrompt: 'Drag a file here to upload', clientFileHandler: null }; }; DragDropFileTarget.prototype.init = function(el, opts){ _.bindAll(this, 'onDragOver', 'onDragEnd', 'onDrop'); this.$target = $(el); this.options = $.extend({}, this.getDefaults(), opts); this.$target.attr('data-upload-prompt', this.options.uploadPrompt); //bind drag & drop events this.$target.on('dragover', this.onDragOver); this.$target.on('dragleave', this.onDragEnd); this.$target.on('dragend', this.onDragEnd); this.$target.on('drop', this.onDrop); }; DragDropFileTarget.prototype.onDragOver = function(e){ e.preventDefault(); this.$target.addClass(this.options.activeDropTargetClass); }; DragDropFileTarget.prototype.onDragEnd = function(e){ e.preventDefault(); this.$target.removeClass(this.options.activeDropTargetClass); }; DragDropFileTarget.prototype.onDrop = function(e){ e.preventDefault(); e.originalEvent.preventDefault(); this.$target.removeClass(this.options.activeDropTargetClass); if (this.options.clientFileHandler) { this.options.clientFileHandler.handleFiles(e.originalEvent.dataTransfer.files, e.originalEvent.target, e); } }; return DragDropFileTarget; })(); window.UploadInterceptor = (function(){ function UploadInterceptor(el, opts){ return this.init.apply(this, arguments); } UploadInterceptor.prototype.defaults = { replacementEl: undefined, clientFileHandler: null }; UploadInterceptor.prototype.init = function(el, opts) { _.bindAll(this, 'onSelectFile', 'onReplacementClick'); this.$el = $(el); this.options = $.extend({}, this.defaults, opts); this.$el.on('change', this.onSelectFile); if (this.options.replacementEl) { this.$replacement = $(this.options.replacementEl); this.$el.hide(); // IE marks a file input as compromised if has a click triggered programmatically // and this prevents you from later submitting it's form via Javascript. // The work around is to use a label as the replacementEl with the `for` set to the file input, // but it requires that the click handler below not be bound. So regardless of whether you want // to use the workaround or not, the handler should not be bound in IE. if ($.browser && $.browser.msie) { if (!this.$replacement.is('label')) { // Workaround is not being used, fallback to showing the regular file element and hide the replacement this.$replacement.hide(); this.$el.show(); } } else { this.$replacement.on('click', this.onReplacementClick); } } }; UploadInterceptor.prototype.onSelectFile = function(e){ if ($(e.target).val() && this.options.clientFileHandler) { this.options.clientFileHandler.handleFiles(e.target.files, this.$el, e); } }; UploadInterceptor.prototype.onReplacementClick = function(e){ e.preventDefault(); this.$el.click(); }; UploadInterceptor.prototype.destroy = function(){ this.$el.off('change', this.onSelectFile); this.$replacement.off('click', this.onReplacementClick); }; return UploadInterceptor; })(); window.ImageExplorer = (function(){ function ImageExplorer($container, opts){ this.init.apply(this, arguments); } ImageExplorer.scaleModes = { fill: 'fill', contain: 'contain', containAndFill: 'containAndFill' }; ImageExplorer.zoomModes = { localZoom: 'localZoom', //Keep the area under the mask centered so you zoom further in on the same location. imageZoom: 'imageZoom' //Keep the image centered in its current location, so unless the image is centered under the mask, the area under the mask will change. }; ImageExplorer.prototype.defaults = { initialScaleMode: ImageExplorer.scaleModes.fill, zoomMode: ImageExplorer.zoomModes.localZoom, emptyClass: 'empty', scaleMax: 1 //Maximum image size is 100% (is overridden by whatever the initial scale is calculated to be) }; ImageExplorer.prototype.init = function($container, opts){ this.$container = $container; this.$imageView = this.$container.find('.image-explorer-image-view'); this.$sourceImage = this.$container.find('.image-explorer-source'); this.$mask = this.$container.find('.image-explorer-mask'); this.$dragDelegate = this.$container.find('.image-explorer-drag-delegate'); this.$scaleSlider = this.$container.find('.zoom-slider'); this.$zoomOutButton = this.$container.find('.zoom-out'); this.$zoomInButton = this.$container.find('.zoom-in'); this.options = $.extend({}, this.defaults, opts); this.imageProperties = {}; _.bindAll(this, 'getImageSrc', 'setImageSrc', 'initImage', 'initDragDelegate', 'initScaleSlider', 'setInitialScale', 'getFillScale', 'getContainedScale', 'getCircularContainedScale', 'sliderValToScale', 'scaleToSliderVal', 'updateImageScale', 'resetImagePosition', 'resetScaleSlider', 'toggleEmpty', 'get$ImageView', 'get$SourceImage', 'get$Mask', 'get$DragDelegate', 'getMaskedImageProperties', 'showError', 'clearError', 'hasValidImage', '_resetFromError', '_removeError', 'initZoomSlider'); this.toggleEmpty(true); //assume the explorer is empty initially and override below if otherwise if (this.$sourceImage[0].naturalWidth) { //The image has already loaded (most likely because the src was specified in the html), //so remove the empty class and call initImage passing through a fake event object with the target this.toggleEmpty(false); this.initImage({ target:this.$sourceImage[0] }); } this.$sourceImage.on('load', this.initImage); this.initScaleSlider(); this.initDragDelegate(); }; ImageExplorer.prototype.getImageSrc = function(){ return (this.$sourceImage) ? this.$sourceImage.attr('src') : undefined; }; ImageExplorer.prototype.setImageSrc = function(src){ if (this.$sourceImage) { this.$sourceImage.attr('src', '').attr('src', src); //Force image to reset if the user uploads the same image } }; ImageExplorer.prototype.initImage = function(e){ var image = e.target; this.imageProperties.naturalWidth = image.naturalWidth; this.imageProperties.naturalHeight = image.naturalHeight; this._removeError(); this.toggleEmpty(false); this.setInitialScale(); }; ImageExplorer.prototype.initDragDelegate = function(){ var imageOffset; this.$dragDelegate.draggable({ start: _.bind(function(){ imageOffset = this.$sourceImage.offset(); }, this), drag: _.bind(function(e, ui){ this.$sourceImage.offset({ top: imageOffset.top + ui.position.top - ui.originalPosition.top, left: imageOffset.left + ui.position.left - ui.originalPosition.left }); }, this) }); }; ImageExplorer.prototype.initZoomSlider = function(value = 0) { const container = document.querySelector('.avatar-dialog'); const wrapper = container && container.querySelector('.zoom-slider-wrap'); const $elm = $('.zoom-slider', wrapper); const setValue = tryCatch(() => { wrapper.dataset.perc = value; $elm.slider('value', value); }); if (!wrapper) { if (d) { console.error('zoom-slider-wrap not found.'); } return; } if (wrapper.dataset.perc) { // Update existing slider. return setValue(); } // Init zoom slider $elm.slider({ min: 0, max: 100, range: 'min', step: 1, change: function(e, ui) { $(this).attr('title', `${ui.value}%`); wrapper.dataset.perc = ui.value; }, slide: _.bind(function(e, ui) { $(this).attr('title', `${ui.value}%`); this.updateImageScale(this.sliderValToScale(ui.value)); },this), create: setValue }); } ImageExplorer.prototype.initScaleSlider = function() { this.initZoomSlider(0); this.$zoomOutButton.on('click', _.bind(function() { this.$scaleSlider.slider('value', parseInt(this.$scaleSlider.slider('value')) - 10); this.updateImageScale(this.sliderValToScale(this.$scaleSlider.slider('value'))); }, this)); this.$zoomInButton.on('click', _.bind(function() { this.$scaleSlider.slider('value', parseInt(this.$scaleSlider.slider('value')) + 10); this.updateImageScale(this.sliderValToScale(this.$scaleSlider.slider('value'))); }, this)); }; ImageExplorer.prototype.setInitialScale = function(){ var maskWidth = this.$mask.width(), maskHeight =this.$mask.height(), naturalWidth = this.imageProperties.naturalWidth, naturalHeight = this.imageProperties.naturalHeight, initialScale = 1; this.minScale = 1; switch(this.options.initialScaleMode) { case ImageExplorer.scaleModes.fill: //sets the scale of the image to the smallest size possible that completely fills the mask. this.minScale = initialScale = this.getFillScale(naturalWidth, naturalHeight, maskWidth, maskHeight); break; case ImageExplorer.scaleModes.contain: //Sets the scale of the image so that the entire image is visible inside the mask. if (this.$mask.hasClass('circle-mask')) { this.minScale = initialScale = this.getCircularContainedScale(naturalWidth, naturalHeight, maskWidth / 2); } else { this.minScale = initialScale = this.getContainedScale(naturalWidth, naturalHeight, maskWidth, maskHeight); } break; case ImageExplorer.scaleModes.containAndFill: //Set the min scale so that the lower bound is the same as scaleModes.contain, but the initial scale is scaleModes.fill if (this.$mask.hasClass('circle-mask')) { this.minScale = this.getCircularContainedScale(naturalWidth, naturalHeight, maskWidth / 2); } else { this.minScale = this.getContainedScale(naturalWidth, naturalHeight, maskWidth, maskHeight); } initialScale = this.getFillScale(naturalWidth, naturalHeight, maskWidth, maskHeight); break; } this.maxScale = Math.max(initialScale, this.options.scaleMax); this.resetScaleSlider(); //Always use ImageExplorer.zoomModes.imageZoom when setting the initial scale to center the image. this.updateImageScale(initialScale, ImageExplorer.zoomModes.imageZoom); this.resetImagePosition(); }; ImageExplorer.prototype.getFillScale = function(imageWidth, imageHeight, constraintWidth, constraintHeight){ var widthRatio = constraintWidth / imageWidth, heightRatio = constraintHeight / imageHeight; return Math.max(widthRatio, heightRatio); }; ImageExplorer.prototype.getContainedScale = function(imageWidth, imageHeight, constraintWidth, constraintHeight){ var widthRatio = constraintWidth / imageWidth, heightRatio = constraintHeight / imageHeight; return Math.min(widthRatio, heightRatio); }; ImageExplorer.prototype.getCircularContainedScale = function(imageWidth, imageHeight, constraintRadius){ var theta = Math.atan(imageHeight / imageWidth), scaledWidth = Math.cos(theta) * constraintRadius * 2; //Math.cos(theta) * constraintRadius gives the width from the centre of the circle to one edge so we need to double it. return scaledWidth / imageWidth; }; ImageExplorer.prototype.sliderValToScale = function(sliderValue) { var sliderValAsUnitInterval = sliderValue / (this.$scaleSlider.slider('option', 'max') - this.$scaleSlider.slider('option', 'min')); //http://math.stackexchange.com/questions/2489/is-there-a-name-for-0-1 (was tempted to use sliderValAsWombatNumber) return this.minScale + (sliderValAsUnitInterval * (this.maxScale - this.minScale)); }; ImageExplorer.prototype.scaleToSliderVal = function(scale) { //Slider represents the range between maxScale and minScale, normalised as a percent (the HTML slider range is 0-100). var sliderValAsUnitInterval = (scale - this.minScale) / (this.maxScale - this.minScale); return sliderValAsUnitInterval * (this.$scaleSlider.slider('option', 'max') - this.$scaleSlider.slider('option', 'min')); }; ImageExplorer.prototype.updateImageScale = function(newScale, zoomMode){ var newWidth = Math.round(newScale * this.imageProperties.naturalWidth) + 7, newHeight = Math.round(newScale * this.imageProperties.naturalHeight) + 7, newMarginLeft, newMarginTop; zoomMode = zoomMode || this.options.zoomMode; switch (zoomMode) { case ImageExplorer.zoomModes.imageZoom: newMarginLeft = -1 * newWidth / 2; newMarginTop = -1 * newHeight / 2; break; case ImageExplorer.zoomModes.localZoom: var oldWidth = this.$sourceImage.width(), oldHeight = this.$sourceImage.height(), oldMarginLeft = parseInt(this.$sourceImage.css('margin-left'), 10), oldMarginTop = parseInt(this.$sourceImage.css('margin-top'), 10), sourceImagePosition = this.$sourceImage.position(), //Position top & left only. Doesn't take into account margins imageViewCenterX = this.$imageView.width() / 2, imageViewCenterY = this.$imageView.height() / 2, //Which pixel is currently in the center of the mask? (assumes the mask is centered in the $imageView) oldImageFocusX = imageViewCenterX - sourceImagePosition.left - oldMarginLeft, oldImageFocusY = imageViewCenterY - sourceImagePosition.top - oldMarginTop, //Where will that pixel be once the image is resized? newImageFocusX = (oldImageFocusX / oldWidth) * newWidth, newImageFocusY = (oldImageFocusY / oldHeight) * newHeight; //How many pixels do we need to shift the image to put the new focused pixel in the center of the mask? newMarginLeft = imageViewCenterX - sourceImagePosition.left - newImageFocusX; newMarginTop = imageViewCenterY - sourceImagePosition.top - newImageFocusY; break; } this.$sourceImage.add(this.$dragDelegate) .width(newWidth) .height(newHeight) .css({ 'margin-left': Math.round(newMarginLeft) +'px', 'margin-top': Math.round(newMarginTop) +'px' }); var x1 = this.$mask.offset().left + this.$mask.width() - newMarginLeft - newWidth + 4; var y1 = this.$mask.offset().top + this.$mask.height() - newMarginTop - newHeight + 4; var x2 = this.$mask.offset().left - newMarginLeft - 4; var y2 = this.$mask.offset().top - newMarginTop - 4; this.$dragDelegate.draggable('option', 'containment', [x1, y1, x2, y2]); }; ImageExplorer.prototype.resetImagePosition = function(){ this.$sourceImage.add(this.$dragDelegate).css({ top: '50%', left: '50%' }); }; ImageExplorer.prototype.resetScaleSlider = function(){ this.$scaleSlider.slider('value', 0) .removeClass('disabled') .removeAttr('disabled'); }; ImageExplorer.prototype.toggleEmpty = function(toggle) { this.$container.toggleClass(this.options.emptyClass, toggle); }; ImageExplorer.prototype.get$ImageView = function(){ return this.$imageView; }; ImageExplorer.prototype.get$SourceImage = function(){ return this.$sourceImage; }; ImageExplorer.prototype.get$Mask = function(){ return this.$mask; }; ImageExplorer.prototype.get$DragDelegate = function(){ return this.$dragDelegate; }; ImageExplorer.prototype.getMaskedImageProperties = function(){ var currentScaleX = this.$sourceImage.width() / this.imageProperties.naturalWidth, currentScaleY = this.$sourceImage.height() / this.imageProperties.naturalHeight, maskPosition = this.$mask.position(), imagePosition = this.$sourceImage.position(); maskPosition.top += parseInt(this.$mask.css('margin-top'), 10); maskPosition.left += parseInt(this.$mask.css('margin-left'), 10); imagePosition.top += parseInt(this.$sourceImage.css('margin-top'), 10); imagePosition.left += parseInt(this.$sourceImage.css('margin-left'), 10); return { maskedAreaImageX : Math.round((maskPosition.left - imagePosition.left) / currentScaleX), maskedAreaImageY : Math.round((maskPosition.top - imagePosition.top) / currentScaleY), maskedAreaWidth : Math.round(this.$mask.width() / currentScaleX), maskedAreaHeight : Math.round(this.$mask.height() / currentScaleY) }; }; ImageExplorer.prototype.showError = function(title, contents) { this._removeError(); this.toggleEmpty(true); alert(title + ' ' + contents); }; ImageExplorer.prototype.clearError = function() { this._removeError(); this._resetFromError(); }; ImageExplorer.prototype.hasValidImage = function(){ return !!(this.getImageSrc() && this.$sourceImage.prop('naturalWidth')); }; ImageExplorer.prototype._resetFromError = function(){ // When the error is closed/removed, if there was a valid img in the explorer, show that, // otherwise keep displaying the 'empty' view // Might also need to do something in the caller (e.g. ImageUploadAndCrop) so fire an optional callback. var hasValidImage = this.hasValidImage(); this.toggleEmpty(!hasValidImage); this.$container.removeClass('error'); _.isFunction(this.options.onErrorReset) && this.options.onErrorReset(hasValidImage ? this.getImageSrc() : undefined); }; ImageExplorer.prototype._removeError = function(){ this.$imageView.find('.aui-message.error').remove(); }; return ImageExplorer; })(); window.ImageUploadAndCrop = (function(){ function ImageUploadAndCrop($container, opts){ if (!ImageUploadAndCrop.isSupported()) { throw new Error("This browser doesn't support ImageUploadAndCrop."); } this.init.apply(this, arguments); } ImageUploadAndCrop.isSupported = function() { return CanvasCropper.isSupported(); }; ImageUploadAndCrop.prototype.defaults = { HiDPIMultiplier: 2, //The canvas crop size is multiplied by this to support HiDPI screens dragDropUploadPrompt: l[1390], onImageUpload: $.noop, onImageUploadError: $.noop, onCrop: $.noop, outputFormat: 'image/png', fallbackUploadOptions: {}, initialScaleMode: ImageExplorer.scaleModes.fill, scaleMax: 1, fileSizeLimit: 15 * 1024 * 1024, //5MB maxImageDimension: 5000 //In pixels }; ImageUploadAndCrop.prototype.init = function($container, opts){ this.options = $.extend({}, this.defaults, opts); this.$container = $container; _.bindAll(this, 'crop', 'resetState', '_onFileProcessed', 'setImageSrc', 'validateImageResolution', '_onFilesError', '_onFileError', '_resetFileUploadField', '_onErrorReset'); this.imageExplorer = new ImageExplorer(this.$container.find('.image-explorer-container'), { initialScaleMode: this.options.initialScaleMode, scaleMax: this.options.scaleMax, onErrorReset: this._onErrorReset }); if (ClientFileReader.isSupported()) { this.clientFileReader = new ClientFileReader({ readMethod: 'ArrayBuffer', onRead: this._onFileProcessed, onError: this._onFilesError, fileTypeFilter: ClientFileReader.typeFilters.imageWeb, fileCountLimit: 1, fileSizeLimit: this.options.fileSizeLimit }); //drag drop uploading is only possible in browsers that support the fileReaderAPI this.dragDropFileTarget = new DragDropFileTarget(this.imageExplorer.get$ImageView(), { uploadPrompt: this.options.dragDropUploadPrompt, clientFileHandler: this.clientFileReader }); } else { //Fallback for older browsers. TODO: Client side filetype filtering? this.$container.addClass("filereader-unsupported"); var fallbackOptions = $.extend({ onUpload: this._onFileProcessed, onError: this._onFileError }, this.options.fallbackUploadOptions); this.clientFileReader = new ClientFileIframeUploader(fallbackOptions); } this.uploadIntercepter = new UploadInterceptor(this.$container.find('.image-upload-field'), { replacementEl: this.$container.find('.image-upload-field-replacement'), clientFileHandler: this.clientFileReader }); var mask = this.imageExplorer.get$Mask(); this.canvasCroppper = new CanvasCropper( 250, 250, //mask.width() * this.options.HiDPIMultiplier, //mask.height() * this.options.HiDPIMultiplier, { outputFormat: this.options.outputFormat } ); this.options.cropButton && $(this.options.cropButton).click(this.crop); }; ImageUploadAndCrop.prototype.crop = function(){ var cropProperties = this.imageExplorer.getMaskedImageProperties(), croppedDataURI = this.canvasCroppper.cropToDataURI( this.imageExplorer.get$SourceImage()[0], cropProperties.maskedAreaImageX, cropProperties.maskedAreaImageY, cropProperties.maskedAreaWidth, cropProperties.maskedAreaHeight ); _.isFunction(this.options.onCrop) && this.options.onCrop(croppedDataURI); }; ImageUploadAndCrop.prototype.resetState = function(){ this.imageExplorer.clearError(); this._resetFileUploadField(); }; ImageUploadAndCrop.prototype._onFileProcessed = function(imageData) { if (!imageData || !imageData.byteLength) { return this._onFileProcessed2(imageData); } queueMicrotask(async() => { this.options.maxImageDimension = NaN; this._onFileProcessed2(URL.createObjectURL(await webgl.getIntrinsicImage(imageData, 2160))); }); }; ImageUploadAndCrop.prototype._onFileProcessed2 = function(imageSrc){ if (imageSrc){ if (!isNaN(this.options.maxImageDimension)) { var validatePromise = this.validateImageResolution(imageSrc); validatePromise .done(_.bind(function(imageWidth, imageHeight){ this.setImageSrc(imageSrc); }, this)) .fail(_.bind(function(imageWidth, imageHeight){ this._onFileError('The selected image size is ' + imageWidth + 'px * ' + imageHeight + 'px. The maximum allowed image size is ' + this.options.maxImageDimension + 'px * ' + this.options.maxImageDimension + 'px'); }, this)); } else { // If imageResolutionMax isn't valid, skip the validation and just set the image src. this.setImageSrc(imageSrc); } } else { this._onFileError(); } }; ImageUploadAndCrop.prototype.setImageSrc = function(imageSrc) { this.imageExplorer.setImageSrc(imageSrc); _.isFunction(this.options.onImageUpload) && this.options.onImageUpload(imageSrc); this._resetFileUploadField(); }; ImageUploadAndCrop.prototype.validateImageResolution = function(imageSrc){ var validatePromise = $.Deferred(), tmpImage = new Image(), self = this; tmpImage.onload = function(){ if (this.naturalWidth > self.options.maxImageDimension || this.naturalHeight > self.options.maxImageDimension) { validatePromise.reject(this.naturalWidth, this.naturalHeight); } else { validatePromise.resolve(this.naturalWidth, this.naturalHeight); } }; tmpImage.src = imageSrc; return validatePromise; }; ImageUploadAndCrop.prototype._onFilesError = function(invalidFiles) { // Work out the most appropriate error to display. Because drag and drop uploading can accept multiple files and we can't restrict this, // it's not an all or nothing situation, we need to try and find the most correct file and base the error on that. // If there was at least 1 valid file, then this wouldn't be called, so we don't need to worry about files rejected because of the fileCountLimit if (invalidFiles && invalidFiles.bySize && invalidFiles.bySize.length){ //Some image files of the correct type were filtered because they were too big. Pick the first one to use as an example. var file = _.first(invalidFiles.bySize); this._onFileError('File "' + str_mtrunc(file.name, 50) + '" is ' + bytesToSize(file.size) + ' which is larger than the maximum allowed size of ' + bytesToSize(this.options.fileSizeLimit)); } else { //No files of the correct type were uploaded. The default error message will cover this. this._onFileError(); } }; ImageUploadAndCrop.prototype._onFileError = function(error){ var title = 'There was an error uploading your image', contents = error || 'Please check that your file is a valid image and try again.'; this.imageExplorer.showError(title, contents); this._resetFileUploadField(); _.isFunction(this.options.onImageUploadError) && this.options.onImageUploadError(error); }; ImageUploadAndCrop.prototype._resetFileUploadField = function(){ //clear out the fileUpload field so the user could select the same file again to "reset" the imageExplorer var form = this.$container.find("#image-upload-and-crop-upload-field").prop('form'); form && form.reset(); }; ImageUploadAndCrop.prototype._onErrorReset = function(imgSrc){ //If we have a valid image after resetting from the error, notify the calling code. if (imgSrc) { _.isFunction(this.options.onImageUpload) && this.options.onImageUpload(imgSrc); } }; return ImageUploadAndCrop; })(); })(this); // Note: Referral Program is called as affiliate program at begining, so all systemic names are under word affiliate // i.e. affiliate === referral function affiliateUI() { 'use strict'; // Prevent ephemeral session to access if (u_type === 0) { msgDialog('confirmation', l[998], l[17146] + ' ' + l[999], l[1000], function(e) { if (e) { loadSubPage('register'); return false; } loadSubPage('fm'); }); return false; } $('.fm-right-files-block, .section.conversations, .fm-right-block.dashboard').addClass('hidden'); $('.nw-fm-left-icon').removeClass('active'); $('.nw-fm-left-icon.affiliate').addClass('active').removeClass('animate'); M.onSectionUIOpen('affiliate'); loadingDialog.show('affiliateRefresh'); M.affiliate.getAffiliateData().catch(function() { if (d) { console.error('Pulling affiliate data failed due to one of it\'s operation failed.'); } msgDialog('warninga', '', l[200] + ' ' + l[253], '', function() { loadingDialog.hide('affiliateRefresh'); }); }).then(function() { onIdle(clickURLs); M.affiliate.lastupdate = Date.now(); loadingDialog.hide('affiliateRefresh'); affiliateUI.startRender(); }); affiliateUI.$body = $('.fm-affiliate.body'); $('.breadcrumbs .item.active', affiliateUI.$body).rebind('click.breadcrumbs', function() { loadSubPage('/fm/dashboard'); }); // Init Referral content scrolling var $scrollBlock = $('.scroll-block', affiliateUI.$body); if ($scrollBlock.is('.ps')) { Ps.update($scrollBlock[0]); } else { Ps.initialize($scrollBlock[0]); } } /* * Dialogs start */ /** * Affiliate guide dialog */ affiliateUI.guideDialog = { // Init event on affiliate dashboard to open dialog. init: function() { 'use strict'; var self = this; $('.guide-dialog', affiliateUI.$body).rebind('click.guide-dialog', function() { affiliateUI.guideDialog.show(); if (this.classList.contains('to-rules')) { onIdle(function() { self.$firstStepBlock.removeClass('active'); self.$secondStepBlock.addClass('active'); self.showAffiliateSlide(3); }); } }); }, // Show dialog show: function() { 'use strict'; var self = this; this.$dialog = $('.mega-dialog.affiliate-guide'); this.$firstStepBlock = $('.step1', this.$dialog); this.$secondStepBlock = $('.step2', this.$dialog); this.slidesLength = $('.affiliate-guide-content', this.$secondStepBlock).length; this.bindDialogEvents(); // Reset dialog contents this.$firstStepBlock.addClass('active'); this.$secondStepBlock.removeClass('active'); this.showAffiliateSlide(); M.safeShowDialog('affiliate-guide-dialog', self.$dialog); }, // Dialog event binding bindDialogEvents: function() { 'use strict'; var self = this; // Step1. Welcome dialog, How it works button click - > show step 2. $('button.how-it-works', this.$dialog).rebind('click.guide-dialog-hiw-btn', function() { self.$firstStepBlock.removeClass('active'); self.$secondStepBlock.addClass('active'); }); // Step 2. Back/Next buttons $('.bottom-button', this.$dialog).rebind('click.btns', function() { var currentSlide = $('.nav-button.active', self.$dialog).data('slide'); if ($(this).hasClass('next') && currentSlide + 1 <= self.slidesLength) { self.showAffiliateSlide(currentSlide + 1); } else if ($(this).hasClass('back') && currentSlide - 1 >= 0) { self.showAffiliateSlide(currentSlide - 1); } }); $('button.dashboard', this.$secondStepBlock).rebind('click.to-aff-page', function() { loadSubPage('fm/refer'); }); // Step 2.Top nav buttons $('.nav-button', this.$dialog).rebind('click.top-nav', function() { self.showAffiliateSlide($(this).attr('data-slide')); }); // Closing dialog related $('button.js-close', this.$dialog).rebind('click.close-dialog', function() { closeDialog(); $('.fm-dialog-overlay').off('click.affGuideDialog'); }); $('.fm-dialog-overlay').rebind('click.affGuideDialog', function() { $('.fm-dialog-overlay').off('click.affGuideDialog'); }); }, // Step 2. Show Slides. showAffiliateSlide: function(num) { 'use strict'; num = num | 0 || 1; $('.affiliate-guide-content.active', this.$secondStepBlock).removeClass('active'); $('.affiliate-guide-content.slide' + num, this.$secondStepBlock).addClass('active'); // Show/hide Back button if (num === 1) { $('.bottom-button.back', this.$secondStepBlock).addClass('hidden'); } else { $('.bottom-button.back', this.$secondStepBlock).removeClass('hidden'); } // Slide 3 requires scrollpane if (num === 3) { var $scrollBlock = $('.affiliate-guide-content.slide3', this.$secondStepBlock); if ($scrollBlock.is('.ps')) { Ps.update($scrollBlock[0]); } else { Ps.initialize($scrollBlock[0]); } $('footer', this.$dialog).addClass('has-divider'); } else { $('footer', this.$dialog).removeClass('has-divider'); } // Show/hide Affiliate Dashboard button/Next button if (num === this.slidesLength) { $('.bottom-button.next', this.$secondStepBlock).addClass('hidden'); if (page === 'fm/refer') { $('button.dashboard', this.$secondStepBlock).addClass('hidden'); } else { $('button.dashboard', this.$secondStepBlock).removeClass('hidden'); } } else { $('.mega-button.positive', this.$secondStepBlock).addClass('hidden'); $('.bottom-button.next', this.$secondStepBlock).removeClass('hidden'); } // Change top buttons state $('.nav-button.active', this.$secondStepBlock).removeClass('active'); $('.nav-button.slide' + num, this.$secondStepBlock).addClass('active'); } }; /** * Affiliate referral url generation dialog */ affiliateUI.referralUrlDialog = { // Show dialog show: function() { 'use strict'; var self = this; this.$dialog = $('.mega-dialog.generate-url'); this.bindDialogEvents(); $('.page-names span[data-page="start"]', this.$dialog).click(); var showWelcome = !M.affiliate.id; this.updateURL().then(function() { M.safeShowDialog('referral-url-dialog', self.$dialog); if (showWelcome) { affiliateUI.registeredDialog.show(1); } }); }, // Bind dialog dom event bindDialogEvents: function() { 'use strict'; var self = this; var $urlBlock = $('.url', this.$dialog); $urlBlock.rebind('click.select', function() { var sel, range; var el = $(this)[0]; if (window.getSelection && document.createRange) { sel = window.getSelection(); if (sel.toString() === ''){ window.setTimeout(function(){ range = document.createRange(); range.selectNodeContents(el); sel.removeAllRanges(); sel.addRange(range); },1); } } }); var $copyBtn = $('.copy-button', this.$dialog); if (is_extension || M.execCommandUsable()) { $copyBtn.removeClass('hidden'); $copyBtn.rebind('click.copy-to-clipboard', function() { var links = $.trim($urlBlock.text()); var toastTxt = l[7654]; copyToClipboard(links, toastTxt); }); } else { $copyBtn.addClass('hidden'); } var $pageBtns = $('.page-names span', this.$dialog); $pageBtns.rebind('click.select-target-page', function() { var $this = $(this); $pageBtns.removeClass('active'); $this.addClass('active'); if ($this.data('page') === 'more') { self.updateURL(1); $('.custom-block', self.$dialog).removeClass('hidden'); } else { self.updateURL(); $('.custom-block', self.$dialog).addClass('hidden'); } }); $('.url-input', this.$dialog).rebind('keyup.enter-custom-url', function(e) { if (e.keyCode === 13) { self.checkAndSetCustomURL(); } }); $('.custom-button', this.$dialog).rebind('click.enter-custom-url', function() { self.checkAndSetCustomURL(); }); $('button.js-close', this.$dialog).rebind('click.close-dialog', function() { closeDialog(); }); }, /** * Check manually entered url by user is valid to generate custom url. * If url is not valid (non mega url, etc) show error. * @returns {Boolean|undefined} False if value is empty, or undefined as it act as void function */ checkAndSetCustomURL: function() { 'use strict'; var val = $('.url-input', this.$dialog).val(); var baseUrl = getBaseUrl(); var baseUrlRegExp = new RegExp(baseUrl, 'ig'); if (!val) { return false; } else if (val.search(baseUrlRegExp) === 0) { this.customTargetPage = val.replace(baseUrlRegExp, ''); this.updateURL(); } else if (('https://' + val).search(baseUrlRegExp) === 0) { this.customTargetPage = val.replace(baseUrl.replace('https://', ''), ''); this.updateURL(); } else { $('.custom-block', this.$dialog).addClass('error'); } }, /** * Update dom input element with url generated from checkAndSetCustomURL function * @param {Boolean} clear Clear the input field * @returns {Promise} Promise that resolve once process is done. */ updateURL: function(clear) { 'use strict'; var targetPage = $('.page-names .active', this.$dialog).data('page'); var $urlBlock = $('.url', this.$dialog); $('.custom-block', this.$dialog).removeClass('error'); if (clear) { $urlBlock.empty(); $('.url-input', this.$dialog).val(''); return Promise.resolve(); } if (targetPage === 'start') { targetPage = ''; } else if (targetPage === 'more' && this.customTargetPage !== undefined) { targetPage = this.customTargetPage; if (targetPage.charAt(0) === '/') { targetPage = targetPage.slice(1); } if (targetPage.charAt(targetPage.length - 1) === '/') { targetPage = targetPage.slice(0, -1); } } return M.affiliate.getURL(targetPage).then(function(url) { var urlWithoutAfftag = getBaseUrl() + (targetPage === '' ? '' : '/' + targetPage); $urlBlock.safeHTML(url.replace(urlWithoutAfftag, '<span>' + urlWithoutAfftag + '</span>')); }); }, }; /** * Affiliate welcome(registered) dialog */ affiliateUI.registeredDialog = { // Show dialog show: function(skipReq) { 'use strict'; if (u_type > 2 && mega.flags.refpr && !pfid) { var self = this; this.$dialog = $('.mega-dialog.joined-to-affiliate'); var _showRegisteredDialog = function() { self.bindDialogEvents(); M.safeShowDialog('joined-to-affiliate', self.$dialog); }; if (M.currentdirid === 'refer') { $('.how-it-works span', this.$dialog).text(l[81]); $('.cancel-button', this.$dialog).addClass('hidden'); } // After referal url dialog. if (skipReq) { _showRegisteredDialog(); } // User never see this dialog before. else if (!M.affiliate.id) { M.affiliate.getID().then(function() { _showRegisteredDialog(); }); } } }, // Bind dialog dom event bindDialogEvents: function() { 'use strict'; $('.how-it-works', this.$dialog).rebind('click.to-aff-page', function() { closeDialog(); affiliateUI.guideDialog.show(); }); $('button.js-close, .cancel-button', this.$dialog).rebind('click.close-dialog', function() { closeDialog(); }); } }; /* * Dialogs End */ /* * Dashboard Start */ /* * Start affiliate dashboard rendering. Required Chart.js */ affiliateUI.startRender = function() { 'use strict'; loadingDialog.show('affiliateUI'); affiliateUI.referUsers.init(); affiliateUI.commissionIndex.init(); affiliateUI.redemptionHistory.init(); affiliateUI.geographicDistribution.init(); affiliateUI.guideDialog.init(); M.require('charts_js', 'charthelper_js').done(function() { affiliateUI.registrationIndex.init(); affiliateUI.purchaseIndex.init(); loadingDialog.hide('affiliateUI'); }); }; /* * Refer users section */ affiliateUI.referUsers = { init: function() { 'use strict'; this.bindEvents(); }, bindEvents: function() { 'use strict'; $('.refer-block', affiliateUI.$body).rebind('click.affRefClick', function() { affiliateUI.referUsers.handleClick($(this).data('reftype')); }); $('.refer-users', affiliateUI.$body).rebind('click.affRefClick', function() { $('.scroll-block', affiliateUI.$body).animate({scrollTop: 0}, 500); }); }, /* * Click event handling for refer user feature. * also used for product page. * @param {String} reftype Button clicked by user. */ handleClick: function(reftype) { 'use strict'; switch (reftype) { case 'url': // show URL dialog affiliateUI.referralUrlDialog.show(); break; case 'link': M.safeShowDialog('create-new-link', function() { M.initFileAndFolderSelectDialog('create-new-link'); }); break; case 'chatlink': M.safeShowDialog('create-new-chat-link', function() { M.initNewChatlinkDialog(); }); break; case 'invite': $.hideContextMenu(); if (M.isInvalidUserStatus()) { return; } contactAddDialog(false, true); setContactLink(); break; } } }; /* * Commission index section */ affiliateUI.commissionIndex = { init: function() { 'use strict'; this.$block = $('.mega-data-box.commission', affiliateUI.$body); this.calculateCommission(); this.bindEvents(); }, calculateCommission: function() { 'use strict'; var balance = M.affiliate.balance; var currencyHtml = ''; var localTotal; var localPending; var localAvailable; if (balance.localCurrency === 'EUR') { $('.non-euro-only', this.$block).addClass('hidden'); $('.euro-price', this.$block).addClass('hidden'); localTotal = formatCurrency(balance.localTotal); localPending = formatCurrency(balance.localPending); localAvailable = formatCurrency(balance.localAvailable); } else { localTotal = formatCurrency(balance.localTotal, balance.localCurrency, 'number'); localPending = formatCurrency(balance.localPending, balance.localCurrency, 'number'); localAvailable = formatCurrency(balance.localAvailable, balance.localCurrency, 'number'); currencyHtml = ' <span class="currency">' + balance.localCurrency + '</span>'; $('.commission-block.total .euro-price', this.$block) .text(formatCurrency(balance.pending + balance.available)); $('.commission-block.pending .euro-price', this.$block).text(formatCurrency(balance.pending)); $('.commission-block.available .euro-price', this.$block).text(formatCurrency(balance.available)); } $('.commission-block.total .price', this.$block).safeHTML(localTotal + currencyHtml); $('.commission-block.pending .price', this.$block).safeHTML(localPending + currencyHtml); currencyHtml += balance.localCurrency === 'EUR' ? '' : '<sup>3</sup>'; $('.commission-block.available .price', this.$block).safeHTML(localAvailable + currencyHtml); if (u_attr.b || u_attr.pf) { $('.no-buisness', this.$block).addClass('hidden'); } // Redeem requires at least one payment history and available balance more than 50 euro if (M.affiliate.redeemable && balance.available >= 50 && M.affiliate.utpCount) { $('button.redeem', this.$block).removeClass('disabled'); } else { $('button.redeem', this.$block).addClass('disabled'); } }, bindEvents: function() { 'use strict'; $('button.redeem' ,this.$block).rebind('click.openRedemptionDialog', function() { if ($(this).hasClass('disabled')) { return false; } affiliateUI.redemptionDialog.show(); }); } }; affiliateUI.redemptionDialog = { show: function() { 'use strict'; var self = this; var balance = M.affiliate.balance; this.rdm = M.affiliate.redemption; this.$dialog = $('.mega-dialog.affiliate-redeem'); // Reset dialog and info this.reset(); M.affiliate.getRedemptionMethods().then(function() { self.displaySteps(); const euro = formatCurrency(balance.available); const $availableComissionTemplates = $('.templates .available-comission-template', self.$dialog); const $euroTemplate = $('.available-commission-euro', $availableComissionTemplates) .clone() .removeClass('hidden'); const $localTemplate = $('.available-commission-local', $availableComissionTemplates) .clone() .removeClass('hidden'); const $availableComissionArea = $('.available-comission-quota span', self.$dialog).empty(); const $availableBitcoinArea = $('.available-comission-bitcoin span', self.$dialog).empty(); $euroTemplate.text(euro); if (balance.localCurrency && balance.localCurrency !== 'EUR') { const local = balance.localCurrency + ' ' + formatCurrency(balance.localAvailable, balance.localCurrency, 'narrowSymbol') + '* '; $localTemplate.text(local); $availableComissionArea .safeAppend($localTemplate.prop('outerHTML')) .safeAppend($euroTemplate.prop('outerHTML')); } else { $localTemplate.text(euro); $availableComissionArea.safeAppend($localTemplate.prop('outerHTML')); } $availableBitcoinArea.safeAppend($localTemplate.text(euro).prop('outerHTML')); self.bindDialogEvents(); M.safeShowDialog('affiliate-redeem-dialog', self.$dialog); }).catch(function(ex) { if (d) { console.error('Requesting redeem method list failed: ', ex); } msgDialog('warninga', '', l[200] + ' ' + l[253]); }); }, showSubmitted: function() { 'use strict'; var __closeSubmitted = function() { closeDialog(); $('.fm-dialog-overlay').off('click.redemptionSubmittedClose'); // After closing the dialog, refresh balance and history Promise.all([M.affiliate.getBalance(), M.affiliate.getRedemptionHistory()]).then(() => { affiliateUI.commissionIndex.init(); affiliateUI.redemptionHistory.updateList(); affiliateUI.redemptionHistory.drawTable(); affiliateUI.redemptionHistory.bindEvents(); }).catch((ex) => { if (d) { console.error('Update redmeption page failed: ', ex); } msgDialog('warninga', '', l[200] + ' ' + l[253]); }); }; let $dialog; if (affiliateRedemption.requests.first.m === 0) { $dialog = $('.mega-dialog.affiliate-request-quota', self.$dialog); const {m, s, t} = this.getFormattedPlanData(true); $('.plan-name', $dialog) .text(l.redemption_confirmation_pro_plan_name.replace('%1', affiliateRedemption.plan.planName)); $('.plan-storage', $dialog).text(l.redemption_confirmation_pro_storage.replace('%1', s)); $('.plan-quota', $dialog).text(l.redemption_confirmation_pro_transfer.replace('%1', t)); $('.plan-duration', $dialog).text(l.redemption_confirmation_pro_duration.replace('%1', m)); $('.affiliate-redeem .summary-wrap .pro-price .plan-info', $dialog).addClass('hidden'); $('affiliate-redeem .summary-wrap .pro-price .euro', this.$dialog).addClass('hidden'); } else { $dialog = $('.mega-dialog.affiliate-request.bitcoin', self.$dialog); const message = affiliateRedemption.requests.first.m === 2 ? l[23364] : l[23365]; $('.status-message', $dialog).text(message); } // Bind OK and close buttons $('button', $dialog).rebind('click', __closeSubmitted); $('.fm-dialog-overlay').rebind('click.redemptionSubmittedClose', __closeSubmitted); M.safeShowDialog('affiliate-redeem-submitted', $dialog); }, hide: function(noConfirm) { 'use strict'; var self = this; var __hideAction = function() { self.reset(); closeDialog(); $('.fm-dialog-overlay').off('click.redemptionClose'); }; // if it is not step 1, show confimation dialog before close if (noConfirm) { __hideAction(); } else { msgDialog('confirmation', '', l[20474], l[18229], function(e) { if (e) { __hideAction(); } }); } }, reset: function() { 'use strict'; // Reset previous entered data $('input:not([type="radio"])', this.$dialog).val(''); $('#affiliate-payment-type2', this.$dialog).trigger('click'); $('.next-btn', this.$dialog).addClass('disabled'); $('#affi-bitcoin-address', this.$dialog).val(''); // $('#affiliate-redemption-amount', this.$dialog).attr('data-currencyValue', M.affiliate.balance.available); $('.checkdiv.checkboxOn', this.$dialog).removeClass('checkboxOn').addClass('checkboxOff'); $('.save-data-tip', this.$dialog).addClass('hidden'); $('.dropdown-item-save', this.$dialog).addClass('hidden'); // Reset options for pro plan redemption affiliateRedemption.plan = { chosenPlan: 4, planName: '', planStorage: -1, planQuota: -1, planDuration: undefined, planPriceRedeem: -1, }; affiliateRedemption.reset(); }, bindDialogEvents: function() { 'use strict'; var self = this; var balance = M.affiliate.balance; // Naviagtion & close buttons var $nextbtn = $('.next-btn', this.$dialog); $nextbtn.rebind('click', function() { if ($(this).hasClass('disabled')) { return false; } loadingDialog.show('redeemRequest'); self.$dialog.addClass('arrange-to-back'); affiliateRedemption.processSteps().then(function(res) { if (!res) { return false; } loadingDialog.hide('redeemRequest'); self.$dialog.removeClass('arrange-to-back'); affiliateRedemption.currentStep++; // This is end of flow lets close the dialog and show submitted dialog if (affiliateRedemption.currentStep === 5) { self.showSubmitted(); self.hide(true); } else { self.displaySteps(); } // For Bitcoin payment skip step 3 after update summary table if (affiliateRedemption.currentStep === 3 && affiliateRedemption.requests.first.m === 2) { affiliateRedemption.currentStep++; self.displaySteps(); } // For MEGAquota redemption skip to step 4 if ([2, 3].includes(affiliateRedemption.currentStep) && affiliateRedemption.requests.first.m === 0){ affiliateRedemption.currentStep += (4 - affiliateRedemption.currentStep); // skip to step 4 self.displaySteps(); } }); }); $('.prev-btn', this.$dialog).rebind('click', function() { affiliateRedemption.currentStep--; // For Bitcoin payment skip step 3 if (affiliateRedemption.currentStep === 3 && affiliateRedemption.requests.first.m === 2) { affiliateRedemption.currentStep--; } else if ([2, 3, 4].includes(affiliateRedemption.currentStep) && affiliateRedemption.requests.first.m === 0){ affiliateRedemption.currentStep = 1; } self.displaySteps(); // If this arrive step 2 again, clear country, currency, and dynamic inputs if (affiliateRedemption.currentStep === 2) { delete affiliateRedemption.requests.first.c; delete affiliateRedemption.requests.first.cc; affiliateRedemption.dynamicInputs = {}; affiliateRedemption.requests.second = {}; // uncheck all checkbox from step 3. $('.step3 .checkdiv.checkboxOn', this.$dialog).removeClass('checkboxOn').addClass('checkboxOff'); } if (affiliateRedemption.currentStep === 1) { $('#affi-bitcoin-address', this.$dialog).val(''); const $checkbox = $('.step2 .checkdiv.checkboxOn', this.$dialog) .removeClass('checkboxOn').addClass('checkboxOff'); $('input[type="checkbox"]' ,$checkbox).prop('checked', false); $('.save-data-tip', this.$dialog).addClass('hidden'); } }); $('button.js-close', this.$dialog).rebind('click', this.hide.bind(this, false)); $('.fm-dialog-overlay').rebind('click.redemptionClose', this.hide.bind(this, false)); // Step 0 const $step0 = $('.cells.step0', this.$dialog); const $template = $('.affiliate-payment-template', $step0); const $wrapper = $('.affiliate-redeem .payment-type-wrapper', $step0); $wrapper.empty(); // Generate redemption options based on given gateways let tickFirstRadio = true; const radioText = {0: l.redemption_method_pro_plan, 2: l[6802]}; for (const type in M.affiliate.redeemGateways) { const $clone = $template.clone(); $clone.children('.radioOff').children().attr('id', 'affiliate-payment-type' + type).val(type); $clone.children('label').attr('for', 'affiliate-payment-type' + type).text(radioText[type]); $clone.removeClass('hidden affiliate-payment-template'); // Make sure first radio option is ticked (Pro plan redemption if it is in gateways) if (tickFirstRadio) { $clone.children('.radioOff').removeClass('radioOff').addClass('radioOn'); tickFirstRadio = false; } $wrapper.safeAppend($clone.prop('outerHTML')); } $('.payment-type input', $step0).rebind('change.selectMethodType', function() { $('.radioOn', $step0).removeClass('radioOn').addClass('radioOff'); $(this).parent().addClass('radioOn').removeClass('radioOff'); $nextbtn.removeClass('disabled'); }); // Step 1 var $step1 = $('.cells.step1', this.$dialog); const $amount = $('#affiliate-redemption-amount', $step1); $amount.trigger('input').trigger('blur'); $('.withdraw-txt a', $step1).rebind('click.changeMethod', () => { affiliateRedemption.currentStep = 0; self.displaySteps(); return false; }); $amount.rebind('input', function() { var activeMethodMin = M.affiliate.redeemGateways[$('.payment-type .radioOn input', $step0).val()].min || 50; const megaInput = $(this).data('MegaInputs'); if (megaInput) { megaInput.hideError(); } const val = megaInput ? megaInput.getValue() : 0; if (val >= activeMethodMin && val <= balance.available) { $nextbtn.removeClass('disabled'); } else if (affiliateRedemption.currentStep > 0) { $nextbtn.addClass('disabled'); } }); $amount.rebind('blur', function() { var $this = $(this); var activeMethodMin = M.affiliate.redeemGateways[$('.payment-type .radioOn input', $step0).val()].min || 50; const megaInput = $(this).data('MegaInputs'); const val = megaInput ? megaInput.getValue() : 0; if (!val) { $('.info.price.requested .local', this.$dialog).text('------'); } else if (val < activeMethodMin) { $this.data('MegaInputs').showError(l[23319].replace('%1', formatCurrency(activeMethodMin))); } else if (val > balance.available) { $this.data('MegaInputs').showError(l[23320]); } else { $('.info.price.requested .local', this.$dialog).text(formatCurrency(val)); this.value = $this.attr('type') === 'text' ? formatCurrency(val, 'EUR', 'number') : parseFloat(val).toFixed(2); } }); $('.redeem-all-btn', $step1).rebind('click.redeemAll', function() { $amount.val(balance.available).trigger('input').trigger('blur'); }); // Step 2 var $step2 = $('.cells.step2', this.$dialog); const $saveDataTipBitcoin = $('.save-data-tip', $step2); $('.withdraw-txt a', $step2).rebind('click.changeMethod', function() { affiliateRedemption.currentStep = 0; self.displaySteps(); return false; }); uiCheckboxes($('.save-bitcoin-checkbox', $step2)); uiCheckboxes($('.bitcoin-fill-checkbox', $step2), value => { $('#affi-bitcoin-address', $step2).data('MegaInputs') .setValue(value ? M.affiliate.redeemAccDefaultInfo.an : ''); $saveDataTipBitcoin.addClass('hidden'); }); $('#affi-bitcoin-address', $step2).rebind('input.changeBitcoinAddress', function() { if (!this.value) { $saveDataTipBitcoin.addClass('hidden'); } else if (M.affiliate.redeemAccDefaultInfo && M.affiliate.redeemAccDefaultInfo.an && M.affiliate.redeemAccDefaultInfo.an !== this.value) { $saveDataTipBitcoin.removeClass('hidden'); } Ps.update($step2.children('.ps').get(0)); }); // Step 3 var $step3 = $('.cells.step3', this.$dialog); var $autofillCheckbox = $('.auto-fill-checkbox', $step3); var $saveDataTip = $('.save-data-tip', $step3); var __fillupForm = function(empty) { var keys = Object.keys(M.affiliate.redeemAccDefaultInfo); // Lets do autofill for type first due to it need to render rest of dynamic inputs var $type = $('#account-type', $step3); var savedValue; var $activeOption; if ($type.length) { // If this has multiple types of dynamic input just reset type, clear dynamic inputs box will be enough if (empty) { // Account name var $an = $('#affi-account-name', $step3); $an.data('MegaInputs').setValue('', true); // Account type $('span', $type).text(l[23366]); $('.option.active', $type).removeClass('active'); $('.affi-dynamic-acc-info', $step3).empty(); return; } savedValue = M.affiliate.redeemAccDefaultInfo.type; $activeOption = $('.option.' + savedValue, $type); $type.trigger('click'); $activeOption.trigger('click'); } for (var i = 0; i < keys.length; i++) { var key = keys[i]; // ccc and type are not need to be processed if (key === 'ccc' || key === 'type') { continue; } var hashedKey = MurmurHash3(key); var $target = $('#m' + hashedKey, $step3); if (key === 'an') { $target = $('#affi-account-name', $step3); } savedValue = empty ? '' : M.affiliate.redeemAccDefaultInfo[key]; if ($target.is('.megaInputs.underlinedText')) { $target.data('MegaInputs').setValue(savedValue, true); } else if ($target.hasClass('dropdown-input')) { $activeOption = empty ? $('.option:first', $target) : $('.option[data-type="' + savedValue + '"]', $target); $target.trigger('click'); $activeOption.trigger('click'); } } }; uiCheckboxes($autofillCheckbox, function(value) { const scrollContainer = $step3[0].querySelector('.cell-content'); __fillupForm(!value); $saveDataTip.addClass('hidden'); Ps.update(scrollContainer); scrollContainer.scrollTop = 0; $('input', $step3).off('focus.jsp'); }); uiCheckboxes($('.save-data-checkbox', $step3)); $saveDataTip.add($saveDataTipBitcoin).rebind('click.updateAccData', function(e) { var $this = $(this); var $target = $(e.target); var __hideSaveDataTip = function() { $this.addClass('hidden'); const scrollContainer = $this.closest('.cell-content').get(0); Ps.update(scrollContainer); scrollContainer.scrollTop = 0; $('input', scrollContainer).off('focus.jsp'); }; var _bitcoinValidation = function() { const $input = $('#affi-bitcoin-address'); const megaInput = $input.data('MegaInputs'); if (validateBitcoinAddress($input.val())) { if (megaInput) { megaInput.showError(l[23322]); } else { msgDialog('warninga', '', l[23322]); } return false; } affiliateRedemption.requests.second.extra = {an: $input.val()}; return true; }; const _validation = affiliateRedemption.requests.first.m === 2 ? _bitcoinValidation : affiliateRedemption.validateDynamicAccInputs.bind(affiliateRedemption); if ($target.hasClass('accept') && _validation()) { affiliateRedemption.updateAccInfo(); __hideSaveDataTip(); } else if ($target.hasClass('cancel')) { __hideSaveDataTip(); } }); // Step 4 var $step4 = $('.cells.step4', this.$dialog); $('.withdraw-txt a', $step4).rebind('click.changeMethod', () => { affiliateRedemption.currentStep = 0; self.displaySteps(); return false; }); }, displaySteps: function() { 'use strict'; // If user has a Pro Flexi or Business account, go straight to bitcoin redemption step1 if (affiliateRedemption.currentStep === 0 && u_attr.p === pro.ACCOUNT_LEVEL_BUSINESS || u_attr.p === pro.ACCOUNT_LEVEL_PRO_FLEXI){ affiliateRedemption.currentStep = 1; affiliateRedemption.requests.first.m = 2; } // Show and hide contents $('.cells.left', this.$dialog).addClass('hidden'); const $prevBtn = $('.prev-btn', this.$dialog); const $nextBtn = $('.next-btn', this.$dialog); const buttonText = {0: l[556], 1: l[7348], 2: l[427], 3: l[23367], 4: l[23368]}; const buttonTextQuota = {0: l[556], 1: l[556], 2: l[427], 3: l[23367], 4: l[23368]}; const $currentStep = $('.cells.step' + affiliateRedemption.currentStep, this.$dialog); affiliateRedemption.$step = $currentStep.removeClass('hidden'); // Show and hide prev button if (affiliateRedemption.currentStep === 1 && affiliateRedemption.requests.first.m === 2){ $prevBtn.addClass('hidden'); } else if (affiliateRedemption.currentStep > 0) { $prevBtn.removeClass('hidden'); } else { $prevBtn.addClass('hidden'); } // Timer relates if (affiliateRedemption.currentStep > 2 && affiliateRedemption.requests.first.m !== 0) { affiliateRedemption.startTimer(); } else { affiliateRedemption.stopTimer(); } this['displayStep' + affiliateRedemption.currentStep](); const textToAdd = affiliateRedemption.requests.first.m === 0 ? buttonTextQuota : buttonText; $('span', $nextBtn).text(textToAdd[affiliateRedemption.currentStep]); var $cellContent = $('.cell-content', $currentStep); if ($currentStep.is('.scrollable') && !$cellContent.hasClass('ps')) { Ps.initialize($cellContent[0]); $('input', $currentStep).off('focus.jsp'); } else { $cellContent.scrollTop(0); Ps.update($cellContent[0]); } }, displayStep0: function() { 'use strict'; $('.next-btn', this.$dialog).removeClass('wide disabled').addClass('small'); $('.cells.right', this.$dialog).addClass('hidden'); this.$dialog.addClass('dialog-small').removeClass('dialog-normal dialog-medium dialog-tall disabled'); }, displayStep1: function() { 'use strict'; const $nextBtn = $('.next-btn', this.$dialog).removeClass('wide'); $('.cells.right', this.$dialog).removeClass('hidden'); $('.plan-select-message', this.$dialog).addClass('hidden'); this.$dialog.removeClass('dialog-small').addClass('dialog-normal'); const $bitcoinSummary = $('.cells.right.bitcoin', this.$dialog); const $megaquotaSummary = $('.cells.right.megaquota', this.$dialog); const $planRadios = $('.affiliate-redeem.plan-selection-wrapper', this.$dialog); if (affiliateRedemption.requests.first.m === 0){ $megaquotaSummary.removeClass('hidden'); $bitcoinSummary.addClass('hidden'); $planRadios.removeClass('hidden'); this.$dialog.addClass('dialog-medium').removeClass('dialog-normal'); this.handleProPlans(); } else { $bitcoinSummary.removeClass('hidden').addClass('small step1'); $megaquotaSummary.addClass('hidden'); $planRadios.addClass('hidden'); this.$dialog.removeClass('dialog-medium').addClass('dialog-normal'); $nextBtn.removeClass('small'); if (u_attr.p === pro.ACCOUNT_LEVEL_BUSINESS || u_attr.p === pro.ACCOUNT_LEVEL_PRO_FLEXI){ $('.cells.left .affiliate-redeem.withdraw-txt a', this.$dialog).addClass('hidden'); } } const currentPlan = affiliateRedemption.requests.first.m; // 2 = BTC, 0 = MEGAquota let $currentStep = $('.cells.step1', this.$dialog); if (currentPlan === 0){ $currentStep.addClass('hidden'); $currentStep = $('.cells.step1.megaquota', this.$dialog); $currentStep.removeClass('hidden'); } else { const $amountInput = $('#affiliate-redemption-amount', this.$dialog); const $amountMessage = $('.amount-message-container', this.$dialog); const megaInput = $amountInput.data('MegaInputs') || new mega.ui.MegaInputs($amountInput, { onShowError: function(msg) { $amountMessage.removeClass('hidden').text(msg); }, onHideError: function() { $amountMessage.addClass('hidden'); } }); const amountValue = megaInput.getValue(); const method = affiliateRedemption.requests.first.m; const minValue = M.affiliate.redeemGateways[method].min || 50; if ((amountValue < minValue) || (amountValue > M.affiliate.balance.available) || !amountValue) { $nextBtn.addClass('disabled'); } else { $nextBtn.removeClass('disabled'); } // Summary table update $('.requested.price .euro', this.$dialog).addClass('hidden').text('------'); $('.requested.price .local', this.$dialog).text(amountValue ? formatCurrency(amountValue) : '------'); $('.fee.price .euro', this.$dialog).addClass('hidden').text('------'); $('.fee.price .local', this.$dialog).text('------'); $('.received.price .euro', this.$dialog).addClass('hidden').text('------'); $('.received.price .local', this.$dialog).text('------'); megaInput.hideError(); } // Method text $('.withdraw-txt .method-chosen', $currentStep) .text(affiliateRedemption.getMethodString(affiliateRedemption.requests.first.m)); }, handleProPlans: function() { 'use strict'; const updateRadioButtons = (selection) => { const options = [4, 1, 2, 3]; for (const currentOption of options) { const $currentRadio = $(`#redemptionOption${currentOption}.megaquota-option`, this.$dialog); const $button = $currentRadio.children('.green-active'); $button.removeClass('radioOn radioOff'); if (selection === currentOption) { $button.addClass('radioOn'); } else { $button.addClass('radioOff'); } } const monthsOptions = ['Claim all commission']; for (let i = 1; i <= 24; i++){ monthsOptions.push(i); } affiliateRedemption.plan.chosenPlan = selection; const defaultMonths = affiliateRedemption.plan.planDuration || l.duration; affiliateUI.redemptionDialog.__renderDropdown('redemption-plans', monthsOptions, defaultMonths, this.$dialog); }; const fetchPlansData = () => { const $proPlanOptionTemplate = $('.megaquota-option-template', this.$dialog); const $proPlanOptionArea = $('.megaquota-options-wrapper', this.$dialog); $('.megaquota-option', $proPlanOptionArea).remove(); const acceptedPlans = new Set([4, 1, 2, 3]); // [pro lite, pro 1, pro 2, pro 3] for (const currentPlan of pro.membershipPlans) { const storageFormatted = bytesToSize(currentPlan[pro.UTQA_RES_INDEX_STORAGE] * 1073741824, 0); const storageTxt = l[23789].replace('%1', storageFormatted); const bandwidthFormatted = bytesToSize(currentPlan[pro.UTQA_RES_INDEX_TRANSFER] * 1073741824, 0); const bandwidthTxt = l[23790].replace('%1', bandwidthFormatted); const planNum = currentPlan[pro.UTQA_RES_INDEX_ACCOUNTLEVEL]; const months = currentPlan[pro.UTQA_RES_INDEX_MONTHS]; // There is a 12 month and 1 month version for each plan, // check that it's the one month version so no duplicate plans shown if (acceptedPlans.has(planNum) && months === 1){ const $clone = $proPlanOptionTemplate.clone(); const planName = pro.getProPlanName(planNum); const id = `redemptionOption${planNum}`; $clone.children('.megaquota-option-label').children('.meqaquota-option-name') .text(planName); $clone.children('.megaquota-option-label').children('.meqaquota-option-information') .text(storageTxt + ' / ' + bandwidthTxt); $clone.removeClass('hidden template').addClass('megaquota-option'); $clone.attr('id', id); $proPlanOptionArea.safeAppend($clone.prop('outerHTML')); $proPlanOptionArea.children('#' + id).rebind('click', () => { updateRadioButtons(planNum); this.updateQuotaSummaryTable(planNum); }); } } }; fetchPlansData(); updateRadioButtons(affiliateRedemption.plan.chosenPlan); }, getFormattedPlanData : (shortform, data) => { 'use strict'; shortform = shortform || false; // a: amount, la: localAmount, f: fee, lf: localFee, c: currency, m: months, s: storageQuota, t: transferQuota const {a, la, f, lf, c, m, s, t} = data === undefined ? affiliateRedemption.req1res[0] : data; let monthsTxt = formatCurrency(m, c, 'number', 3); monthsTxt = mega.icu.format(l[922], monthsTxt); // shortform ? n TB : n TB transfer quota const storageFormatted = bytesToSize(s * 1073741825, 3, 4); const storageTxt = shortform ? storageFormatted : l[23789].replace('%1', storageFormatted); const bandwidthFormatted = bytesToSize(t * 1073741824, 3, 4); const bandwidthTxt = shortform ? bandwidthFormatted : l[23790].replace('%1', bandwidthFormatted); return { a: formatCurrency(a, 'EUR', 'narrowSymbol'), la: formatCurrency(la, c, 'narrowSymbol'), f: formatCurrency(f), lf: formatCurrency(lf), c: c || 'EUR', m: monthsTxt, s: storageTxt, t: bandwidthTxt, }; }, getCurrentPlanInfo : (selection) => { 'use strict'; const planInfo = { previousPlanNum : -1, planNum : -1, monthlyPrice : 0, yearlyPrice : 0, monthlyPriceEuros: 0, yearlyPriceEuros : 0, claimAllCommission : false, transferQuota : 0, storageQuota : 0, currency: '', }; for (const currentPlan of pro.membershipPlans) { const months = currentPlan[pro.UTQA_RES_INDEX_MONTHS]; const planNum = currentPlan[pro.UTQA_RES_INDEX_ACCOUNTLEVEL]; planInfo.previousPlanNum = planInfo.planNum; planInfo.planNum = planNum; if (planNum === selection && months === 1) { planInfo.monthlyPrice = currentPlan[pro.UTQA_RES_INDEX_LOCALPRICE]; planInfo.monthlyPriceEuros = currentPlan[pro.UTQA_RES_INDEX_PRICE]; planInfo.transferQuota = currentPlan[pro.UTQA_RES_INDEX_TRANSFER]; } else if (planNum === selection && months === 12){ planInfo.yearlyPrice = currentPlan[pro.UTQA_RES_INDEX_LOCALPRICE]; planInfo.yearlyPriceEuros = currentPlan[pro.UTQA_RES_INDEX_PRICE]; } if (planInfo.planNum === selection && planInfo.planNum === planInfo.previousPlanNum && planInfo.planNum !== -1 && planInfo.previousPlanNum !== -1){ planInfo.storageQuota = currentPlan[pro.UTQA_RES_INDEX_STORAGE]; planInfo.currency = currentPlan[pro.UTQA_RES_INDEX_LOCALPRICECURRENCY]; break; } } return planInfo; }, updateQuotaSummaryTable : (selection) => { 'use strict'; const calculateClaimAllMonths = (availableAmount, pricePerYear, pricePerMonth) => { let months = 0; let counter = 0; while (availableAmount >= pricePerYear && counter <= 100){ availableAmount -= pricePerYear; months += 12; counter++; } months += (availableAmount / pricePerMonth); return months; }; affiliateRedemption.plan.chosenPlan = selection || affiliateRedemption.plan.chosenPlan; const $summaryTable = $('.quota-summary', this.$dialog); const $dropdown = $('.duration-dropdown', this.$dialog); const $planSelectMessage = $('.plan-select-message', this.$dialog); const $dialogWindow = $('.mega-dialog.affiliate-redeem.dialog-template-tool', self.$dialog); let numMonths; numMonths = $('.option.active', $dropdown).data('type'); if (numMonths) { $('.redemption-duration-base', $dropdown).removeClass('redemption-duration-default'); $('.mega-input-title', $dropdown).removeClass('hidden'); } else { $('.duration-dropdown .redemption-duration-base', this.$dialog).addClass('redemption-duration-default'); $('.mega-input-title', $dropdown).addClass('hidden'); } // reset table if (!selection || !numMonths) { $('.pro-plan .plan-info', $summaryTable).text('-'); $('.pro-storage .plan-info', $summaryTable).text('-'); $('.pro-quota .plan-info', $summaryTable).text('-'); $('.pro-duration .plan-info', $summaryTable).text('-'); $('.affiliate-redeem .summary-wrap .pro-price .plan-info', this.$dialog) .text(formatCurrency('', M.affiliate.balance.localCurrency, 'narrowSymbol').replace('NaN', '-')); $('.affiliate-redeem .summary-wrap .pro-price .euro', this.$dialog).addClass('hidden'); $('.next-btn', this.$dialog).addClass('disabled'); $('.insufficient-quota-warning', this.$dialog).addClass('hidden'); $planSelectMessage.removeClass('hidden insufficient-quota-warning').addClass('under-price-warning') .text(l.redemption_cost_too_low); $dialogWindow.addClass('dialog-tall'); return; } const planInfo = affiliateUI.redemptionDialog.getCurrentPlanInfo(selection, numMonths); if (numMonths === 'Claim all commission'){ const claimAllMonths = calculateClaimAllMonths( M.affiliate.balance.available, planInfo.yearlyPriceEuros, planInfo.monthlyPriceEuros); planInfo.claimAllCommission = true; numMonths = claimAllMonths; } const planName = pro.getProPlanName(planInfo.planNum); const monthsCost = planInfo.monthlyPrice * (numMonths % 12); const yearsCost = planInfo.yearlyPrice * Math.floor(numMonths / 12); const monthsCostEuros = planInfo.monthlyPriceEuros * (numMonths % 12); const yearsCostEuros = planInfo.yearlyPriceEuros * Math.floor(numMonths / 12); let totalCost; let totalCostEuros; if (planInfo.claimAllCommission){ totalCost = M.affiliate.balance.localAvailable; totalCostEuros = M.affiliate.balance.available; } else { totalCost = monthsCost + yearsCost; totalCostEuros = monthsCostEuros + yearsCostEuros; } // If the plan is too low cost, reset the table to empty, and warn the user // Otherwise remove the warning if (totalCostEuros < 49.95) { $planSelectMessage.removeClass('hidden insufficient-quota-warning').addClass('under-price-warning') .text(l.redemption_cost_too_low); $dialogWindow.addClass('dialog-tall'); affiliateUI.redemptionDialog.updateQuotaSummaryTable(); return; } $planSelectMessage.addClass('hidden'); $dialogWindow.removeClass('dialog-tall'); totalCostEuros = totalCostEuros.toFixed(8); totalCost = (totalCostEuros * pro.conversionRate).toFixed(8); const dataToFormat = { a: totalCostEuros, la: totalCost, f: 0, lf: 0, c: planInfo.currency, m: numMonths, s: planInfo.storageQuota, t: planInfo.transferQuota * numMonths }; const {a, la, m, s, t, c} = affiliateUI.redemptionDialog.getFormattedPlanData(true, dataToFormat); $('.pro-plan .plan-info', $summaryTable).text(planName); $('.pro-storage .plan-info', $summaryTable).text(s); $('.pro-quota .plan-info', $summaryTable).text(t); $('.pro-duration .plan-info', $summaryTable).text(m); if (!c || c === 'EUR') { $('.affiliate-redeem .summary-wrap .pro-price .plan-info', this.$dialog).text(a + '*'); $('.affiliate-redeem .summary-wrap .pro-price .euro', this.$dialog).addClass('hidden'); } else { $('.affiliate-redeem .summary-wrap .pro-price .plan-info', this.$dialog).text(la + '*'); $('.affiliate-redeem .summary-wrap .pro-price .euro', this.$dialog).text(a).removeClass('hidden'); } affiliateRedemption.plan.planName = planName; affiliateRedemption.plan.planPriceRedeem = totalCostEuros; const $nextBtn = $('.next-btn', this.$dialog); const cost = affiliateRedemption.plan.planPriceRedeem; const method = affiliateRedemption.requests.first.m; const minValue = M.affiliate.redeemGateways[method].min || 50; if (affiliateRedemption.plan.chosenPlan !== -1 && numMonths && cost >= minValue && cost <= M.affiliate.balance.available){ $nextBtn.removeClass('disabled'); } else { $nextBtn.addClass('disabled'); } if (cost > M.affiliate.balance.available){ $planSelectMessage.removeClass('hidden under-price-warning').addClass('insufficient-quota-warning') .text(l.redemption_insufficient_available_commission); $dialogWindow.addClass('dialog-tall'); } else { $planSelectMessage.addClass('hidden'); $dialogWindow.removeClass('dialog-tall'); } }, durationDropdownHandler: function($select) { 'use strict'; const $dropdownItem = $('.option', $select); $dropdownItem.rebind('click.inputDropdown', function() { const $this = $(this); if ($this.hasClass('disabled')) { return; } const months = parseInt($this.data('type')); $select.removeClass('error'); const $item = $('> span', $select); $dropdownItem.removeClass('active').removeAttr('data-state'); $this.addClass('active').attr('data-state', 'active'); const $dropdownSave = $('.dropdown-item-save', $item); const newText = $('.dropdown-item-text', $this).text(); if (months % 12 === 0) { $dropdownSave.removeClass('hidden'); } else { $dropdownSave.addClass('hidden'); } $('.dropdown-text', $item).text(newText); $item.removeClass('placeholder'); $this.trigger('change'); }); }, calculatePrice : function(months, monthlyPrice, yearlyPrice) { 'use strict'; const monthsCost = monthlyPrice * (months % 12); const yearsCost = yearlyPrice * Math.floor(months / 12); return monthsCost + yearsCost; }, setActive : function(type, activeItem, $dropdown, $currentStep) { 'use strict'; if (type === 'redemption-plans') { this.durationDropdownHandler($dropdown); let currentText; if (activeItem === 'Claim all commission') { currentText = l.redemption_claim_max_months; } else if (parseInt(activeItem)) { currentText = mega.icu.format(l[922], activeItem); } else { currentText = activeItem; currentText = parseInt(activeItem) ? mega.icu.format(l[922], activeItem) : activeItem; } this.updateQuotaSummaryTable(affiliateRedemption.plan.chosenPlan); $('#affi-' + type + ' > span span.dropdown-text', $currentStep).text(currentText); } else { $('#affi-' + type + ' > span', $currentStep) .text(type === 'country' ? M.getCountryName(activeItem) : activeItem); } }, __renderDropdown : function(type, list, activeItem, $currentStep) { 'use strict'; const $selectItemTemplate = $('.templates .dropdown-templates .select-item-template', this.$dialog); const $dropdown = $('#affi-' + type, $currentStep); let planInfo; let monthlyPrice; let yearlyPrice; if (type === 'redemption-plans') { $('.duration-dropdown', this.$dialog).rebind("change", () => { const chosenDuration = $('.option.active', '.duration-dropdown').data('type'); affiliateRedemption.plan.planDuration = chosenDuration.toString(); this.updateQuotaSummaryTable(affiliateRedemption.plan.chosenPlan); }); $('.dropdown-scroll', $dropdown).empty(); planInfo = this.getCurrentPlanInfo(affiliateRedemption.plan.chosenPlan); monthlyPrice = planInfo.monthlyPriceEuros || 0; yearlyPrice = planInfo.yearlyPriceEuros || 0; const price = this.calculatePrice(activeItem, monthlyPrice, yearlyPrice); if ((isNaN(price) || price < 49.95) && affiliateRedemption.plan.planDuration !== 'Claim all commission'){ activeItem = l.duration; affiliateRedemption.plan.planDuration = undefined; } } $('.mega-input-dropdown', $currentStep).addClass('hidden'); for (let i = 0; i < list.length; i++) { const $clone = $selectItemTemplate.clone().removeClass('select-item-template'); $clone.remove(); const item = escapeHTML(list[i]); let displayName; if (type === 'country'){ displayName = M.getCountryName(item); } else if (type === 'redemption-plans'){ const cost = this.calculatePrice(i, monthlyPrice, yearlyPrice); displayName = item === 'Claim all commission' ? l.redemption_claim_max_months : mega.icu.format(l[922], item); if (item % 12 === 0){ $clone.children('.dropdown-item-save').removeClass('hidden').text(l.redemption_save_16_percent); } if (cost < 49.95 && item !== 'Claim all commission') { $clone.addClass('disabled'); } } else { displayName = item; } const state = item === activeItem ? 'active' : ''; $clone.attr({"data-type": item, "data-state": state}); $clone.children('.dropdown-item-text').text(displayName); $clone.addClass(state); $('.dropdown-scroll', $dropdown).safeAppend($clone.prop('outerHTML')); } bindDropdownEvents($dropdown); this.setActive(type, activeItem, $dropdown, $currentStep); }, displayStep2: function() { 'use strict'; var $currentStep = $('.cells.step2', this.$dialog); $('.cells.right.bitcoin', this.$dialog).removeClass('step1'); // Method text $('.withdraw-txt .method-chosen', $currentStep) .text(affiliateRedemption.getMethodString(affiliateRedemption.requests.first.m)); // Summary table update $('.requested.price .euro', this.$dialog).addClass('hidden').text('------'); $('.requested.price .local', this.$dialog) .text(formatCurrency(affiliateRedemption.requests.first.p)); $('.fee.price .euro', this.$dialog).addClass('hidden').text('------'); $('.fee.price .local', this.$dialog).text('------'); $('.received.price .euro', this.$dialog).addClass('hidden').text('------'); $('.received.price .local', this.$dialog).text('------'); // Country and currency var selectedGWData = M.affiliate.redeemGateways[affiliateRedemption.requests.first.m]; var seletedGWDefaultData = selectedGWData.data.d || []; var activeCountry = affiliateRedemption.requests.first.cc || seletedGWDefaultData[0] || selectedGWData.data.cc[0]; var activeCurrency = affiliateRedemption.requests.first.c || seletedGWDefaultData[1] || selectedGWData.data.$[0]; this.__renderDropdown('country', selectedGWData.data.cc, activeCountry, $currentStep); this.__renderDropdown('currency', selectedGWData.data.$, activeCurrency, $currentStep); // If this is bitcoin redemption if (affiliateRedemption.requests.first.m === 2) { var megaInput = new mega.ui.MegaInputs($('#affi-bitcoin-address', $currentStep)); megaInput.hideError(); $('.affi-withdraw-currency, .currency-tip', $currentStep).addClass('hidden'); $('.bitcoin-data', $currentStep).removeClass('hidden'); if (M.affiliate.redeemAccDefaultInfo && M.affiliate.redeemAccDefaultInfo.an) { // Autofill bitcoin address $('.save-bitcoin-checkbox', $currentStep).addClass('hidden'); $('.bitcoin-fill-checkbox', $currentStep).removeClass('hidden'); } else { // Save bitcoin address $('.save-bitcoin-checkbox', $currentStep).removeClass('hidden'); $('.bitcoin-fill-checkbox', $currentStep).addClass('hidden'); } } else { $('.affi-withdraw-currency, .currency-tip', $currentStep).removeClass('hidden'); $('.bitcoin-data', $currentStep).addClass('hidden'); } }, displayStep3: function() { 'use strict'; var self = this; var $currentStep = $('.cells.step3', this.$dialog); var selectItemTemplate = '<div class="option @@" data-state="@@" data-type="@@">@@</div>'; var ccc = affiliateRedemption.requests.first.cc + affiliateRedemption.requests.first.c; var req1 = affiliateRedemption.requests.first; var req1res = affiliateRedemption.req1res[0]; // Summary table update if (req1.c !== 'EUR') { $('.requested.price .euro', this.$dialog).removeClass('hidden') .text(`(${formatCurrency(req1.p)})`); $('.fee.price .euro', this.$dialog).removeClass('hidden') .text(`(${formatCurrency(req1res.f)})`); $('.received.price .euro', this.$dialog).removeClass('hidden') .text(`(${formatCurrency(affiliateRedemption.requests.first.p - req1res.f)})`); } if (affiliateRedemption.requests.first.m === 2) { $('.requested.price .local', this.$dialog) .text('BTC ' + parseFloat(req1res.la).toFixed(8)); $('.fee.price .local', this.$dialog).text('BTC ' + parseFloat(req1res.lf).toFixed(8) + '*'); $('.received.price .local', this.$dialog).text('BTC ' + (req1res.la - req1res.lf).toFixed(8)); // This is Bitcoin method just render summary table and proceed. return; } $('.requested.price .local', this.$dialog) .text(formatCurrency(req1res.la, req1res.lc, 'code')); $('.fee.price .local', this.$dialog) .text(formatCurrency(req1res.lf, req1res.lc, 'code') + (req1.c === 'EUR' ? '' : '*')); $('.received.price .local', this.$dialog) .text(formatCurrency(req1res.la - req1res.lf, req1res.lc, 'code')); // Save account relates var $autofillCheckbox = $('.auto-fill-checkbox', $currentStep); var $saveCheckbox = $('.save-data-checkbox', $currentStep); $('.save-data-tip', $currentStep).addClass('hidden'); var __showHideCheckboxes = function() { // If there is saved data and it is same country and currency code, let user have autofill if (M.affiliate.redeemAccDefaultInfo && M.affiliate.redeemAccDefaultInfo.ccc === ccc) { // If saved data exist $autofillCheckbox.removeClass('hidden'); $saveCheckbox.addClass('hidden'); } else { // If saved data do not exist $autofillCheckbox.addClass('hidden'); $saveCheckbox.removeClass('hidden'); } }; __showHideCheckboxes(); var $accountType = $('.affi-dynamic-acc-type', $currentStep); var $selectTemplate = $('.affi-dynamic-acc-select.template', $currentStep); var accNameMegaInput = new mega.ui.MegaInputs($('#affi-account-name', this.$dialog)); accNameMegaInput.hideError(); if (!affiliateRedemption.requests.second.extra) { $('input', $autofillCheckbox).prop('checked', false); $('.checkdiv', $autofillCheckbox).removeClass('checkboxOn').addClass('checkboxOff'); accNameMegaInput.setValue(''); $accountType.empty(); } else if (!affiliateRedemption.requests.second.extra.an) { accNameMegaInput.setValue(''); } else if (!affiliateRedemption.requests.second.extra.type) { $accountType.empty(); } var accTypes = affiliateRedemption.req1res[0].data; // There is dynamic account info required for this. // But if there is already any dynamic input(i.e. it is from step 4) skip rendering if (accTypes && Object.keys(affiliateRedemption.dynamicInputs).length === 0) { $('.affi-dynamic-acc-info', this.$dialog).empty(); // This has multiple account type, therefore let user select it. if (accTypes.length > 1) { var $accountSelector = $selectTemplate.clone().removeClass('template'); $('.mega-input-title', $accountSelector).text(l[23394]); $accountSelector.attr('id', 'account-type'); $('span', $accountSelector).text(l[23366]); var html = ''; var safeArgs = []; for (var i = 0; i < accTypes.length; i++) { html += selectItemTemplate; safeArgs.push(accTypes[i][0], accTypes[i][0], i, accTypes[i][1]); } safeArgs.unshift(html); var $optionWrapper = $('.dropdown-scroll', $accountSelector); $optionWrapper.safeHTML.apply($optionWrapper, safeArgs); $accountType.safeAppend($accountSelector.prop('outerHTML')); bindDropdownEvents($('#account-type', $accountType)); $('#account-type .option' , $accountType).rebind('click.accountTypeSelect', function() { $accountType.parent().removeClass('error'); // Type changed reset dynamic inputs affiliateRedemption.dynamicInputs = {}; self.renderDynamicAccInputs($(this).data('type')); if (!M.affiliate.redeemAccDefaultInfo || M.affiliate.redeemAccDefaultInfo.ccc !== ccc) { $saveCheckbox.removeClass('hidden'); } Ps.update($('.cell-content', $currentStep)[0]); $('input', $currentStep).off('focus.jsp'); }); } else { this.renderDynamicAccInputs(0); } } }, displayStep4: function() { 'use strict'; if (affiliateRedemption.requests.first.m === 0) { $('.next-btn', this.$dialog).addClass('wide'); } else { $('.next-btn', this.$dialog).removeClass('small'); } const $summaryTable = $('.quota-summary', this.$dialog); var $currentStep = $('.cells.step4', this.$dialog); var firstRequest = affiliateRedemption.requests.first; var req1res = affiliateRedemption.req1res[0]; if (firstRequest.m === 0) { $('.bitcoin', $currentStep).addClass('hidden'); $('.megaquota', $currentStep).removeClass('hidden'); const $warning1 = $('.affiliate-redeem .selected-plan-warning1', this.$dialog); const $warning2 = $('.affiliate-redeem .selected-plan-warning2', this.$dialog); const $warning3 = $('.affiliate-redeem .selected-plan-warning3', this.$dialog); const newPlan = affiliateRedemption.plan.chosenPlan; $warning1.addClass('hidden'); $warning2.addClass('hidden'); $warning3.addClass('hidden'); if (u_attr.p === newPlan){ $warning3.removeClass('hidden'); } else if (u_attr.p % 4 > newPlan % 4){ $warning2.removeClass('hidden'); } else if (u_attr.p % 4 < newPlan % 4) { $warning1.removeClass('hidden'); } const planName = pro.getProPlanName(affiliateRedemption.plan.chosenPlan); const {a, la, m, s, t} = this.getFormattedPlanData(true); const $euroArea = $('.affiliate-redeem .summary-wrap .pro-price .euro', this.$dialog); $('.pro-plan .plan-info', $summaryTable).text(planName); $('.pro-storage .plan-info', $summaryTable).text(s); $('.pro-quota .plan-info', $summaryTable).text(t); $('.pro-duration .plan-info', $summaryTable).text(m); $('.affiliate-redeem .summary-wrap .pro-price .plan-info', this.$dialog).text(la + '*'); if (affiliateRedemption.req1res[0].c === 'EUR'){ $euroArea.addClass('hidden'); } else { $euroArea.text(a).removeClass('hidden'); } } else if (firstRequest.m === 2 && (req1res.lf / req1res.la) > 0.1){ $('.bitcoin', $currentStep).removeClass('hidden'); $('.megaquota', $currentStep).addClass('hidden'); $('.fm-dialog-overlay').off('click.redemptionClose'); msgDialog('warningb:!^' + l[78] + '!' + l[79], '', l[24964], l[24965], reject => { if (reject) { this.hide(true); } else { $('.fm-dialog-overlay').rebind('click.redemptionClose', this.hide.bind(this, false)); } }); } $('.email', $currentStep).text(u_attr.email); affiliateRedemption.redemptionAccountDetails($currentStep, firstRequest.m); $('.country', $currentStep).text(M.getCountryName(firstRequest.cc)); $('.currency', $currentStep).text(firstRequest.m === 2 ? 'BTC' : firstRequest.c); }, __showSaveDataTip: function() { 'use strict'; var $step3 = $('.cells.step3', this.$dialog); var ccc = affiliateRedemption.requests.first.cc + affiliateRedemption.requests.first.c; // If it has saved data for it and country and currency code for saved data is same, show update data tip. if (M.affiliate.redeemAccDefaultInfo && M.affiliate.redeemAccDefaultInfo.ccc === ccc) { $('.save-data-tip', $step3).removeClass('hidden'); Ps.update($step3[0].querySelector('.cell-content')); $('input', $step3).off('focus.jsp'); } }, __renderDynamicText: function(textItem, $wrapper) { 'use strict'; var $textTemplate = $('.affi-dynamic-acc-input.template', this.$dialog); var $input = $textTemplate.clone().removeClass('template'); var hashedKey = 'm' + MurmurHash3(textItem.key); $input.attr({ title: '@@', id: hashedKey, minlength: parseInt(textItem.mnl), maxlength: parseInt(textItem.mxl) }); $wrapper.safeAppend($input.prop('outerHTML'), textItem.name); $input = $('#' + hashedKey, $wrapper); var megaInput = new mega.ui.MegaInputs($input); // This is executed to avoid double escaping display in text. updateTitle use text() so safe from XSS. megaInput.updateTitle(textItem.name); if (textItem.example) { $input.parent().addClass('no-trans'); megaInput.showMessage(l[23375].replace('%eg', textItem.example), true); } if (textItem.vr) { $input.data('_vr', textItem.vr); } affiliateRedemption.dynamicInputs[hashedKey] = ['t', $input, textItem.key]; }, __renderDynamicSelect: function(selectItem, $wrapper) { 'use strict'; var self = this; var $selectTemplate = $('.affi-dynamic-acc-select.template', this.$dialog); var selectItemTemplate = '<div class="option %c" data-type="@@" data-state="%s">@@</div>'; var $currentStep = $('.cells.step3', this.$dialog); var defaultCountry; var hashedKey = 'm' + MurmurHash3(selectItem.key); // If there is any country in the gw requested input, prefill it with what already selected. if (selectItem.key.indexOf('country') > -1) { defaultCountry = affiliateRedemption.requests.first.cc; } // This may need to be changed to actual Mega input later. var $select = $selectTemplate.clone().removeClass('template'); $('.mega-input-title', $select).text(selectItem.name); $select.attr({id: hashedKey, title: escapeHTML(selectItem.name)}); var selectHtml = ''; var safeArgs = []; var hasActive = false; for (var j = 0; j < selectItem.va.length; j++) { var option = selectItem.va[j]; var selectItemHtml = selectItemTemplate; safeArgs.push(option.key, option.name); if ((!defaultCountry && j === 0) || (defaultCountry && defaultCountry === option.key)) { selectItemHtml = selectItemHtml.replace('%c', 'active').replace('%s', 'active'); $('span', $select).text(option.name); hasActive = true; } else { selectItemHtml = selectItemHtml.replace('%c', '').replace('%s', ''); } selectHtml += selectItemHtml; } safeArgs.unshift(selectHtml); var $optionWrapper = $('.dropdown-scroll', $select); $optionWrapper.safeHTML.apply($optionWrapper, safeArgs); // If non of option is active with above looping, select first one if (!hasActive) { $('.option', $optionWrapper).first().addClass('active'); } $wrapper.safeAppend($select.prop('outerHTML')); $select = $('#' + hashedKey, $wrapper); bindDropdownEvents($select, 0, '.mega-dialog.affiliate-redeem'); affiliateRedemption.dynamicInputs[hashedKey] = ['s', $select, selectItem.key]; $('.mega-input-dropdown', $select).rebind('click.removeError', function(e) { if ($(e.target).data('type') !== '') { $(this).parents('.mega-input.dropdown-input').removeClass('error'); } }); // There is extra data requires for this. Lets pull it again if (selectItem.rroc) { $wrapper.safeAppend('<div class="extraWrapper" data-parent="@@"></div>', hashedKey); $('.option', $select).rebind('click.showAdditionalInput', function() { var $extraWrapper = $('.extraWrapper[data-parent="' + hashedKey + '"]', $wrapper).empty(); affiliateRedemption.clearDynamicInputs(); // Temporary record for second request as it requires for afftrc. affiliateRedemption.recordSecondReqValues(); loadingDialog.show('rroc'); self.$dialog.addClass('arrange-to-back'); M.affiliate.getExtraAccountDetail().then(function(res) { self.$dialog.removeClass('arrange-to-back'); var additions = res.data[0]; var subtractions = res.data[1]; for (var i = 0; i < subtractions.length; i++) { $('#m' + MurmurHash3(subtractions[i].key)).parent().remove(); } for (var j = 0; j < additions.length; j++) { var hashedAddKey = 'm' + MurmurHash3(additions[j].key); if ($('#' + hashedAddKey, self.$dialog).length === 0) { if (additions[j].va) { self.__renderDynamicSelect(additions[j], $extraWrapper); } else { self.__renderDynamicText(additions[j], $extraWrapper); } } var $newElem = $('#' + hashedAddKey, self.$dialog); var parentHashedKey = $newElem.parents('.extraWrapper').data('parent'); var parentDynamicInput = affiliateRedemption.dynamicInputs[parentHashedKey]; var defaultInfo = M.affiliate.redeemAccDefaultInfo; // This is may triggered by autofill if ($('.auto-fill-checkbox input', self.$dialog).prop('checked') && $('.active', parentDynamicInput[1]).data('type') === defaultInfo[parentDynamicInput[2]]) { if (additions[j].va) { const selectedValue = defaultInfo[additions[j].key]; setDropdownValue( $newElem, ($dropdownInput) => { if (!$dropdownInput.length) { return; } return $(`[data-type="${selectedValue}"]`, $dropdownInput); } ); } else { $newElem.val(defaultInfo[additions[j].key]); } } } $('.option', $extraWrapper) .rebind('click.showSaveTooltip', self.__showSaveDataTip.bind(self)); affiliateRedemption.clearDynamicInputs(); // Lets remove temporary added data for afftrc. affiliateRedemption.requests.second = {}; Ps.update($currentStep[0]); $('input', $currentStep).off('focus.jsp'); loadingDialog.hide('rroc'); }).catch(function(ex) { if (d) { console.error('Extra data pulling error, response:' + ex); } self.$dialog.removeClass('arrange-to-back'); msgDialog('warninga', l[7235], l[200] + ' ' + l[253]); }); }); onIdle(function() { $('.option.active', $select).trigger('click.showAdditionalInput'); }); } }, renderDynamicAccInputs: function(accountType) { 'use strict'; var self = this; var $accountInfo = $('.affi-dynamic-acc-info', this.$dialog).empty(); var $currentStep = $('.cells.step3', this.$dialog); var affr1Res = affiliateRedemption.req1res[0]; var dynamicRequirements = affr1Res.data[accountType][2]; // If this is not array something is wrong, and cannot proceed due to lack of information for the transaction if (!Array.isArray(dynamicRequirements)) { return false; } for (var i = 0; i < dynamicRequirements.length; i++) { var item = dynamicRequirements[i]; // This is select input if (item.va) { self.__renderDynamicSelect(item, $accountInfo); } // This is text input else { self.__renderDynamicText(item, $accountInfo); } } // After rendering, make bind for any input on this stage will show save tooltip when condition met $('input[type="text"]', $currentStep).rebind('input.showSaveTooltip', this.__showSaveDataTip.bind(this)); $('.option', $currentStep).rebind('click.showSaveTooltip', this.__showSaveDataTip.bind(this)); }, }; /* * Redemption history section */ affiliateUI.redemptionHistory = { init: function() { 'use strict'; this.$block = $('.mega-data-box.redemption', affiliateUI.$body); this.$dropdown = $('.dropdown-input.affiliate-redemption', this.$block); // Initial table view for redemption history, no filter, default sort this.list = M.affiliate.redemptionHistory.r; this.sort = 'ts'; this.sortd = 1; this.filter = 'all'; this.drawTable(); this.bindEvents(); }, bindEvents: function() { 'use strict'; var self = this; $('th.sortable', self.$block).rebind('click', function() { var $this = $(this); var $icon = $('i', $this); self.sort = $this.data('type'); if ($icon.hasClass('desc')) { $('.mega-data-box th.sortable i', this.$block) .removeClass('desc asc sprite-fm-mono icon-dropdown'); $icon.addClass('sprite-fm-mono icon-dropdown asc'); self.sortd = -1; } else { $('.mega-data-box th.sortable i', this.$block) .removeClass('desc asc sprite-fm-mono icon-dropdown'); $icon.addClass('sprite-fm-mono icon-dropdown desc'); self.sortd = 1; } self.updateList(); self.drawTable(); self.bindEvents(); }); $(window).rebind('resize.affiliate', self.initRedeemResizeNScroll); // Init redeem detail View/Close link click $('.redeem-table .link', self.$block).rebind('click.redemptionItemExpand', function() { var $this = $(this); var $table = $this.closest('.redeem-scroll'); var $detailBlock = $this.parents('.redeem-summary').next('.redeem-details'); if ($this.hasClass('open')) { // This scroll animation is using CSS animation not jscrollpane animation because it is too heavy. var $scrollBlock = $this.parents('.redeem-scroll').addClass('animateScroll'); $('.expanded', $table).removeClass('expanded'); var rid = $this.data('rid'); const state = $this.data('state'); // After scrolling animation and loading is finihsed expand the item. M.affiliate.getRedemptionDetail(rid, state).then((res) => { affiliateRedemption.fillBasicHistoryInfo($detailBlock, res, state); affiliateRedemption.redemptionAccountDetails($detailBlock, res.gw, res); $table.addClass('expanded-item'); $this.closest('tr').addClass('expanded'); self.initRedeemResizeNScroll(); $scrollBlock.scrollTop($this.parents('tr').position().top); // Just waiting animation to be finihsed setTimeout(function() { $scrollBlock.removeClass('animateScroll'); }, 301); }).catch(function(ex) { if (d) { console.error('Getting redemption detail failed, rid: ' + rid, ex); } msgDialog('warninga', '', l[200] + ' ' + l[253]); }); } else { $table.removeClass('expanded-item'); $this.closest('tr').removeClass('expanded').prev().removeClass('expanded'); self.initRedeemResizeNScroll(); } }); bindDropdownEvents(self.$dropdown, false, affiliateUI.$body); // Click event for item on filter dropdown $('.option', self.$dropdown).rebind('click.showList', function() { var $this = $(this); if (self.filter === $this.data('type')) { return false; } self.filter = $this.data('type'); self.updateList(); self.drawTable(); self.bindEvents(); }); }, updateList: function() { 'use strict'; var self = this; this.list = M.affiliate.getFilteredRedempHistory(this.filter); this.list.sort(function(a, b) { if (a[self.sort] > b[self.sort]) { return -1 * self.sortd; } else if (a[self.sort] < b[self.sort]) { return self.sortd; } // Fallback with timestamp if (a.ts > b.ts) { return -1 * self.sortd; } else if (a.ts < b.ts) { return self.sortd; } return 0; }); }, drawTable: function() { 'use strict'; var $noRedemptionBlock = $('.no-redemption', this.$block); var $itemBlock = $('.redeem-scroll', this.$block); var $table = $('.redeem-table.main', $itemBlock); var $itemSummaryTemplate = $('.redeem-summary.template', $table); var $itemDetailTemplate = $('.redeem-details.template', $table); $('tr:not(:first):not(.template)', $table).remove(); if (this.list.length) { $noRedemptionBlock.addClass('hidden'); $itemBlock.removeClass('hidden'); var html = ''; for (var i = 0; i < this.list.length; i++) { var item = this.list[i]; let proSuccessful; if (item.gw === 0) { if (item.hasOwnProperty('state')) { proSuccessful = item.state === 4; } else { proSuccessful = item.s === 4; } } var itemStatus = affiliateRedemption.getRedemptionStatus(item.s, proSuccessful); var la = parseFloat(item.la); // Filling item data for the summary part var $itemSummary = $itemSummaryTemplate.clone().removeClass('hidden template') .addClass(itemStatus.class); $('.receipt', $itemSummary).text(item.ridd || item.rid); $('.date', $itemSummary).text(time2date(item.ts, 1)); $('.method', $itemSummary).text(affiliateRedemption.getMethodString(item.gw)); if (item.c === 'XBT') { $('.amount', $itemSummary).text('BTC ' + la.toFixed(8)); } else { $('.amount', $itemSummary).text(formatCurrency(la, item.c, 'code')); } $('.status span', $itemSummary).addClass(itemStatus.c).text(itemStatus.s); $('.link', $itemSummary).attr('data-rid', item.rid).attr('data-state', item.s); // Lets prefill details part to reduce looping var $itemDetail = $itemDetailTemplate.clone().removeClass('template'); html += $itemSummary.prop('outerHTML') + $itemDetail.prop('outerHTML'); } $('tbody', $table).safeAppend(html); this.initRedeemResizeNScroll(); } else { $noRedemptionBlock.removeClass('hidden'); $itemBlock.addClass('hidden'); } }, // Init redeem content scrolling and table header resizing initRedeemResizeNScroll: function() { 'use strict'; // If list is empty, do not need to abjust the view. if (!M.affiliate.redemptionHistory || M.affiliate.redemptionHistory.r.length === 0) { return false; } var $scrollElement = $('.redeem-scroll', this.$block); if ($scrollElement.hasClass('ps')) { Ps.update($scrollElement[0]); } else { Ps.initialize($scrollElement[0]); } var $header = $('.redeem-table.main th', this.$block); for (var i = 0; i < $header.length; i++) { var $clonedHeader = $('.redeem-table.clone th', this.$block); if ($clonedHeader.eq(i).length) { $clonedHeader.eq(i).outerWidth($header.eq(i).outerWidth()); } } // Remove default focus scrolling from jsp $('a', $scrollElement).off('focus.jsp'); }, }; /* * Registration index section */ affiliateUI.registrationIndex = { init: function() { 'use strict'; this.$registerChartBlock = $('.mega-data-box.registration', affiliateUI.$body); this.type = $('.fm-affiliate.chart-period.active', this.$registerChartBlock).data('type'); this.bindEvents(); this.calculateTotal(); this.setChartTimeBlock(); this.drawChart(); }, bindEvents: function() { 'use strict'; var self = this; var $buttons = $('.fm-affiliate.chart-period', this.$registerChartBlock); var $datesWrapper = $('.fm-affiliate.chart-dates', this.$registerChartBlock); $buttons.rebind('click.chooseChartPeriod', function() { $buttons.removeClass('active'); this.classList.add('active'); self.type = this.dataset.type; self.setChartTimeBlock(); self.drawChart(); }); $datesWrapper.rebind('click.selectDateRange', function(e) { var classList = e.target.classList; if (classList.contains('prev-arrow') && !classList.contains('disabled')) { self.setChartTimeBlock(self.start - 1); } else if (classList.contains('next-arrow') && !classList.contains('disabled')) { self.setChartTimeBlock(self.end + 1); } else { return false; } self.drawChart(); }); }, /** * Calculate Total registerations and incremented value over last month * @returns {Void} void function */ calculateTotal: function() { 'use strict'; this.total = M.affiliate.signupList.length; $('.affiliate-total-reg', this.$registerChartBlock).text(this.total); var thisMonth = calculateCalendar('m'); var thisMonthCount = 0; M.affiliate.signupList.forEach(function(item) { if (thisMonth.start <= item.ts && item.ts <= thisMonth.end) { thisMonthCount++; } }); $('.charts-head .compare span', this.$registerChartBlock).text(thisMonthCount); }, /** * Set period block(start and end times) to render chart, depends on time given, type of period. * @param {Number} unixTime unixtime stamp given. * @returns {Void} void function */ setChartTimeBlock: function(unixTime) { 'use strict'; var $datesBlock = $('.fm-affiliate.chart-dates .dates', affiliateUI.$body); var calendar = calculateCalendar(this.type || 'w', unixTime); this.start = calendar.start; this.end = calendar.end; if (this.type === 'w') { var startDate = acc_time2date(this.start, true); var endDate = acc_time2date(this.end, true); $datesBlock.text(l[22899].replace('%d1' ,startDate).replace('%d2' ,endDate)); } else if (this.type === 'm') { $datesBlock.text(time2date(this.start, 3)); } else if (this.type === 'y') { $datesBlock.text(new Date(this.start * 1000).getFullYear()); } var startlimit = 1577836800; var endlimit = Date.now() / 1000; var $prevBtn = $('.fm-affiliate.chart-dates .prev-arrow', affiliateUI.$body).removeClass('disabled'); var $nextBtn = $('.fm-affiliate.chart-dates .next-arrow', affiliateUI.$body).removeClass('disabled'); if (this.start < startlimit) { $prevBtn.addClass('disabled'); } if (this.end > endlimit) { $nextBtn.addClass('disabled'); } }, /** * Set labels for the chart depends on type of period. * @returns {Array} lbl An array of labels */ getLabels: function() { 'use strict'; var lbl = []; if (this.type === 'w') { for (var i = 0; i <= 6; i++) { lbl.push(time2date(this.start + i * 86400, 10).toUpperCase()); } } else if (this.type === 'm') { var endDateOfMonth = new Date(this.end * 1000).getDate(); for (var k = 1; k <= endDateOfMonth; k++) { lbl.push(k); } } else if (this.type === 'y') { var startDate = new Date(this.start * 1000); lbl.push(time2date(startDate.getTime() / 1000, 12).toUpperCase()); for (var j = 0; j <= 10; j++) { startDate.setMonth(startDate.getMonth() + 1); lbl.push(time2date(startDate.getTime() / 1000, 12).toUpperCase()); } } return lbl; }, /** * Lets draw chart with settled options and data. * @returns {Void} void function */ drawChart: function() { 'use strict'; var self = this; var labels = this.getLabels(); var data = []; if (this.chart) { this.chart.destroy(); } M.affiliate.signupList.forEach(function(item) { if (self.start <= item.ts && item.ts <= self.end) { var val; switch (self.type) { case 'w': val = time2date(item.ts, 10).toUpperCase(); break; case 'm': val = new Date(item.ts * 1000).getDate(); break; case 'y': val = time2date(item.ts, 12).toUpperCase(); break; } var index = labels.indexOf(val); data[index] = data[index] + 1 || 1; } }); var $chartWrapper = $('.mega-data-box.registration', affiliateUI.$body); var $ctx = $('#register-chart', $chartWrapper); var chartColor1 = $ctx.css('--label-blue'); var chartColor2 = $ctx.css('--label-blue-hover'); var dividerColor = $ctx.css('--surface-grey-2'); var textColor = $ctx.css('--text-color-low'); var ticksLimit = 6; $ctx.outerHeight(186); // TODO: set ticksLimit=4 for all months after library update if (this.type === 'm') { var daysInMonth = new Date(this.end * 1000).getDate(); ticksLimit = daysInMonth === 28 ? 5 : 4; } this.chart = new Chart($ctx[0], { type: 'bar', data: { labels: labels, datasets: [{ data: data, backgroundColor: chartColor1, hoverBackgroundColor: chartColor2, borderWidth: 1, borderColor: chartColor1.replace(/^\s/ , '') }] }, options: { barRoundness: 1, maintainAspectRatio: false, legend: { display: false }, responsive: true, scales: { xAxes: [{ display: true, maxBarThickness: 8, gridLines : { display : false }, ticks: { fontColor: textColor, fontSize: 12, autoSkip: true, maxTicksLimit: ticksLimit, maxRotation: 0 } }], yAxes: [{ display: true, ticks: { fontColor: textColor, fontSize: 12, beginAtZero: true, precision: 0, suggestedMax: 4 }, gridLines: { color: dividerColor, zeroLineColor: dividerColor, drawBorder: false, } }] }, tooltips: { displayColors: false, callbacks: { title: function(tooltipItem) { if (self.type === 'm') { var ttDate = new Date(self.start * 1000); ttDate.setDate(tooltipItem[0].xLabel | 0); return acc_time2date(ttDate.getTime() / 1000, true); } return tooltipItem[0].xLabel; } } } } }); } }; /** * Purchase index */ affiliateUI.purchaseIndex = { init: function() { 'use strict'; this.$purchaseChartBlock = $('.mega-data-box.purchase', affiliateUI.$body); this.count(); this.drawChart(); }, /** * Count type of purchase made * @returns {Void} void function */ count: function() { 'use strict'; var self = this; var creditList = M.affiliate.creditList.active.concat(M.affiliate.creditList.pending); var thisMonth = calculateCalendar('m'); var proPlanIDMap = {}; this.totalCount = creditList.length; this.monthCount = 0; this.countedData = {}; pro.membershipPlans.forEach(function(item) { proPlanIDMap[item[0]] = item[1]; }); creditList.forEach(function(item) { if (thisMonth.start <= item.gts && item.gts <= thisMonth.end) { self.monthCount++; } const index = proPlanIDMap[item.si]; if (item.b && index !== 101) { self.countedData.b = ++self.countedData.b || 1; } else { self.countedData[index] = ++self.countedData[index] || 1; } }); $('.affiliate-total-pur', this.$purchaseChartBlock).text(this.totalCount); $('.charts-head .compare span', this.$purchaseChartBlock).text(this.monthCount); $('.list-item.prol .label', this.$purchaseChartBlock) .text(formatPercentage(this.countedData[4] / this.totalCount || 0)); $('.list-item.prol .num', this.$purchaseChartBlock).text(this.countedData[4] || 0); $('.list-item.pro1 .label', this.$purchaseChartBlock) .text(formatPercentage(this.countedData[1] / this.totalCount || 0)); $('.list-item.pro1 .num', this.$purchaseChartBlock).text(this.countedData[1] || 0); $('.list-item.pro2 .label', this.$purchaseChartBlock) .text(formatPercentage(this.countedData[2] / this.totalCount || 0)); $('.list-item.pro2 .num', this.$purchaseChartBlock).text(this.countedData[2] || 0); $('.list-item.pro3 .label', this.$purchaseChartBlock) .text(formatPercentage(this.countedData[3] / this.totalCount || 0)); $('.list-item.pro3 .num', this.$purchaseChartBlock).text(this.countedData[3] || 0); $('.list-item.pro101 .label', this.$purchaseChartBlock) .text(formatPercentage(this.countedData[101] / this.totalCount || 0)); $('.list-item.pro101 .num', this.$purchaseChartBlock).text(this.countedData[101] || 0); $('.list-item.business .label', this.$purchaseChartBlock) .text(formatPercentage(this.countedData.b / this.totalCount || 0)); $('.list-item.business .num', this.$purchaseChartBlock).text(this.countedData.b || 0); }, /** * Let draw chart with given data. * @returns {Void} void function */ drawChart: function() { 'use strict'; if (this.chart) { this.chart.destroy(); } var $chartWrapper = $('.mega-data-box.purchase', affiliateUI.$body); var $ctx = $('#purchase-chart', $chartWrapper); this.chart = new Chart($ctx[0], { type: 'doughnut', data: { datasets: [{ data: [ this.countedData[4], this.countedData[1], this.countedData[2], this.countedData[3], this.countedData[101], this.countedData.b, $.isEmptyObject(this.countedData) ? 1 : 0 ], backgroundColor: [ $ctx.css('--label-yellow'), $ctx.css('--label-orange'), $ctx.css('--label-red'), $ctx.css('--label-purple'), $ctx.css('--label-blue'), $ctx.css('--label-green'), $ctx.css('--surface-grey-2') ], borderWidth: 0 }] }, options: { events: [], legend: { display: false }, cutoutPercentage: 74 } }); }, }; /** * Geographic distribution */ affiliateUI.geographicDistribution = { init: function() { 'use strict'; this.$geoDistBlock = $('.distribution', affiliateUI.$body); this.bindEvents(); this.count(); this.drawTable(); }, bindEvents: function() { 'use strict'; var self = this; $('.distribution-head .tab-button', this.$geoDistBlock).rebind('click.geoDist', function() { var $this = $(this); $('.tab-button', self.$geoDistBlock).removeClass('active'); $('.chart-body', self.$geoDistBlock).addClass('hidden'); $this.addClass('active'); $('.chart-body.' + $(this).data('table'), self.$geoDistBlock).removeClass('hidden'); }); }, /** * Count how many registration/puchases are made on each country * @returns {Void} void function */ count: function() { 'use strict'; var self = this; this.signupGeo = {}; M.affiliate.signupList.forEach(function(item) { self.signupGeo[item.cc] = ++self.signupGeo[item.cc] || 1; }); this.creditGeo = {}; var creditList = M.affiliate.creditList.active.concat(M.affiliate.creditList.pending); creditList.forEach(function(item) { self.creditGeo[item.cc] = ++self.creditGeo[item.cc] || 1; }); }, /** * Let's draw table with give data * @returns {Void} void function */ drawTable: function() { 'use strict'; var self = this; var template = '<div class="fm-affiliate list-item">' + '<div class="img-wrap"><img src="$countryImg" alt=""></div>' + '<span class="name">$countryName</span><span class="num">$count</span>' + '</div>'; var _sortFunc = function(a, b) { return self.signupGeo[b] - self.signupGeo[a]; }; var orderedSignupGeoKeys = Object.keys(this.signupGeo).sort(_sortFunc); var orderedCreditGeoKeys = Object.keys(this.creditGeo).sort(_sortFunc); var html = ''; var countList = this.signupGeo; var _htmlFunc = function(item) { var country = countrydetails(item); html += template.replace('$countryImg', staticpath + 'images/flags/' + country.icon) .replace('$countryName', country.name || 'Unknown').replace('$count', countList[item]); }; orderedSignupGeoKeys.forEach(_htmlFunc); if (html) { $('.geo-dist-reg .list', this.$geoDistBlock).safeHTML(html); } else { $('.geo-dist-reg .list', this.$geoDistBlock).empty(); } html = ''; countList = this.creditGeo; orderedCreditGeoKeys.forEach(_htmlFunc); if (html) { $('.geo-dist-pur .list', this.$geoDistBlock).safeHTML(html); } else { $('.geo-dist-pur .list', this.$geoDistBlock).empty(); } } }; /* * Dashboard End */