Prechádzať zdrojové kódy

Merge pull request #1752 from nextcloud/feature/1464/markAsRead

mark message as unread / conversation as read
Marcel Hibbe 3 rokov pred
rodič
commit
5287fc2a96

+ 7 - 0
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -429,4 +429,11 @@ public interface NcApi {
 
     @GET
     Observable<HoverCardOverall> hoverCard(@Header("Authorization") String authorization, @Url String url);
+
+    // Url is: /api/{apiVersion}/chat/{token}/read
+    @FormUrlEncoded
+    @POST
+    Observable<GenericOverall> setChatReadMarker(@Header("Authorization") String authorization,
+                                                 @Url String url,
+                                                 @Field("lastReadMessage") int lastReadMessage);
 }

+ 71 - 1
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -2049,6 +2049,22 @@ class ChatController(args: Bundle) :
 
             var countGroupedMessages = 0
             if (!isFromTheFuture) {
+                var previousMessageId = NO_PREVIOUS_MESSAGE_ID
+                for (i in chatMessageList.indices.reversed()) {
+                    val chatMessage = chatMessageList[i]
+
+                    if (previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
+                        chatMessage.previousMessageId = previousMessageId
+                    } else if (adapter?.isEmpty != true) {
+                        if (adapter!!.items[0].item is ChatMessage) {
+                            chatMessage.previousMessageId = (adapter!!.items[0].item as ChatMessage).jsonMessageId
+                        } else if (adapter!!.items.size > 1 && adapter!!.items[1].item is ChatMessage) {
+                            chatMessage.previousMessageId = (adapter!!.items[1].item as ChatMessage).jsonMessageId
+                        }
+                    }
+
+                    previousMessageId = chatMessage.jsonMessageId
+                }
 
                 for (i in chatMessageList.indices) {
                     if (chatMessageList.size > i + 1) {
@@ -2092,6 +2108,23 @@ class ChatController(args: Bundle) :
                 val isThereANewNotice =
                     shouldAddNewMessagesNotice || adapter?.getMessagePositionByIdInReverse("-1") != -1
 
+                var previousMessageId = NO_PREVIOUS_MESSAGE_ID
+                for (i in chatMessageList.indices.reversed()) {
+                    val chatMessageItem = chatMessageList[i]
+
+                    if (previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
+                        chatMessageItem.previousMessageId = previousMessageId
+                    } else if (adapter?.isEmpty != true) {
+                        if (adapter!!.items[0].item is ChatMessage) {
+                            chatMessageItem.previousMessageId = (adapter!!.items[0].item as ChatMessage).jsonMessageId
+                        } else if (adapter!!.items.size > 1 && adapter!!.items[1].item is ChatMessage) {
+                            chatMessageItem.previousMessageId = (adapter!!.items[1].item as ChatMessage).jsonMessageId
+                        }
+                    }
+
+                    previousMessageId = chatMessageItem.jsonMessageId
+                }
+
                 for (i in chatMessageList.indices) {
                     chatMessage = chatMessageList[i]
 
@@ -2319,6 +2352,40 @@ class ChatController(args: Bundle) :
                         clipboardManager.setPrimaryClip(clipData)
                         true
                     }
+                    R.id.action_mark_as_unread -> {
+                        val chatMessage = message as ChatMessage?
+                        if (chatMessage!!.previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
+                            ncApi!!.setChatReadMarker(
+                                credentials,
+                                ApiUtils.getUrlForSetChatReadMarker(
+                                    ApiUtils.getChatApiVersion(conversationUser, intArrayOf(ApiUtils.APIv1)),
+                                    conversationUser?.baseUrl,
+                                    roomToken
+                                ),
+                                chatMessage.previousMessageId
+                            )
+                                .subscribeOn(Schedulers.io())
+                                .observeOn(AndroidSchedulers.mainThread())
+                                .subscribe(object : Observer<GenericOverall> {
+                                    override fun onSubscribe(d: Disposable) {
+                                        // unused atm
+                                    }
+
+                                    override fun onNext(t: GenericOverall) {
+                                        // unused atm
+                                    }
+
+                                    override fun onError(e: Throwable) {
+                                        Log.e(TAG, e.message, e)
+                                    }
+
+                                    override fun onComplete() {
+                                        // unused atm
+                                    }
+                                })
+                        }
+                        true
+                    }
                     R.id.action_forward_message -> {
                         val bundle = Bundle()
                         bundle.putBoolean(BundleKeys.KEY_FORWARD_MSG_FLAG, true)
@@ -2471,8 +2538,10 @@ class ChatController(args: Bundle) :
             menu.findItem(R.id.action_delete_message).isVisible = isShowMessageDeletionButton(message)
             menu.findItem(R.id.action_forward_message).isVisible =
                 ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getMessageType()
+            menu.findItem(R.id.action_mark_as_unread).isVisible = message.previousMessageId > NO_PREVIOUS_MESSAGE_ID &&
+                ChatMessage.MessageType.SYSTEM_MESSAGE != message.getMessageType()
             if (menu.hasVisibleItems()) {
-                if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                     setForceShowIcon(true)
                 }
                 show()
@@ -2723,5 +2792,6 @@ class ChatController(args: Bundle) :
         private const val SEMI_TRANSPARENT_INT: Int = 99
         private const val VOICE_MESSAGE_SEEKBAR_BASE: Int = 1000
         private const val SECOND: Long = 1000
+        private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
     }
 }

+ 7 - 1
app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java

@@ -74,6 +74,7 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
 
 @AutoInjector(NextcloudTalkApplication.class)
 public class CallMenuController extends BaseController implements FlexibleAdapter.OnItemClickListener {
+    public static final int ALL_MESSAGES_READ = 0;
     @BindView(R.id.recycler_view)
     RecyclerView recyclerView;
 
@@ -170,6 +171,12 @@ public class CallMenuController extends BaseController implements FlexibleAdapte
                                                                           R.color.grey_600)));
             }
 
+            if(conversation.unreadMessages > ALL_MESSAGES_READ && CapabilitiesUtil.canSetChatReadMarker(currentUser)) {
+                menuItems.add(new MenuItem(getResources().getString(R.string.nc_mark_as_read),
+                                           96,
+                                           ContextCompat.getDrawable(context, R.drawable.ic_eye)));
+            }
+
             if (conversation.isNameEditable(currentUser)) {
                 menuItems.add(new MenuItem(getResources().getString(R.string.nc_rename),
                                            2,
@@ -340,7 +347,6 @@ public class CallMenuController extends BaseController implements FlexibleAdapte
         }
 
         return null;
-
     }
 
     @Parcel

+ 15 - 1
app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.java

@@ -33,6 +33,7 @@ import android.widget.Button;
 import android.widget.ImageView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import com.bluelinelabs.conductor.RouterTransaction;
 import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
@@ -74,6 +75,7 @@ import io.reactivex.Observer;
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.disposables.Disposable;
 import io.reactivex.schedulers.Schedulers;
+import okhttp3.ResponseBody;
 import retrofit2.HttpException;
 import retrofit2.Response;
 
@@ -278,7 +280,8 @@ public class OperationsMenuController extends BaseController {
         }
 
         credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken());
-        int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[] {ApiUtils.APIv4, 1});
+        int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[] {ApiUtils.APIv4, ApiUtils.APIv1});
+        int chatApiVersion = ApiUtils.getChatApiVersion(currentUser, new int[] {ApiUtils.APIv1});
 
         switch (operationCode) {
             case 2:
@@ -480,6 +483,17 @@ public class OperationsMenuController extends BaseController {
                         });
 
                 break;
+            case 96:
+                ncApi.setChatReadMarker(credentials,
+                                        ApiUtils.getUrlForSetChatReadMarker(chatApiVersion,
+                                                                            currentUser.getBaseUrl(),
+                                                                            conversation.getToken()),
+                                        conversation.lastMessage.jsonMessageId)
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .retry(1)
+                    .subscribe(genericOperationsObserver);
+                break;
             case 97:
             case 98:
                 if (operationCode == 97) {

+ 4 - 0
app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java

@@ -75,6 +75,10 @@ public abstract class CapabilitiesUtil {
         return !hasSpreedFeatureCapability(user, "chat-replies");
     }
 
+    public static boolean canSetChatReadMarker(@Nullable UserEntity user) {
+        return hasSpreedFeatureCapability(user, "chat-read-marker");
+    }
+
     public static boolean hasSpreedFeatureCapability(@Nullable UserEntity user, String capabilityName) {
         if (user != null && user.getCapabilities() != null) {
             try {

+ 2 - 0
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java

@@ -62,6 +62,8 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
     public boolean isDeleted;
     @JsonField(name = "id")
     public int jsonMessageId;
+    @JsonIgnore
+    public int previousMessageId = -1;
     @JsonField(name = "token")
     public String token;
     // guests or users

+ 13 - 7
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -40,6 +40,8 @@ import androidx.annotation.Nullable;
 import okhttp3.Credentials;
 
 public class ApiUtils {
+    public static final int APIv1 = 1;
+    public static final int APIv2 = 2;
     public static final int APIv3 = 3;
     public static final int APIv4 = 4;
     private static final String TAG = "ApiUtils";
@@ -59,7 +61,7 @@ public class ApiUtils {
      */
     @Deprecated
     public static String getUrlForRemovingParticipantFromConversation(String baseUrl, String roomToken, boolean isGuest) {
-        String url = getUrlForParticipants(1, baseUrl, roomToken);
+        String url = getUrlForParticipants(APIv1, baseUrl, roomToken);
 
         if (isGuest) {
             url += "/guests";
@@ -121,7 +123,7 @@ public class ApiUtils {
     public static int getConversationApiVersion(UserEntity user, int[] versions) throws NoSupportedApiException {
         boolean hasApiV4 = false;
         for (int version : versions) {
-            hasApiV4 |= version == 4;
+            hasApiV4 |= version == APIv4;
         }
 
         if (!hasApiV4) {
@@ -135,11 +137,11 @@ public class ApiUtils {
             }
 
             // Fallback for old API versions
-            if ((version == 1 || version == 2)) {
+            if ((version == APIv1 || version == APIv2)) {
                 if (CapabilitiesUtil.hasSpreedFeatureCapability(user, "conversation-v2")) {
                     return version;
                 }
-                if (version == 1  &&
+                if (version == APIv1  &&
                         CapabilitiesUtil.hasSpreedFeatureCapability(user, "mention-flag") &&
                         !CapabilitiesUtil.hasSpreedFeatureCapability(user, "conversation-v4")) {
                     return version;
@@ -155,13 +157,13 @@ public class ApiUtils {
                 return version;
             }
 
-            if (version == 2 &&
+            if (version == APIv2 &&
                     CapabilitiesUtil.hasSpreedFeatureCapability(user, "sip-support") &&
                     !CapabilitiesUtil.hasSpreedFeatureCapability(user, "signaling-v3")) {
                 return version;
             }
 
-            if (version == 1 &&
+            if (version == APIv1 &&
                     !CapabilitiesUtil.hasSpreedFeatureCapability(user, "signaling-v3")) {
                 // Has no capability, we just assume it is always there when there is no v3 or later
                 return version;
@@ -172,7 +174,7 @@ public class ApiUtils {
 
     public static int getChatApiVersion(UserEntity user, int[] versions) throws NoSupportedApiException {
         for (int version : versions) {
-            if (version == 1 && CapabilitiesUtil.hasSpreedFeatureCapability(user, "chat-v2")) {
+            if (version == APIv1 && CapabilitiesUtil.hasSpreedFeatureCapability(user, "chat-v2")) {
                 // Do not question that chat-v2 capability shows the availability of api/v1/ endpoint *see no evil*
                 return version;
             }
@@ -406,4 +408,8 @@ public class ApiUtils {
 
     public static String getUrlForHoverCard(String baseUrl, String userId) { return baseUrl + ocsApiVersion +
         "/hovercard/v1/" + userId; }
+
+    public static String getUrlForSetChatReadMarker(int version, String baseUrl, String roomToken) {
+        return getUrlForChat(version, baseUrl, roomToken) + "/read";
+    }
 }

+ 25 - 0
app/src/main/res/drawable/ic_eye.xml

@@ -0,0 +1,25 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2021 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#757575"
+        android:pathData="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z" />
+</vector>

+ 26 - 0
app/src/main/res/drawable/ic_eye_off.xml

@@ -0,0 +1,26 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2021 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="@color/medium_emphasis_text"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#000"
+        android:pathData="M11.83,9L15,12.16C15,12.11 15,12.05 15,12A3,3 0 0,0 12,9C11.94,9 11.89,9 11.83,9M7.53,9.8L9.08,11.35C9.03,11.56 9,11.77 9,12A3,3 0 0,0 12,15C12.22,15 12.44,14.97 12.65,14.92L14.2,16.47C13.53,16.8 12.79,17 12,17A5,5 0 0,1 7,12C7,11.21 7.2,10.47 7.53,9.8M2,4.27L4.28,6.55L4.73,7C3.08,8.3 1.78,10 1,12C2.73,16.39 7,19.5 12,19.5C13.55,19.5 15.03,19.2 16.38,18.66L16.81,19.08L19.73,22L21,20.73L3.27,3M12,7A5,5 0 0,1 17,12C17,12.64 16.87,13.26 16.64,13.82L19.57,16.75C21.07,15.5 22.27,13.86 23,12C21.27,7.61 17,4.5 12,4.5C10.6,4.5 9.26,4.75 8,5.2L10.17,7.35C10.74,7.13 11.35,7 12,7Z" />
+</vector>

+ 6 - 0
app/src/main/res/menu/chat_message_menu.xml

@@ -8,6 +8,12 @@
         android:title="@string/nc_copy_message"
         app:showAsAction="always" />
 
+    <item
+        android:id="@+id/action_mark_as_unread"
+        android:icon="@drawable/ic_eye_off"
+        android:title="@string/nc_mark_as_unread"
+        app:showAsAction="always" />
+
     <item
         android:id="@+id/action_forward_message"
         android:icon="@drawable/ic_share_action"

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -187,6 +187,8 @@
     <string name="nc_new_conversation">New conversation</string>
     <string name="nc_join_via_link">Join with a link</string>
     <string name="nc_join_via_web">Join via web</string>
+    <string name="nc_mark_as_read">Mark as read</string>
+    <string name="nc_mark_as_unread">Mark as unread</string>
     <string name="nc_add_to_favorites">Add to favorites</string>
     <string name="nc_remove_from_favorites">Remove from favorites</string>