diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 79b25739b..b568dd210 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -429,4 +429,11 @@ public interface NcApi { @GET Observable hoverCard(@Header("Authorization") String authorization, @Url String url); + + // Url is: /api/{apiVersion}/chat/{token}/read + @FormUrlEncoded + @POST + Observable setChatReadMarker(@Header("Authorization") String authorization, + @Url String url, + @Field("lastReadMessage") int lastReadMessage); } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index 55295ca6a..b8f72cab6 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/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 { + 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 } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java index 42b367b67..a958de538 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java +++ b/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 diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.java b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.java index d084ee654..3be1a2537 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.java +++ b/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) { diff --git a/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java b/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java index bd4fe3952..b5ba0a04b 100644 --- a/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java +++ b/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 { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java index ce3451ad4..247ea33b9 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java +++ b/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 diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index 413c8c72a..6b0be29d0 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/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"; + } } diff --git a/app/src/main/res/drawable/ic_eye.xml b/app/src/main/res/drawable/ic_eye.xml new file mode 100644 index 000000000..b34a2006a --- /dev/null +++ b/app/src/main/res/drawable/ic_eye.xml @@ -0,0 +1,25 @@ + + + + diff --git a/app/src/main/res/drawable/ic_eye_off.xml b/app/src/main/res/drawable/ic_eye_off.xml new file mode 100644 index 000000000..f5e96aba4 --- /dev/null +++ b/app/src/main/res/drawable/ic_eye_off.xml @@ -0,0 +1,26 @@ + + + + diff --git a/app/src/main/res/menu/chat_message_menu.xml b/app/src/main/res/menu/chat_message_menu.xml index 17fe7420c..3f4cdad81 100644 --- a/app/src/main/res/menu/chat_message_menu.xml +++ b/app/src/main/res/menu/chat_message_menu.xml @@ -8,6 +8,12 @@ android:title="@string/nc_copy_message" app:showAsAction="always" /> + + New conversation Join with a link Join via web + Mark as read + Mark as unread Add to favorites Remove from favorites