Browse Source

Merge pull request #4392 from nextcloud/restore_user_status

Restore user status
Sowjanya Kota 5 months ago
parent
commit
b361d56250

+ 1 - 0
app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusClickListener.kt

@@ -10,4 +10,5 @@ import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
 
 interface PredefinedStatusClickListener {
     fun onClick(predefinedStatus: PredefinedStatus)
+    fun revertStatus()
 }

+ 3 - 2
app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusListAdapter.kt

@@ -16,7 +16,8 @@ import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
 
 class PredefinedStatusListAdapter(
     private val clickListener: PredefinedStatusClickListener,
-    val context: Context
+    val context: Context,
+    var isBackupStatusAvailable: Boolean
 ) : RecyclerView.Adapter<PredefinedStatusViewHolder>() {
     internal var list: List<PredefinedStatus> = emptyList()
 
@@ -26,7 +27,7 @@ class PredefinedStatusListAdapter(
     }
 
     override fun onBindViewHolder(holder: PredefinedStatusViewHolder, position: Int) {
-        holder.bind(list[position], clickListener, context)
+        holder.bind(list[position], clickListener, context, isBackupStatusAvailable)
     }
 
     override fun getItemCount(): Int {

+ 17 - 1
app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusViewHolder.kt

@@ -8,6 +8,7 @@
 package com.nextcloud.talk.adapters
 
 import android.content.Context
+import android.view.View
 import androidx.recyclerview.widget.RecyclerView
 import com.nextcloud.talk.R
 import com.nextcloud.talk.databinding.PredefinedStatusBinding
@@ -16,9 +17,15 @@ import com.nextcloud.talk.utils.DisplayUtils
 
 private const val ONE_SECOND_IN_MILLIS = 1000
 
+@Suppress("DEPRECATION")
 class PredefinedStatusViewHolder(private val binding: PredefinedStatusBinding) : RecyclerView.ViewHolder(binding.root) {
 
-    fun bind(status: PredefinedStatus, clickListener: PredefinedStatusClickListener, context: Context) {
+    fun bind(
+        status: PredefinedStatus,
+        clickListener: PredefinedStatusClickListener,
+        context: Context,
+        isBackupStatusAvailable: Boolean
+    ) {
         binding.root.setOnClickListener { clickListener.onClick(status) }
         binding.icon.text = status.icon
         binding.name.text = status.message
@@ -40,5 +47,14 @@ class PredefinedStatusViewHolder(private val binding: PredefinedStatusBinding) :
                 }
             }
         }
+        if (isBackupStatusAvailable) {
+            binding.resetStatusButton.visibility = if (position == 0) View.VISIBLE else View.GONE
+            if (position == 0) {
+                binding.clearAt.text = context.getString(R.string.previously_set)
+            }
+            binding.resetStatusButton.setOnClickListener {
+                clickListener.revertStatus()
+            }
+        }
     }
 }

+ 6 - 4
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -286,6 +286,9 @@ public interface NcApi {
     @GET
     Observable<UserProfileOverall> getUserData(@Header("Authorization") String authorization, @Url String url);
 
+    @DELETE
+    Observable<GenericOverall> revertStatus(@Header("Authentication") String authorization, @Url String url);
+
     @FormUrlEncoded
     @PUT
     Observable<GenericOverall> setUserData(@Header("Authorization") String authorization,
@@ -552,13 +555,12 @@ public interface NcApi {
     @GET
     Observable<RoomsOverall> getOpenConversations(@Header("Authorization") String authorization, @Url String url);
 
-
-    /*
-     * OCS Status API
-     */
     @GET
     Observable<StatusOverall> status(@Header("Authorization") String authorization, @Url String url);
 
+    @GET
+    Observable<StatusOverall> backupStatus(@Header("Authorization") String authorization, @Url String url);
+
     @GET
     Observable<ResponseBody> getPredefinedStatuses(@Header("Authorization") String authorization, @Url String url);
 

+ 3 - 1
app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.kt

@@ -19,9 +19,11 @@ import kotlinx.serialization.Serializable
 data class UserStatusCapability(
     @JsonField(name = ["enabled"])
     var enabled: Boolean,
+    @JsonField(name = ["restore"])
+    var restore: Boolean,
     @JsonField(name = ["supports_emoji"])
     var supportsEmoji: Boolean
 ) : Parcelable {
     // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
-    constructor() : this(false, false)
+    constructor() : this(false, false, false)
 }

+ 1 - 1
app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatus.kt

@@ -18,7 +18,7 @@ data class PredefinedStatus(
     @JsonField(name = ["id"])
     var id: String,
     @JsonField(name = ["icon"])
-    var icon: String,
+    var icon: String?,
     @JsonField(name = ["message"])
     var message: String,
     @JsonField(name = ["clearAt"])

+ 141 - 32
app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt

@@ -39,11 +39,13 @@ import com.nextcloud.talk.databinding.DialogSetStatusBinding
 import com.nextcloud.talk.models.json.generic.GenericOverall
 import com.nextcloud.talk.models.json.status.ClearAt
 import com.nextcloud.talk.models.json.status.Status
+import com.nextcloud.talk.models.json.status.StatusOverall
 import com.nextcloud.talk.models.json.status.StatusType
 import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus
 import com.nextcloud.talk.models.json.status.predefined.PredefinedStatusOverall
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.CapabilitiesUtil.isRestoreStatusAvailable
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
 import com.vanniktech.emoji.EmojiPopup
@@ -54,6 +56,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.disposables.Disposable
 import io.reactivex.schedulers.Schedulers
 import okhttp3.ResponseBody
+import retrofit2.HttpException
 import java.util.Calendar
 import java.util.Locale
 import javax.inject.Inject
@@ -86,12 +89,16 @@ class SetStatusDialogFragment :
 
     private var currentUser: User? = null
     private var currentStatus: Status? = null
+    private lateinit var backupStatus: Status
 
     val predefinedStatusesList = ArrayList<PredefinedStatus>()
 
+    private val disposables: MutableList<Disposable> = ArrayList()
+
     private lateinit var adapter: PredefinedStatusListAdapter
     private var clearAt: Long? = null
     private lateinit var popup: EmojiPopup
+    private var isBackupStatusAvailable = false
 
     @Inject
     lateinit var ncApi: NcApi
@@ -114,42 +121,88 @@ class SetStatusDialogFragment :
             currentStatus = it.getParcelable(ARG_CURRENT_STATUS_PARAM)
 
             credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)!!
-            ncApi.getPredefinedStatuses(credentials, ApiUtils.getUrlForPredefinedStatuses(currentUser?.baseUrl!!))
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(object : Observer<ResponseBody> {
+            if (isRestoreStatusAvailable(currentUser!!)) {
+                checkBackupStatus()
+            }
+            fetchPredefinedStatuses()
+        }
+    }
 
-                    override fun onSubscribe(d: Disposable) {
-                        // unused atm
+    private fun fetchPredefinedStatuses() {
+        ncApi.getPredefinedStatuses(credentials, ApiUtils.getUrlForPredefinedStatuses(currentUser?.baseUrl!!))
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<ResponseBody> {
+                override fun onSubscribe(d: Disposable) {
+                    disposables.add(d)
+                }
+
+                @SuppressLint("NotifyDataSetChanged")
+                override fun onNext(responseBody: ResponseBody) {
+                    val predefinedStatusOverall: PredefinedStatusOverall = LoganSquare.parse(
+                        responseBody.string(),
+                        PredefinedStatusOverall::class.java
+                    )
+                    predefinedStatusOverall.ocs?.data?.let { predefinedStatusesList.addAll(it) }
+
+                    if (currentStatus?.messageIsPredefined == true && currentStatus?.messageId?.isNotEmpty() == true) {
+                        val messageId = currentStatus!!.messageId
+                        selectedPredefinedStatus = predefinedStatusesList.firstOrNull { ps -> messageId == ps.id }
                     }
 
-                    override fun onNext(responseBody: ResponseBody) {
-                        val predefinedStatusOverall: PredefinedStatusOverall = LoganSquare.parse(
-                            responseBody
-                                .string(),
-                            PredefinedStatusOverall::class.java
-                        )
-                        predefinedStatusOverall.ocs?.data?.let { it1 -> predefinedStatusesList.addAll(it1) }
+                    adapter.notifyDataSetChanged()
+                }
 
-                        if (currentStatus?.messageIsPredefined == true &&
-                            currentStatus?.messageId?.isNotEmpty() == true
-                        ) {
-                            val messageId = currentStatus!!.messageId
-                            selectedPredefinedStatus = predefinedStatusesList.firstOrNull { ps -> messageId == ps.id }
-                        }
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "Error while fetching predefined statuses", e)
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    private fun checkBackupStatus() {
+        ncApi.backupStatus(credentials, ApiUtils.getUrlForBackupStatus(currentUser?.baseUrl!!, currentUser?.userId!!))
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<StatusOverall> {
 
+                override fun onSubscribe(d: Disposable) {
+                    disposables.add(d)
+                }
+
+                @SuppressLint("NotifyDataSetChanged")
+                override fun onNext(statusOverall: StatusOverall) {
+                    if (statusOverall.ocs?.meta?.statusCode == HTTP_STATUS_CODE_OK) {
+                        backupStatus = statusOverall.ocs?.data!!
+                        isBackupStatusAvailable = true
+                        val backupPredefinedStatus = PredefinedStatus(
+                            backupStatus.userId!!,
+                            backupStatus.icon,
+                            backupStatus.message!!,
+                            ClearAt(type = "period", time = backupStatus.clearAt.toString())
+                        )
+                        binding.automaticStatus.visibility = View.VISIBLE
+                        adapter.isBackupStatusAvailable = true
+                        predefinedStatusesList.add(0, backupPredefinedStatus)
                         adapter.notifyDataSetChanged()
                     }
+                }
 
-                    override fun onError(e: Throwable) {
-                        Log.e(TAG, "Error while fetching predefined statuses", e)
+                override fun onError(e: Throwable) {
+                    if (e is HttpException && e.code() == HTTP_STATUS_CODE_NOT_FOUND) {
+                        Log.d(TAG, "User does not have a backup status set")
+                    } else {
+                        Log.e(TAG, "Error while getting user backup status", e)
                     }
+                }
 
-                    override fun onComplete() {
-                        // unused atm
-                    }
-                })
-        }
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
     }
 
     @SuppressLint("InflateParams")
@@ -168,7 +221,7 @@ class SetStatusDialogFragment :
 
         setupCurrentStatus()
 
-        adapter = PredefinedStatusListAdapter(this, requireContext())
+        adapter = PredefinedStatusListAdapter(this, requireContext(), isBackupStatusAvailable)
         adapter.list = predefinedStatusesList
 
         binding.predefinedStatusList.adapter = adapter
@@ -183,7 +236,6 @@ class SetStatusDialogFragment :
         binding.clearStatus.setOnClickListener { clearStatus() }
         binding.setStatus.setOnClickListener { setStatusMessage() }
         binding.emoji.setOnClickListener { openEmojiPopup() }
-
         popup = EmojiPopup(
             rootView = view,
             editText = binding.emoji,
@@ -244,6 +296,44 @@ class SetStatusDialogFragment :
         }
     }
 
+    override fun revertStatus() {
+        if (isRestoreStatusAvailable(currentUser!!)) {
+            ncApi.revertStatus(
+                credentials,
+                ApiUtils.getUrlForRevertStatus(currentUser?.baseUrl!!, currentStatus?.messageId)
+            )
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(object : Observer<GenericOverall> {
+
+                    override fun onSubscribe(d: Disposable) {
+                        disposables.add(d)
+                    }
+
+                    @SuppressLint("NotifyDataSetChanged")
+                    override fun onNext(genericOverall: GenericOverall) {
+                        Log.d(TAG, "$genericOverall")
+                        if (genericOverall.ocs?.meta?.statusCode == HTTP_STATUS_CODE_OK) {
+                            binding.automaticStatus.visibility = View.GONE
+                            adapter.isBackupStatusAvailable = false
+                            predefinedStatusesList.removeAt(0)
+                            adapter.notifyDataSetChanged()
+                            currentStatus = backupStatus
+                            setupCurrentStatus()
+                            dismiss()
+                        }
+                    }
+                    override fun onError(e: Throwable) {
+                        Log.e(TAG, "Failed to revert user status", e)
+                    }
+
+                    override fun onComplete() {
+                        // unused atm
+                    }
+                })
+        }
+    }
+
     private fun setupGeneralStatusOptions() {
         binding.onlineStatus.setOnClickListener { setStatus(StatusType.ONLINE) }
         binding.dndStatus.setOnClickListener { setStatus(StatusType.DND) }
@@ -357,7 +447,7 @@ class SetStatusDialogFragment :
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread()).subscribe(object : Observer<GenericOverall> {
                 override fun onSubscribe(d: Disposable) {
-                    // unused atm
+                    disposables.add(d)
                 }
 
                 override fun onNext(statusOverall: GenericOverall) {
@@ -384,7 +474,7 @@ class SetStatusDialogFragment :
             )
             .observeOn(AndroidSchedulers.mainThread()).subscribe(object : Observer<GenericOverall> {
                 override fun onSubscribe(d: Disposable) {
-                    // unused atm
+                    disposables.add(d)
                 }
 
                 override fun onNext(statusOverall: GenericOverall) {
@@ -487,7 +577,9 @@ class SetStatusDialogFragment :
             )
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())?.subscribe(object : Observer<GenericOverall> {
-                    override fun onSubscribe(d: Disposable) = Unit
+                    override fun onSubscribe(d: Disposable) {
+                        disposables.add(d)
+                    }
 
                     override fun onNext(t: GenericOverall) {
                         Log.d(TAG, "PredefinedStatusMessage successfully set")
@@ -498,7 +590,9 @@ class SetStatusDialogFragment :
                         Log.e(TAG, "failed to set PredefinedStatusMessage", e)
                     }
 
-                    override fun onComplete() = Unit
+                    override fun onComplete() {
+                        // unused atm
+                    }
                 })
         }
     }
@@ -544,11 +638,26 @@ class SetStatusDialogFragment :
         }
     }
 
+    private fun dispose() {
+        for (i in disposables.indices) {
+            if (!disposables[i].isDisposed) {
+                disposables[i].dispose()
+            }
+        }
+    }
+
+    override fun onDestroy() {
+        dispose()
+        super.onDestroy()
+    }
+
     /**
      * Fragment creator
      */
     companion object {
         private val TAG = SetStatusDialogFragment::class.simpleName
+        private const val HTTP_STATUS_CODE_OK = 200
+        private const val HTTP_STATUS_CODE_NOT_FOUND = 404
 
         @JvmStatic
         fun newInstance(status: Status): SetStatusDialogFragment {

+ 9 - 0
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt

@@ -482,6 +482,15 @@ object ApiUtils {
         return "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/user_status"
     }
 
+    @JvmStatic
+    fun getUrlForBackupStatus(baseUrl: String, userId: String): String {
+        return "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/statuses/_$userId"
+    }
+
+    fun getUrlForRevertStatus(baseUrl: String, messageId: String?): String {
+        return "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/user_status/revert/$messageId"
+    }
+
     fun getUrlForSetStatusType(baseUrl: String): String {
         return getUrlForStatus(baseUrl) + "/status"
     }

+ 4 - 0
app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt

@@ -274,6 +274,10 @@ object CapabilitiesUtil {
         user.capabilities?.userStatusCapability?.enabled == true &&
             user.capabilities?.userStatusCapability?.supportsEmoji == true
 
+    fun isRestoreStatusAvailable(user: User): Boolean {
+        return user.capabilities?.userStatusCapability?.restore == true
+    }
+
     // endregion
 
     private val TAG = CapabilitiesUtil::class.java.simpleName

+ 10 - 0
app/src/main/res/layout/dialog_set_status.xml

@@ -386,6 +386,16 @@
 
         </LinearLayout>
 
+        <TextView
+            android:id="@+id/automatic_status"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:padding ="10dp"
+            android:text= "@string/automatic_status_set"
+            android:visibility="gone"
+            tools:visibility="visible">
+        </TextView>
+
         <androidx.recyclerview.widget.RecyclerView
             android:id="@+id/predefinedStatusList"
             android:layout_width="match_parent"

+ 42 - 24
app/src/main/res/layout/predefined_status.xml

@@ -9,7 +9,8 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="48dp">
+    android:layout_height="wrap_content"
+    android:orientation="horizontal">
 
     <TextView
         android:id="@+id/icon"
@@ -19,30 +20,47 @@
         android:textSize="25sp"
         tools:text="📆" />
 
-    <TextView
-        android:id="@+id/name"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:gravity="center_vertical"
-        android:textAppearance="?android:attr/textAppearanceListItem"
-        tools:text="In a meeting" />
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_weight="1"
+        android:paddingStart="8dp"
+        android:paddingEnd="8dp"
+        android:paddingTop="8dp"
+        android:paddingBottom="4dp">
 
-    <TextView
-        android:id="@+id/divider"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:layout_margin="@dimen/standard_half_margin"
-        android:gravity="center_vertical"
-        android:text="@string/divider"
-        android:textAppearance="?android:attr/textAppearanceListItem"
-        android:textColor="?android:attr/textColorSecondary" />
+        <TextView
+            android:id="@+id/name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="start"
+            android:textAppearance="?android:attr/textAppearanceListItem"
+            tools:text="In a meeting" />
 
-    <TextView
-        android:id="@+id/clearAt"
+        <TextView
+            android:id="@+id/clearAt"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="start"
+            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+            android:textColor="?android:attr/textColorSecondary"
+            tools:text="an hour" />
+    </LinearLayout>
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/reset_status_button"
         android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:gravity="center_vertical"
-        android:textAppearance="?android:attr/textAppearanceListItem"
-        android:textColor="?android:attr/textColorSecondary"
-        tools:text="an hour" />
+        android:layout_height="wrap_content"
+        android:minWidth="48dp"
+        android:minHeight="48dp"
+        android:layout_gravity="center"
+        android:backgroundTint="@color/secondary_button_background"
+        android:textColor="@android:color/black"
+        android:text= "@string/reset_status"
+        android:textSize="14sp"
+        android:padding="8dp"
+        android:visibility="gone"
+        tools:visibility="visible"/>
+
 </LinearLayout>

+ 1 - 0
app/src/main/res/values/colors.xml

@@ -91,5 +91,6 @@
 
     <color name="icon_on_bg_default">#99000000</color>
     <color name="badge_color">#EF3B02</color>
+    <color name="secondary_button_background">#DBE2E9</color>
 
 </resources>

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

@@ -381,6 +381,7 @@ How to translate with transifex:
     <string name="fourHours">4 hours</string>
     <string name="thisWeek">This week</string>
     <string name="secondsAgo">seconds ago</string>
+    <string name="reset_status">Reset status</string>
 
     <!-- Conversations List-->
     <string name="nc_new_mention">Unread mentions</string>
@@ -826,5 +827,8 @@ How to translate with transifex:
     <string name="archived">Archived</string>
     <string name="archive_hint">Once a conversation is archived, it will be hidden by default. Select the filter \'Archived\' to view archived conversations. Direct mentions will still be received.</string>
     <string name="unarchive_hint">Once a conversation is unarchived, it will be shown by default again.</string>
+    <string name="previously_set">Previously set</string>
     <string name="conversation_read_only_failed">Failed to set conversation Read-only</string>
+    <string name="status_reverted">Status Reverted</string>
+    <string name="automatic_status_set">Your status was set automatically</string>
 </resources>