/* global $ ConversationController, extension, ConversationController getConversations, getInboxCollection, i18n, Whisper, textsecure, Signal */ // eslint-disable-next-line func-names (function() { 'use strict'; window.Whisper = window.Whisper || {}; Whisper.ConversationStack = Whisper.View.extend({ className: 'conversation-stack', open(conversation) { const id = `conversation-${conversation.cid}`; const container = $('#main-view .conversation-stack'); // Has been opened since app start, but not focussed const conversationExists = container.children(`#${id}`).length > 0; // Is focussed const conversationOpened = container.children().first().id === id; // To limit the size of the DOM for speed const maxNumConversations = 10; const numConversations = container .children() .not('.conversation.placeholder').length; const shouldTrimConversations = numConversations > maxNumConversations; if (shouldTrimConversations) { // Removes conversation which has been hidden the longest container.children()[numConversations - 1].remove(); } if (conversationExists) { // User opened conversation, move it to top of stack, rather than re-rendering const conversations = container .children() .not('.conversation.placeholder'); container .children(`#${id}`) .first() .insertBefore(conversations.first()); conversation.trigger('opened'); return; } if (!conversationOpened) { this.$el .first() .find('video, audio') .each(function pauseMedia() { this.pause(); }); let $el = this.$(`#${id}`); if ($el === null || $el.length === 0) { const view = new Whisper.ConversationView({ model: conversation, window: this.model.window, }); view.view.resetScrollPosition(); // eslint-disable-next-line prefer-destructuring $el = view.$el; } container.prepend($el); } conversation.trigger('opened'); }, close(conversation) { const $el = $(`#conversation-${conversation.cid}`); if ($el && $el.length > 0) { $el.remove(); } }, showToast({ message }) { window.pushToast({ title: message, type: 'success', }); }, showConfirmationDialog({ title, message, onOk, onCancel }) { window.confirmationDialog({ title, message, resolve: onOk, reject: onCancel, }); }, }); Whisper.AppLoadingScreen = Whisper.View.extend({ templateName: 'app-loading-screen', className: 'app-loading-screen', updateProgress() {}, render_attributes: { message: i18n('loading'), }, }); Whisper.InboxView = Whisper.View.extend({ templateName: 'two-column', className: 'inbox index', initialize(options = {}) { this.ready = false; this.render(); this.$el.attr('tabindex', '1'); this.conversation_stack = new Whisper.ConversationStack({ el: this.$('.conversation-stack'), model: { window: options.window }, }); if (!window.storage.get('betaReleaseDisclaimerAccepted')) { this.showBetaReleaseDisclaimer(); } if (!options.initialLoadComplete) { this.appLoadingScreen = new Whisper.AppLoadingScreen(); this.appLoadingScreen.render(); this.appLoadingScreen.$el.prependTo(this.el); this.startConnectionListener(); } // Inbox const inboxCollection = getInboxCollection(); // ConversationCollection const conversations = getConversations(); this.listenTo(conversations, 'remove', conversation => { if (this.conversation_stack) { this.conversation_stack.close(conversation); } }); this.listenTo(inboxCollection, 'messageError', () => { if (this.networkStatusView) { this.networkStatusView.render(); } }); this.networkStatusView = new Whisper.NetworkStatusView(); this.$el .find('.network-status-container') .append(this.networkStatusView.render().el); if (extension.expired()) { const banner = new Whisper.ExpiredAlertBanner().render(); banner.$el.prependTo(this.$el); this.$el.addClass('expired'); } // FIXME: Fix this for new react views this.updateInboxSectionUnread(); this.setupLeftPane(); }, render_attributes: { welcomeToSignal: i18n('welcomeToSignal'), selectAContact: i18n('selectAContact'), }, events: { click: 'onClick', 'click .section-toggle': 'toggleSection', }, setupLeftPane() { // Here we set up a full redux store with initial state for our LeftPane Root const convoCollection = getConversations(); const conversations = convoCollection.map( conversation => conversation.cachedProps ); const initialState = { conversations: { conversationLookup: Signal.Util.makeLookup(conversations, 'id'), }, user: { regionCode: window.storage.get('regionCode'), ourNumber: window.storage.get('primaryDevicePubKey') || textsecure.storage.user.getNumber(), isSecondaryDevice: !!window.storage.get('isSecondaryDevice'), i18n: window.i18n, }, }; this.store = Signal.State.createStore(initialState); window.inboxStore = this.store; this.leftPaneView = new Whisper.ReactWrapperView({ JSX: Signal.State.Roots.createLeftPane(this.store), className: 'left-pane-wrapper', }); // Enables our redux store to be updated by backbone events in the outside world const { conversationAdded, conversationChanged, conversationRemoved, removeAllConversations, messageExpired, openConversationExternal, } = Signal.State.bindActionCreators( Signal.State.Ducks.conversations.actions, this.store.dispatch ); const { userChanged } = Signal.State.bindActionCreators( Signal.State.Ducks.user.actions, this.store.dispatch ); this.openConversationAction = openConversationExternal; // In the future this listener will be added by the conversation view itself. But // because we currently have multiple converations open at once, we install just // one global handler. // $(document).on('keydown', event => { // const { ctrlKey, key } = event; // We can add Command-E as the Mac shortcut when we add it to our Electron menus: // https://stackoverflow.com/questions/27380018/when-cmd-key-is-kept-pressed-keyup-is-not-triggered-for-any-other-key // For now, it will stay as CTRL-E only // if (key === 'e' && ctrlKey) { // const state = this.store.getState(); // const selectedId = state.conversations.selectedConversation; // const conversation = ConversationController.get(selectedId); // if (conversation && !conversation.get('isArchived')) { // conversation.setArchived(true); // conversation.trigger('unload'); // } // } // }); this.listenTo(convoCollection, 'remove', conversation => { const { id } = conversation || {}; conversationRemoved(id); }); this.listenTo(convoCollection, 'add', conversation => { const { id, cachedProps } = conversation || {}; conversationAdded(id, cachedProps); }); this.listenTo(convoCollection, 'change', conversation => { const { id, cachedProps } = conversation || {}; conversationChanged(id, cachedProps); }); this.listenTo(convoCollection, 'reset', removeAllConversations); Whisper.events.on('messageExpired', messageExpired); Whisper.events.on('userChanged', userChanged); // Finally, add it to the DOM this.$('.left-pane-placeholder').append(this.leftPaneView.el); }, startConnectionListener() { this.interval = setInterval(() => { const status = window.getSocketStatus(); switch (status) { case WebSocket.CONNECTING: break; case WebSocket.OPEN: clearInterval(this.interval); // Default to connected, but lokinet is slow so we pretend empty event this.onEmpty(); this.interval = null; break; case WebSocket.CLOSING: case WebSocket.CLOSED: clearInterval(this.interval); this.interval = null; // if we failed to connect, we pretend we got an empty event this.onEmpty(); break; default: // We also replicate empty here this.onEmpty(); break; } }, 1000); }, onEmpty() { const view = this.appLoadingScreen; if (view) { this.appLoadingScreen = null; view.remove(); } }, onProgress(count) { const view = this.appLoadingScreen; if (view) { view.updateProgress(count); } }, focusConversation(e) { if (e && this.$(e.target).closest('.placeholder').length) { return; } this.$('#header, .gutter').addClass('inactive'); this.$('.conversation-stack').removeClass('inactive'); }, focusHeader() { this.$('.conversation-stack').addClass('inactive'); this.$('#header, .gutter').removeClass('inactive'); this.$('.conversation:first .menu').trigger('close'); }, reloadBackgroundPage() { window.location.reload(); }, toggleSection(e) { // Expand or collapse this panel const $target = this.$(e.currentTarget); const $next = $target.next(); // Toggle section visibility $next.slideToggle('fast'); $target.toggleClass('section-toggle-visible'); }, async openConversation(id, messageId) { const conversationExists = await ConversationController.get(id); // If we call this to create a new conversation, it can only be private // (group conversations are created elsewhere) const conversation = await ConversationController.getOrCreateAndWait( id, 'private' ); if (this.openConversationAction) { this.openConversationAction(id, messageId); } if (conversation) { if (conversation.isRss()) { window.mixpanel.track('RSS Feed Opened'); } if (conversation.isPublic()) { window.mixpanel.track('Loki Public Chat Opened'); } if (conversation.isPrivate()) { if (conversation.isMe()) { window.mixpanel.track('Note To Self Opened'); } else if (conversationExists) { window.mixpanel.track('Conversation Opened'); } } conversation.updateProfileName(); } this.conversation_stack.open(conversation); this.focusConversation(); }, closeConversation(conversation) { if (conversation) { this.inboxListView.removeItem(conversation); this.conversation_stack.close(conversation); } }, closeRecording(e) { if (e && this.$(e.target).closest('.capture-audio').length > 0) { return; } this.$('.conversation:first .recorder').trigger('close'); }, updateInboxSectionUnread() { // FIXME: Fix this for new react views // const $section = this.$('.section-conversations-unread-counter'); // const models = // (this.inboxListView.collection && // this.inboxListView.collection.models) || // []; // const unreadCount = models.reduce( // (count, m) => count + Math.max(0, m.get('unreadCount')), // 0 // ); // $section.text(unreadCount); // if (unreadCount > 0) { // $section.show(); // } else { // $section.hide(); // } }, onClick(e) { this.closeRecording(e); }, showToastMessageInGutter(message) { const toast = new Whisper.MessageToastView({ message, }); toast.$el.appendTo(this.$('.gutter')); toast.render(); }, showBetaReleaseDisclaimer() { const dialog = new Whisper.BetaReleaseDisclaimer(); this.el.append(dialog.el); }, }); Whisper.ExpiredAlertBanner = Whisper.View.extend({ templateName: 'expired_alert', className: 'expiredAlert clearfix', render_attributes() { return { expiredWarning: i18n('expiredWarning'), upgrade: i18n('upgrade'), }; }, }); })();