Merge pull request #4392 from nextcloud/restore_user_status

Restore user status
This commit is contained in:
Sowjanya Kota 2024-11-14 17:44:54 +01:00 committed by GitHub
commit b361d56250
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 243 additions and 66 deletions

View File

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

View File

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

View File

@ -8,6 +8,7 @@
package com.nextcloud.talk.adapters package com.nextcloud.talk.adapters
import android.content.Context import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.databinding.PredefinedStatusBinding import com.nextcloud.talk.databinding.PredefinedStatusBinding
@ -16,9 +17,15 @@ import com.nextcloud.talk.utils.DisplayUtils
private const val ONE_SECOND_IN_MILLIS = 1000 private const val ONE_SECOND_IN_MILLIS = 1000
@Suppress("DEPRECATION")
class PredefinedStatusViewHolder(private val binding: PredefinedStatusBinding) : RecyclerView.ViewHolder(binding.root) { 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.root.setOnClickListener { clickListener.onClick(status) }
binding.icon.text = status.icon binding.icon.text = status.icon
binding.name.text = status.message 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()
}
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -482,6 +482,15 @@ object ApiUtils {
return "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/user_status" 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 { fun getUrlForSetStatusType(baseUrl: String): String {
return getUrlForStatus(baseUrl) + "/status" return getUrlForStatus(baseUrl) + "/status"
} }

View File

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

View File

@ -386,6 +386,16 @@
</LinearLayout> </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 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/predefinedStatusList" android:id="@+id/predefinedStatusList"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -9,7 +9,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp"> android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView <TextView
android:id="@+id/icon" android:id="@+id/icon"
@ -19,30 +20,47 @@
android:textSize="25sp" android:textSize="25sp"
tools:text="📆" /> tools:text="📆" />
<TextView <LinearLayout
android:id="@+id/name" android:layout_width="0dp"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="match_parent" android:orientation="vertical"
android:gravity="center_vertical" android:layout_weight="1"
android:textAppearance="?android:attr/textAppearanceListItem" android:paddingStart="8dp"
tools:text="In a meeting" /> android:paddingEnd="8dp"
android:paddingTop="8dp"
android:paddingBottom="4dp">
<TextView <TextView
android:id="@+id/divider" android:id="@+id/name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_half_margin" android:gravity="start"
android:gravity="center_vertical" android:textAppearance="?android:attr/textAppearanceListItem"
android:text="@string/divider" tools:text="In a meeting" />
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary" />
<TextView <TextView
android:id="@+id/clearAt" 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_width="wrap_content"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:gravity="center_vertical" android:minWidth="48dp"
android:textAppearance="?android:attr/textAppearanceListItem" android:minHeight="48dp"
android:textColor="?android:attr/textColorSecondary" android:layout_gravity="center"
tools:text="an hour" /> 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> </LinearLayout>

View File

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

View File

@ -381,6 +381,7 @@ How to translate with transifex:
<string name="fourHours">4 hours</string> <string name="fourHours">4 hours</string>
<string name="thisWeek">This week</string> <string name="thisWeek">This week</string>
<string name="secondsAgo">seconds ago</string> <string name="secondsAgo">seconds ago</string>
<string name="reset_status">Reset status</string>
<!-- Conversations List--> <!-- Conversations List-->
<string name="nc_new_mention">Unread mentions</string> <string name="nc_new_mention">Unread mentions</string>
@ -826,5 +827,8 @@ How to translate with transifex:
<string name="archived">Archived</string> <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="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="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="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> </resources>