Ver código fonte

Implement pinning of conversations

Danilo Bargen 7 anos atrás
pai
commit
bcd29f7935

+ 7 - 1
public/i18n/de.json

@@ -183,7 +183,13 @@
         "MUTED_MENTION_ONLY": "Nur bei Erwähnung benachrichtigen",
         "MUTED_SILENT": "Stumme Benachrichtigungen",
         "ALL": "Alle",
-        "LOADING_MESSAGES": "Nachrichten werden geladen…"
+        "LOADING_MESSAGES": "Nachrichten werden geladen…",
+        "PINNED_CONVERSATION": "Unterhaltung ist angepinnt. Klicken, um sie zu entpinnen.",
+        "UNPINNED_CONVERSATION": "Unterhaltung ist nicht angepinnt. Klicken, um sie anzupinnen.",
+        "PINNED_CONVERSATION_OK": "Unterhaltung angepinnt",
+        "PINNED_CONVERSATION_ERROR": "Unterhaltung konnte nicht angepinnt werden",
+        "UNPINNED_CONVERSATION_OK": "Unterhaltung entpinnt",
+        "UNPINNED_CONVERSATION_ERROR": "Unterhaltung konnte nicht entpinnt werden"
     },
     "messageStates": {
         "WE_ACK": "Sie haben ein Daumen-Hoch gesendet",

+ 7 - 1
public/i18n/en.json

@@ -182,7 +182,13 @@
         "MUTED_MENTION_ONLY": "Only show notification when mentioned",
         "MUTED_SILENT": "Silent notifications",
         "ALL": "All",
-        "LOADING_MESSAGES": "Loading messages…"
+        "LOADING_MESSAGES": "Loading messages…",
+        "PINNED_CONVERSATION": "Conversation is pinned. Click to unpin.",
+        "UNPINNED_CONVERSATION": "Conversation is not pinned. Click to pin.",
+        "PINNED_CONVERSATION_OK": "Conversation pinned",
+        "PINNED_CONVERSATION_ERROR": "Conversation could not be pinned",
+        "UNPINNED_CONVERSATION_OK": "Conversation unpinned",
+        "UNPINNED_CONVERSATION_ERROR": "Conversation could not be unpinned"
     },
     "messageStates": {
         "WE_ACK": "You sent thumbs-up",

+ 10 - 0
public/img/ic_pin.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+  height="24"
+  width="24"
+  viewBox="0 0 24 24">
+  <path
+    fill="#000000"
+    fill-opacity="0.54"
+    d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z"/>
+</svg>

+ 10 - 0
public/img/ic_unpin.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+  height="24"
+  width="24"
+  viewBox="0 0 24 24">
+  <path
+    fill="#000000"
+    fill-opacity="0.54"
+    d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12M8.8,14L10,12.8V4H14V12.8L15.2,14H8.8Z" />
+</svg>

+ 23 - 0
src/partials/messenger.conversation.html

@@ -6,6 +6,8 @@
 
     <div id="conversation-header" class="detail-header">
         <ng-include src="'partials/messenger.backbutton.html'"></ng-include>
+
+        <!-- Conversation details -->
         <div class="header-avatar" ng-click="ctrl.showReceiver()">
             <eee-avatar eee-receiver="ctrl.receiver"
                         eee-resolution="'low'"></eee-avatar>
@@ -22,7 +24,28 @@
                 <span ng-bind-html="ctrl.receiver.members | idsToNames | escapeHtml | emojify"></span>
             </div>
         </div>
+
+        <!-- Menu -->
+        <div class="header-buttons">
+            <md-button
+                class="md-icon-button"
+                aria-label="Conversation is pinned"
+                translate-attr="{'aria-label': 'messenger.PINNED_CONVERSATION', 'title': 'messenger.PINNED_CONVERSATION'}"
+                ng-if="ctrl.conversation.isStarred"
+                ng-click="ctrl.unpinConversation()">
+                <md-icon><img src="img/ic_pin.svg"></md-icon>
+            </md-button>
+            <md-button
+                class="md-icon-button"
+                aria-label="Conversation is not pinned"
+                translate-attr="{'aria-label': 'messenger.UNPINNED_CONVERSATION', 'title': 'messenger.UNPINNED_CONVERSATION'}"
+                ng-if="!ctrl.conversation.isStarred"
+                ng-click="ctrl.pinConversation()">
+                <md-icon><img src="img/ic_unpin.svg"></md-icon>
+            </md-button>
+        </div>
     </div>
+
     <div id="conversation-is-private" ng-if="ctrl.locked">
         <md-card>
             <md-toolbar class="md-warn">

+ 39 - 2
src/partials/messenger.ts

@@ -233,6 +233,7 @@ class ConversationController {
 
     // The conversation receiver
     public receiver: threema.Receiver;
+    public conversation: threema.Conversation;
     public type: threema.ReceiverType;
 
     // The conversation messages
@@ -342,9 +343,10 @@ class ConversationController {
             }, 100, this), supportsPassive() ? {passive: true} : false);
         }
 
-        // Set receiver and type
+        // Set receiver, conversation and type
         try {
             this.receiver = webClientService.receivers.getData({type: $stateParams.type, id: $stateParams.id});
+            this.conversation = this.webClientService.conversations.find(this.receiver);
             this.type = $stateParams.type;
 
             if (this.receiver.type === undefined) {
@@ -493,7 +495,7 @@ class ConversationController {
         this.webClientService.setQuote(this.receiver);
     }
 
-    public showError(errorMessage: string, hideDelayMs = 3000) {
+    public showError(errorMessage?: string, hideDelayMs = 3000) {
         if (errorMessage === undefined || errorMessage.length === 0) {
             errorMessage = this.$translate.instant('error.ERROR_OCCURRED');
         }
@@ -503,6 +505,15 @@ class ConversationController {
                 .position('bottom center')
                 .hideDelay(hideDelayMs));
     }
+
+    public showMessage(msgTranslation: string, hideDelayMs = 3000) {
+        this.$mdToast.show(
+            this.$mdToast.simple()
+                .textContent(this.$translate.instant(msgTranslation))
+                .position('bottom center')
+                .hideDelay(hideDelayMs));
+    }
+
     /**
      * Submit function for input field. Can contain text or file data.
      * Return whether sending was successful.
@@ -856,6 +867,32 @@ class ConversationController {
         const chat = this.domChatElement;
         this.showScrollJump = chat.scrollHeight - (chat.scrollTop + chat.offsetHeight) > 10;
     }
+
+    /**
+     * Mark the current conversation as pinned.
+     */
+    public pinConversation(): void {
+        this.webClientService
+            .modifyConversation(this.conversation, true)
+            .then(() => this.showMessage('messenger.PINNED_CONVERSATION_OK'))
+            .catch((msg) => {
+                this.showMessage('messenger.PINNED_CONVERSATION_ERROR');
+                this.$log.error(this.logTag, 'Pinning conversation failed: ' + msg);
+            });
+    }
+
+    /**
+     * Mark the current conversation as not pinned.
+     */
+    public unpinConversation(): void {
+        this.webClientService
+            .modifyConversation(this.conversation, false)
+            .then(() => this.showMessage('messenger.UNPINNED_CONVERSATION_OK'))
+            .catch((msg) => {
+                this.showMessage('messenger.UNPINNED_CONVERSATION_ERROR');
+                this.$log.error(this.logTag, 'Unpinning conversation failed: ' + msg);
+            });
+    }
 }
 
 class NavigationController {

+ 1 - 0
src/sass/sections/_conversation.scss

@@ -14,6 +14,7 @@
         .header-details {
             @include mouse-hand;
             overflow: hidden;
+            flex-grow: 1;
 
             & > *:first-child {
                 font-weight: bold;

+ 29 - 1
src/services/webclient.ts

@@ -23,7 +23,8 @@ import {StateService as UiStateService} from '@uirouter/angularjs';
 
 import * as msgpack from 'msgpack-lite';
 import {
-    arraysAreEqual, hasFeature, hasValue, hexToU8a, msgpackVisualizer, randomString, stringToUtf8a, u8aToHex,
+    arraysAreEqual, copyDeep, hasFeature, hasValue, hexToU8a,
+    msgpackVisualizer, randomString, stringToUtf8a, u8aToHex,
 } from '../helpers';
 import {isContactReceiver, isDistributionListReceiver, isGroupReceiver, isValidReceiverType} from '../typeguards';
 import {BatteryStatusService} from './battery';
@@ -1817,6 +1818,33 @@ export class WebClientService {
         return promise;
     }
 
+    /*
+     * Modify a conversation.
+     */
+    public modifyConversation(conversation: threema.Conversation, isPinned?: boolean): Promise<null> {
+        const DATA_STARRED = 'isStarred';
+
+        // Prepare payload data
+        const args = {
+            [WebClientService.ARGUMENT_RECEIVER_TYPE]: conversation.type,
+            [WebClientService.ARGUMENT_RECEIVER_ID]: conversation.id,
+        };
+        const data = {};
+        if (hasValue(isPinned)) {
+            data[DATA_STARRED] = isPinned;
+        }
+
+        // If no changes happened, resolve the promise immediately.
+        if (Object.keys(data).length === 0) {
+            this.$log.warn(this.logTag, 'Trying to modify conversation without any changes');
+            return Promise.resolve(null);
+        }
+
+        // Send update
+        const subType = WebClientService.SUB_TYPE_CONVERSATION;
+        return this.sendUpdateWireMessage(subType, true, args, data);
+    }
+
     /**
      * Create a group receiver.
      */

+ 4 - 0
src/threema/container.ts

@@ -331,6 +331,10 @@ export class Conversations implements threema.Container.Conversations {
                 if (returnOld) {
                     previousConversation = copyShallow(this.conversations[i]);
                 }
+
+                // Explicitly set defaults, to be able to override old values
+                setDefault(conversation, 'isStarred', false);
+
                 // Copy properties from new conversation to old conversation
                 Object.assign(this.conversations[i], conversation);