mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-19 11:39:42 +01:00
Merge pull request #4333 from nextcloud/issue-4257-archive-conversation
Archived Conversations 🗃️
This commit is contained in:
commit
085711d077
@ -2,7 +2,7 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"identityHash": "bc802cadfdef41d3eb94ffbb0729eb89",
|
"identityHash": "7edb537b6987d0de6586a6760c970958",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "User",
|
"tableName": "User",
|
||||||
@ -138,7 +138,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "Conversations",
|
"tableName": "Conversations",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "internalId",
|
"fieldPath": "internalId",
|
||||||
@ -409,6 +409,12 @@
|
|||||||
"columnName": "unreadMessages",
|
"columnName": "unreadMessages",
|
||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hasArchived",
|
||||||
|
"columnName": "hasArchived",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
@ -713,7 +719,7 @@
|
|||||||
"views": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bc802cadfdef41d3eb94ffbb0729eb89')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7edb537b6987d0de6586a6760c970958')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -110,4 +110,10 @@ interface NcApiCoroutines {
|
|||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
suspend fun deleteConversationAvatar(@Header("Authorization") authorization: String, @Url url: String): RoomOverall
|
suspend fun deleteConversationAvatar(@Header("Authorization") authorization: String, @Url url: String): RoomOverall
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun archiveConversation(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
suspend fun unarchiveConversation(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,10 @@ import android.view.View
|
|||||||
import android.view.View.GONE
|
import android.view.View.GONE
|
||||||
import android.view.View.VISIBLE
|
import android.view.View.VISIBLE
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.fragment.app.FragmentTransaction
|
import androidx.fragment.app.FragmentTransaction
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
@ -86,6 +88,7 @@ import io.reactivex.Observer
|
|||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
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 kotlinx.coroutines.launch
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
@ -756,6 +759,40 @@ class ConversationInfoActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!CapabilitiesUtil.isArchiveConversationsAvailable(spreedCapabilities)) {
|
||||||
|
binding.archiveConversationBtn.visibility = GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.archiveConversationBtn.setOnClickListener {
|
||||||
|
this.lifecycleScope.launch {
|
||||||
|
if (conversation!!.hasArchived) {
|
||||||
|
viewModel.unarchiveConversation(conversationUser, conversationToken)
|
||||||
|
binding.archiveConversationIcon
|
||||||
|
.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.outline_archive_24, null))
|
||||||
|
binding.archiveConversationText.text = resources.getString(R.string.archive_conversation)
|
||||||
|
binding.archiveConversationTextHint.text = resources.getString(R.string.archive_hint)
|
||||||
|
} else {
|
||||||
|
viewModel.archiveConversation(conversationUser, conversationToken)
|
||||||
|
binding.archiveConversationIcon
|
||||||
|
.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_eye, null))
|
||||||
|
binding.archiveConversationText.text = resources.getString(R.string.unarchive_conversation)
|
||||||
|
binding.archiveConversationTextHint.text = resources.getString(R.string.unarchive_hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversation!!.hasArchived) {
|
||||||
|
binding.archiveConversationIcon
|
||||||
|
.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_eye, null))
|
||||||
|
binding.archiveConversationText.text = resources.getString(R.string.unarchive_conversation)
|
||||||
|
binding.archiveConversationTextHint.text = resources.getString(R.string.unarchive_hint)
|
||||||
|
} else {
|
||||||
|
binding.archiveConversationIcon
|
||||||
|
.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.outline_archive_24, null))
|
||||||
|
binding.archiveConversationText.text = resources.getString(R.string.archive_conversation)
|
||||||
|
binding.archiveConversationTextHint.text = resources.getString(R.string.archive_hint)
|
||||||
|
}
|
||||||
|
|
||||||
if (!isDestroyed) {
|
if (!isDestroyed) {
|
||||||
binding.dangerZoneOptions.visibility = VISIBLE
|
binding.dangerZoneOptions.visibility = VISIBLE
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import com.nextcloud.talk.models.domain.ConversationModel
|
|||||||
import com.nextcloud.talk.models.json.capabilities.SpreedCapability
|
import com.nextcloud.talk.models.json.capabilities.SpreedCapability
|
||||||
import com.nextcloud.talk.models.json.generic.GenericOverall
|
import com.nextcloud.talk.models.json.generic.GenericOverall
|
||||||
import com.nextcloud.talk.models.json.participants.TalkBan
|
import com.nextcloud.talk.models.json.participants.TalkBan
|
||||||
|
import com.nextcloud.talk.repositories.conversations.ConversationsRepository
|
||||||
import com.nextcloud.talk.utils.ApiUtils
|
import com.nextcloud.talk.utils.ApiUtils
|
||||||
import io.reactivex.Observer
|
import io.reactivex.Observer
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
@ -26,7 +27,8 @@ import io.reactivex.schedulers.Schedulers
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ConversationInfoViewModel @Inject constructor(
|
class ConversationInfoViewModel @Inject constructor(
|
||||||
private val chatNetworkDataSource: ChatNetworkDataSource
|
private val chatNetworkDataSource: ChatNetworkDataSource,
|
||||||
|
private val conversationsRepository: ConversationsRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
object LifeCycleObserver : DefaultLifecycleObserver {
|
object LifeCycleObserver : DefaultLifecycleObserver {
|
||||||
@ -200,6 +202,18 @@ class ConversationInfoViewModel @Inject constructor(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun archiveConversation(user: User, token: String) {
|
||||||
|
val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
|
||||||
|
val url = ApiUtils.getUrlForArchive(apiVersion, user.baseUrl, token)
|
||||||
|
conversationsRepository.archiveConversation(user.getCredentials(), url)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun unarchiveConversation(user: User, token: String) {
|
||||||
|
val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
|
||||||
|
val url = ApiUtils.getUrlForArchive(apiVersion, user.baseUrl, token)
|
||||||
|
conversationsRepository.unarchiveConversation(user.getCredentials(), url)
|
||||||
|
}
|
||||||
|
|
||||||
inner class GetRoomObserver : Observer<ConversationModel> {
|
inner class GetRoomObserver : Observer<ConversationModel> {
|
||||||
override fun onSubscribe(d: Disposable) {
|
override fun onSubscribe(d: Disposable) {
|
||||||
// unused atm
|
// unused atm
|
||||||
|
@ -210,7 +210,9 @@ class ConversationsListActivity :
|
|||||||
private var filterState =
|
private var filterState =
|
||||||
mutableMapOf(
|
mutableMapOf(
|
||||||
FilterConversationFragment.MENTION to false,
|
FilterConversationFragment.MENTION to false,
|
||||||
FilterConversationFragment.UNREAD to false
|
FilterConversationFragment.UNREAD to false,
|
||||||
|
FilterConversationFragment.ARCHIVE to false,
|
||||||
|
FilterConversationFragment.DEFAULT to true
|
||||||
)
|
)
|
||||||
val searchBehaviorSubject = BehaviorSubject.createDefault(false)
|
val searchBehaviorSubject = BehaviorSubject.createDefault(false)
|
||||||
private lateinit var accountIconBadge: BadgeDrawable
|
private lateinit var accountIconBadge: BadgeDrawable
|
||||||
@ -380,7 +382,7 @@ class ConversationsListActivity :
|
|||||||
sortConversations(conversationItemsWithHeader)
|
sortConversations(conversationItemsWithHeader)
|
||||||
|
|
||||||
// Filter Conversations
|
// Filter Conversations
|
||||||
if (!filterState.containsValue(true)) filterableConversationItems = conversationItems
|
if (!hasFilterEnabled()) filterableConversationItems = conversationItems
|
||||||
filterConversation()
|
filterConversation()
|
||||||
adapter!!.updateDataSet(filterableConversationItems, false)
|
adapter!!.updateDataSet(filterableConversationItems, false)
|
||||||
Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong())
|
Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong())
|
||||||
@ -395,6 +397,14 @@ class ConversationsListActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasFilterEnabled(): Boolean {
|
||||||
|
for ((k, v) in filterState) {
|
||||||
|
if (k != FilterConversationFragment.DEFAULT && v) return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
fun filterConversation() {
|
fun filterConversation() {
|
||||||
val accountId = UserIdUtils.getIdForUser(userManager.currentUser.blockingGet())
|
val accountId = UserIdUtils.getIdForUser(userManager.currentUser.blockingGet())
|
||||||
filterState[FilterConversationFragment.UNREAD] = (
|
filterState[FilterConversationFragment.UNREAD] = (
|
||||||
@ -413,22 +423,24 @@ class ConversationsListActivity :
|
|||||||
).blockingGet()?.value ?: ""
|
).blockingGet()?.value ?: ""
|
||||||
) == "true"
|
) == "true"
|
||||||
|
|
||||||
|
filterState[FilterConversationFragment.ARCHIVE] = (
|
||||||
|
arbitraryStorageManager.getStorageSetting(
|
||||||
|
accountId,
|
||||||
|
FilterConversationFragment.ARCHIVE,
|
||||||
|
""
|
||||||
|
).blockingGet()?.value ?: ""
|
||||||
|
) == "true"
|
||||||
|
|
||||||
val newItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
|
val newItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
|
||||||
if (filterState[FilterConversationFragment.UNREAD] == false &&
|
val items = conversationItems
|
||||||
filterState[FilterConversationFragment.MENTION] == false
|
for (i in items) {
|
||||||
) {
|
val conversation = (i as ConversationItem).model
|
||||||
adapter!!.updateDataSet(conversationItems, true)
|
if (filter(conversation)) {
|
||||||
} else {
|
newItems.add(i)
|
||||||
val items = conversationItems
|
|
||||||
for (i in items) {
|
|
||||||
val conversation = (i as ConversationItem).model
|
|
||||||
if (filter(conversation)) {
|
|
||||||
newItems.add(i)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
adapter!!.updateDataSet(newItems, true)
|
|
||||||
setFilterableItems(newItems)
|
|
||||||
}
|
}
|
||||||
|
adapter!!.updateDataSet(newItems, true)
|
||||||
|
setFilterableItems(newItems)
|
||||||
|
|
||||||
updateFilterConversationButtonColor()
|
updateFilterConversationButtonColor()
|
||||||
}
|
}
|
||||||
@ -449,10 +461,19 @@ class ConversationsListActivity :
|
|||||||
)
|
)
|
||||||
|
|
||||||
FilterConversationFragment.UNREAD -> result = result && (conversation.unreadMessages > 0)
|
FilterConversationFragment.UNREAD -> result = result && (conversation.unreadMessages > 0)
|
||||||
|
|
||||||
|
FilterConversationFragment.DEFAULT -> {
|
||||||
|
result = if (filterState[FilterConversationFragment.ARCHIVE] == true) {
|
||||||
|
result && conversation.hasArchived
|
||||||
|
} else {
|
||||||
|
result && !conversation.hasArchived
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Conversation: ${conversation.name} Result: $result")
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -649,7 +670,7 @@ class ConversationsListActivity :
|
|||||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
initSearchDisposable()
|
initSearchDisposable()
|
||||||
adapter!!.setHeadersShown(true)
|
adapter!!.setHeadersShown(true)
|
||||||
if (!filterState.containsValue(true)) filterableConversationItems = searchableConversationItems
|
if (!hasFilterEnabled()) filterableConversationItems = searchableConversationItems
|
||||||
adapter!!.updateDataSet(filterableConversationItems, false)
|
adapter!!.updateDataSet(filterableConversationItems, false)
|
||||||
adapter!!.showAllHeaders()
|
adapter!!.showAllHeaders()
|
||||||
binding.swipeRefreshLayoutView?.isEnabled = false
|
binding.swipeRefreshLayoutView?.isEnabled = false
|
||||||
@ -659,7 +680,7 @@ class ConversationsListActivity :
|
|||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
adapter!!.setHeadersShown(false)
|
adapter!!.setHeadersShown(false)
|
||||||
if (!filterState.containsValue(true)) filterableConversationItems = conversationItemsWithHeader
|
if (!hasFilterEnabled()) filterableConversationItems = conversationItemsWithHeader
|
||||||
adapter!!.updateDataSet(filterableConversationItems, false)
|
adapter!!.updateDataSet(filterableConversationItems, false)
|
||||||
adapter!!.hideAllHeaders()
|
adapter!!.hideAllHeaders()
|
||||||
if (searchHelper != null) {
|
if (searchHelper != null) {
|
||||||
@ -1826,7 +1847,7 @@ class ConversationsListActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateFilterConversationButtonColor() {
|
fun updateFilterConversationButtonColor() {
|
||||||
if (filterState.containsValue(true)) {
|
if (hasFilterEnabled()) {
|
||||||
binding.filterConversationsButton.let { viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) }
|
binding.filterConversationsButton.let { viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) }
|
||||||
} else {
|
} else {
|
||||||
binding.filterConversationsButton.let {
|
binding.filterConversationsButton.let {
|
||||||
|
@ -70,8 +70,12 @@ import okhttp3.OkHttpClient
|
|||||||
class RepositoryModule {
|
class RepositoryModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideConversationsRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ConversationsRepository {
|
fun provideConversationsRepository(
|
||||||
return ConversationsRepositoryImpl(ncApi, userProvider)
|
ncApi: NcApi,
|
||||||
|
ncApiCoroutines: NcApiCoroutines,
|
||||||
|
userProvider: CurrentUserProviderNew
|
||||||
|
): ConversationsRepository {
|
||||||
|
return ConversationsRepositoryImpl(ncApi, ncApiCoroutines, userProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
@ -59,7 +59,8 @@ fun ConversationModel.asEntity() =
|
|||||||
callStartTime = callStartTime,
|
callStartTime = callStartTime,
|
||||||
recordingConsentRequired = recordingConsentRequired,
|
recordingConsentRequired = recordingConsentRequired,
|
||||||
remoteServer = remoteServer,
|
remoteServer = remoteServer,
|
||||||
remoteToken = remoteToken
|
remoteToken = remoteToken,
|
||||||
|
hasArchived = hasArchived
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ConversationEntity.asModel() =
|
fun ConversationEntity.asModel() =
|
||||||
@ -109,7 +110,8 @@ fun ConversationEntity.asModel() =
|
|||||||
callStartTime = callStartTime,
|
callStartTime = callStartTime,
|
||||||
recordingConsentRequired = recordingConsentRequired,
|
recordingConsentRequired = recordingConsentRequired,
|
||||||
remoteServer = remoteServer,
|
remoteServer = remoteServer,
|
||||||
remoteToken = remoteToken
|
remoteToken = remoteToken,
|
||||||
|
hasArchived = hasArchived
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Conversation.asEntity(accountId: Long) =
|
fun Conversation.asEntity(accountId: Long) =
|
||||||
@ -158,5 +160,6 @@ fun Conversation.asEntity(accountId: Long) =
|
|||||||
callStartTime = callStartTime,
|
callStartTime = callStartTime,
|
||||||
recordingConsentRequired = recordingConsentRequired,
|
recordingConsentRequired = recordingConsentRequired,
|
||||||
remoteServer = remoteServer,
|
remoteServer = remoteServer,
|
||||||
remoteToken = remoteToken
|
remoteToken = remoteToken,
|
||||||
|
hasArchived = hasArchived
|
||||||
)
|
)
|
||||||
|
@ -92,7 +92,8 @@ data class ConversationEntity(
|
|||||||
@ColumnInfo(name = "type") var type: ConversationEnums.ConversationType,
|
@ColumnInfo(name = "type") var type: ConversationEnums.ConversationType,
|
||||||
@ColumnInfo(name = "unreadMention") var unreadMention: Boolean = false,
|
@ColumnInfo(name = "unreadMention") var unreadMention: Boolean = false,
|
||||||
@ColumnInfo(name = "unreadMentionDirect") var unreadMentionDirect: Boolean,
|
@ColumnInfo(name = "unreadMentionDirect") var unreadMentionDirect: Boolean,
|
||||||
@ColumnInfo(name = "unreadMessages") var unreadMessages: Int = 0
|
@ColumnInfo(name = "unreadMessages") var unreadMessages: Int = 0,
|
||||||
|
@ColumnInfo(name = "hasArchived") var hasArchived: Boolean = false
|
||||||
// missing/not needed: attendeeId
|
// missing/not needed: attendeeId
|
||||||
// missing/not needed: attendeePin
|
// missing/not needed: attendeePin
|
||||||
// missing/not needed: attendeePermissions
|
// missing/not needed: attendeePermissions
|
||||||
|
@ -9,6 +9,7 @@ package com.nextcloud.talk.data.source.local
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import java.sql.SQLException
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
object Migrations {
|
object Migrations {
|
||||||
@ -40,6 +41,13 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATION_11_12 = object : Migration(11, 12) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
Log.i("Migrations", "Migrating 11 to 12")
|
||||||
|
addArchiveConversations(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun migrateToRoom(db: SupportSQLiteDatabase) {
|
fun migrateToRoom(db: SupportSQLiteDatabase) {
|
||||||
db.execSQL(
|
db.execSQL(
|
||||||
"CREATE TABLE User_new (" +
|
"CREATE TABLE User_new (" +
|
||||||
@ -237,4 +245,15 @@ object Migrations {
|
|||||||
"ON `ChatBlocks` (`internalConversationId`)"
|
"ON `ChatBlocks` (`internalConversationId`)"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addArchiveConversations(db: SupportSQLiteDatabase) {
|
||||||
|
try {
|
||||||
|
db.execSQL(
|
||||||
|
"ALTER TABLE Conversations " +
|
||||||
|
"ADD `hasArchived` INTEGER;"
|
||||||
|
)
|
||||||
|
} catch (e: SQLException) {
|
||||||
|
Log.i("Migrations", "hasArchived already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,9 +49,9 @@ import java.util.Locale
|
|||||||
ChatMessageEntity::class,
|
ChatMessageEntity::class,
|
||||||
ChatBlockEntity::class
|
ChatBlockEntity::class
|
||||||
],
|
],
|
||||||
version = 11,
|
version = 12,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 9, to = 10)
|
AutoMigration(from = 9, to = 11)
|
||||||
],
|
],
|
||||||
exportSchema = true
|
exportSchema = true
|
||||||
)
|
)
|
||||||
@ -113,7 +113,8 @@ abstract class TalkDatabase : RoomDatabase() {
|
|||||||
Migrations.MIGRATION_6_8,
|
Migrations.MIGRATION_6_8,
|
||||||
Migrations.MIGRATION_7_8,
|
Migrations.MIGRATION_7_8,
|
||||||
Migrations.MIGRATION_8_9,
|
Migrations.MIGRATION_8_9,
|
||||||
Migrations.MIGRATION_10_11
|
Migrations.MIGRATION_10_11,
|
||||||
|
Migrations.MIGRATION_11_12
|
||||||
)
|
)
|
||||||
.allowMainThreadQueries()
|
.allowMainThreadQueries()
|
||||||
.addCallback(
|
.addCallback(
|
||||||
|
@ -59,6 +59,7 @@ class ConversationModel(
|
|||||||
var recordingConsentRequired: Int = 0,
|
var recordingConsentRequired: Int = 0,
|
||||||
var remoteServer: String? = null,
|
var remoteServer: String? = null,
|
||||||
var remoteToken: String? = null,
|
var remoteToken: String? = null,
|
||||||
|
var hasArchived: Boolean = false,
|
||||||
|
|
||||||
// attributes that don't come from API. This should be changed?!
|
// attributes that don't come from API. This should be changed?!
|
||||||
var password: String? = null
|
var password: String? = null
|
||||||
@ -120,7 +121,8 @@ class ConversationModel(
|
|||||||
callStartTime = conversation.callStartTime,
|
callStartTime = conversation.callStartTime,
|
||||||
recordingConsentRequired = conversation.recordingConsentRequired,
|
recordingConsentRequired = conversation.recordingConsentRequired,
|
||||||
remoteServer = conversation.remoteServer,
|
remoteServer = conversation.remoteServer,
|
||||||
remoteToken = conversation.remoteToken
|
remoteToken = conversation.remoteToken,
|
||||||
|
hasArchived = conversation.hasArchived
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,5 +159,8 @@ data class Conversation(
|
|||||||
var remoteServer: String? = "",
|
var remoteServer: String? = "",
|
||||||
|
|
||||||
@JsonField(name = ["remoteToken"])
|
@JsonField(name = ["remoteToken"])
|
||||||
var remoteToken: String? = ""
|
var remoteToken: String? = "",
|
||||||
|
|
||||||
|
@JsonField(name = ["isArchived"])
|
||||||
|
var hasArchived: Boolean = false
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.nextcloud.talk.repositories.conversations
|
package com.nextcloud.talk.repositories.conversations
|
||||||
|
|
||||||
|
import com.nextcloud.talk.models.json.generic.GenericOverall
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
|
|
||||||
interface ConversationsRepository {
|
interface ConversationsRepository {
|
||||||
@ -29,4 +30,8 @@ interface ConversationsRepository {
|
|||||||
val successful: Boolean
|
val successful: Boolean
|
||||||
)
|
)
|
||||||
fun resendInvitations(token: String): Observable<ResendInvitationsResult>
|
fun resendInvitations(token: String): Observable<ResendInvitationsResult>
|
||||||
|
|
||||||
|
suspend fun archiveConversation(credentials: String, url: String): GenericOverall
|
||||||
|
|
||||||
|
suspend fun unarchiveConversation(credentials: String, url: String): GenericOverall
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,10 @@ package com.nextcloud.talk.repositories.conversations
|
|||||||
|
|
||||||
import com.bluelinelabs.logansquare.LoganSquare
|
import com.bluelinelabs.logansquare.LoganSquare
|
||||||
import com.nextcloud.talk.api.NcApi
|
import com.nextcloud.talk.api.NcApi
|
||||||
|
import com.nextcloud.talk.api.NcApiCoroutines
|
||||||
import com.nextcloud.talk.data.user.model.User
|
import com.nextcloud.talk.data.user.model.User
|
||||||
import com.nextcloud.talk.models.json.conversations.password.PasswordOverall
|
import com.nextcloud.talk.models.json.conversations.password.PasswordOverall
|
||||||
|
import com.nextcloud.talk.models.json.generic.GenericOverall
|
||||||
import com.nextcloud.talk.repositories.conversations.ConversationsRepository.AllowGuestsResult
|
import com.nextcloud.talk.repositories.conversations.ConversationsRepository.AllowGuestsResult
|
||||||
import com.nextcloud.talk.repositories.conversations.ConversationsRepository.PasswordResult
|
import com.nextcloud.talk.repositories.conversations.ConversationsRepository.PasswordResult
|
||||||
import com.nextcloud.talk.repositories.conversations.ConversationsRepository.ResendInvitationsResult
|
import com.nextcloud.talk.repositories.conversations.ConversationsRepository.ResendInvitationsResult
|
||||||
@ -20,6 +22,7 @@ import io.reactivex.Observable
|
|||||||
|
|
||||||
class ConversationsRepositoryImpl(
|
class ConversationsRepositoryImpl(
|
||||||
private val api: NcApi,
|
private val api: NcApi,
|
||||||
|
private val coroutineApi: NcApiCoroutines,
|
||||||
private val userProvider: CurrentUserProviderNew
|
private val userProvider: CurrentUserProviderNew
|
||||||
) :
|
) :
|
||||||
ConversationsRepository {
|
ConversationsRepository {
|
||||||
@ -89,6 +92,14 @@ class ConversationsRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun archiveConversation(credentials: String, url: String): GenericOverall {
|
||||||
|
return coroutineApi.archiveConversation(credentials, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun unarchiveConversation(credentials: String, url: String): GenericOverall {
|
||||||
|
return coroutineApi.unarchiveConversation(credentials, url)
|
||||||
|
}
|
||||||
|
|
||||||
private fun apiVersion(): Int {
|
private fun apiVersion(): Int {
|
||||||
return ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4))
|
return ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4))
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,8 @@ class FilterConversationFragment : DialogFragment() {
|
|||||||
binding.run {
|
binding.run {
|
||||||
listOf(
|
listOf(
|
||||||
unreadFilterChip,
|
unreadFilterChip,
|
||||||
mentionedFilterChip
|
mentionedFilterChip,
|
||||||
|
archivedFilterChip
|
||||||
)
|
)
|
||||||
}.forEach(viewThemeUtils.talk::themeChipFilter)
|
}.forEach(viewThemeUtils.talk::themeChipFilter)
|
||||||
|
|
||||||
@ -89,6 +90,12 @@ class FilterConversationFragment : DialogFragment() {
|
|||||||
processSubmit()
|
processSubmit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.archivedFilterChip.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
filterState[ARCHIVE] = isChecked
|
||||||
|
binding.archivedFilterChip.isChecked = isChecked
|
||||||
|
processSubmit()
|
||||||
|
}
|
||||||
|
|
||||||
binding.buttonClose.setOnClickListener {
|
binding.buttonClose.setOnClickListener {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
@ -97,6 +104,7 @@ class FilterConversationFragment : DialogFragment() {
|
|||||||
private fun setUpChips() {
|
private fun setUpChips() {
|
||||||
binding.unreadFilterChip.isChecked = filterState[UNREAD]!!
|
binding.unreadFilterChip.isChecked = filterState[UNREAD]!!
|
||||||
binding.mentionedFilterChip.isChecked = filterState[MENTION]!!
|
binding.mentionedFilterChip.isChecked = filterState[MENTION]!!
|
||||||
|
binding.archivedFilterChip.isChecked = filterState[ARCHIVE]!!
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processSubmit() {
|
private fun processSubmit() {
|
||||||
@ -104,9 +112,11 @@ class FilterConversationFragment : DialogFragment() {
|
|||||||
val accountId = UserIdUtils.getIdForUser(userManager.currentUser.blockingGet())
|
val accountId = UserIdUtils.getIdForUser(userManager.currentUser.blockingGet())
|
||||||
val mentionValue = filterState[MENTION] == true
|
val mentionValue = filterState[MENTION] == true
|
||||||
val unreadValue = filterState[UNREAD] == true
|
val unreadValue = filterState[UNREAD] == true
|
||||||
|
val archivedValue = filterState[ARCHIVE] == true
|
||||||
|
|
||||||
arbitraryStorageManager.storeStorageSetting(accountId, MENTION, mentionValue.toString(), "")
|
arbitraryStorageManager.storeStorageSetting(accountId, MENTION, mentionValue.toString(), "")
|
||||||
arbitraryStorageManager.storeStorageSetting(accountId, UNREAD, unreadValue.toString(), "")
|
arbitraryStorageManager.storeStorageSetting(accountId, UNREAD, unreadValue.toString(), "")
|
||||||
|
arbitraryStorageManager.storeStorageSetting(accountId, ARCHIVE, archivedValue.toString(), "")
|
||||||
|
|
||||||
(requireActivity() as ConversationsListActivity).filterConversation()
|
(requireActivity() as ConversationsListActivity).filterConversation()
|
||||||
}
|
}
|
||||||
@ -126,5 +136,7 @@ class FilterConversationFragment : DialogFragment() {
|
|||||||
val TAG: String = FilterConversationFragment::class.java.simpleName
|
val TAG: String = FilterConversationFragment::class.java.simpleName
|
||||||
const val MENTION: String = "mention"
|
const val MENTION: String = "mention"
|
||||||
const val UNREAD: String = "unread"
|
const val UNREAD: String = "unread"
|
||||||
|
const val ARCHIVE: String = "archive"
|
||||||
|
const val DEFAULT: String = "default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -588,4 +588,8 @@ object ApiUtils {
|
|||||||
fun getUrlForUnban(baseUrl: String, token: String, banId: Int): String {
|
fun getUrlForUnban(baseUrl: String, token: String, banId: Int): String {
|
||||||
return "${getUrlForBans(baseUrl, token)}/$banId"
|
return "${getUrlForBans(baseUrl, token)}/$banId"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getUrlForArchive(version: Int, baseUrl: String?, token: String?): String {
|
||||||
|
return "${getUrlForRoom(version, baseUrl, token)}/archive"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,8 @@ enum class SpreedFeatures(val value: String) {
|
|||||||
FEDERATION_V1("federation-v1"),
|
FEDERATION_V1("federation-v1"),
|
||||||
DELETE_MESSAGES_UNLIMITED("delete-messages-unlimited"),
|
DELETE_MESSAGES_UNLIMITED("delete-messages-unlimited"),
|
||||||
BAN_V1("ban-v1"),
|
BAN_V1("ban-v1"),
|
||||||
EDIT_MESSAGES_NOTE_TO_SELF("edit-messages-note-to-self")
|
EDIT_MESSAGES_NOTE_TO_SELF("edit-messages-note-to-self"),
|
||||||
|
ARCHIVE_CONVERSATIONS("archived-conversations")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
@ -255,6 +256,10 @@ object CapabilitiesUtil {
|
|||||||
user.capabilities!!.spreedCapability!!.config!!["federation"]!!.containsKey("enabled")
|
user.capabilities!!.spreedCapability!!.config!!["federation"]!!.containsKey("enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isArchiveConversationsAvailable(spreedCapabilities: SpreedCapability): Boolean {
|
||||||
|
return hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.ARCHIVE_CONVERSATIONS)
|
||||||
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
//region ThemingCapabilities
|
//region ThemingCapabilities
|
||||||
|
18
app/src/main/res/drawable/outline_archive_24.xml
Normal file
18
app/src/main/res/drawable/outline_archive_24.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!--
|
||||||
|
~ Nextcloud Talk - Android Client
|
||||||
|
~
|
||||||
|
~ SPDX-FileCopyrightText: 2024 Google LLC
|
||||||
|
~ SPDX-License-Identifier: Apache-2.0
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="#000000"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:width="24dp">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M480,720L640,560L584,504L520,568L520,400L440,400L440,568L376,504L320,560L480,720ZM200,320L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,320L200,320ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,261Q120,247 124.5,234Q129,221 138,210L188,149Q199,135 215.5,127.5Q232,120 250,120L710,120Q728,120 744.5,127.5Q761,135 772,149L822,210Q831,221 835.5,234Q840,247 840,261L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM216,240L744,240L710,200Q710,200 710,200Q710,200 710,200L250,200Q250,200 250,200Q250,200 250,200L216,240ZM480,540L480,540L480,540Q480,540 480,540Q480,540 480,540L480,540Q480,540 480,540Q480,540 480,540Z" />
|
||||||
|
|
||||||
|
</vector>
|
@ -383,6 +383,44 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/archive_conversation_btn"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="@dimen/standard_margin"
|
||||||
|
android:paddingTop="@dimen/standard_half_margin"
|
||||||
|
android:paddingEnd="@dimen/standard_margin"
|
||||||
|
android:paddingBottom="@dimen/standard_half_margin"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:background="?android:attr/selectableItemBackground">
|
||||||
|
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/archive_conversation_icon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginEnd="@dimen/standard_margin"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
tools:src="@drawable/outline_archive_24"
|
||||||
|
app:tint="@color/grey_600" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/archive_conversation_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
tools:text="@string/unarchive_conversation"
|
||||||
|
android:textSize="@dimen/headline_text_size" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/archive_conversation_text_hint"
|
||||||
|
android:layout_marginHorizontal="@dimen/standard_margin"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/archive_hint" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/danger_zone_options"
|
android:id="@+id/danger_zone_options"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
android:layout_marginTop="@dimen/standard_half_margin"
|
android:layout_marginTop="@dimen/standard_half_margin"
|
||||||
android:layout_marginEnd="@dimen/standard_margin"
|
android:layout_marginEnd="@dimen/standard_margin"
|
||||||
android:layout_marginBottom="@dimen/standard_half_margin"
|
android:layout_marginBottom="@dimen/standard_half_margin"
|
||||||
app:chipSpacingHorizontal="@dimen/standard_margin">
|
app:chipSpacingHorizontal="@dimen/standard_half_margin">
|
||||||
|
|
||||||
<com.google.android.material.chip.Chip
|
<com.google.android.material.chip.Chip
|
||||||
android:id="@+id/unread_filter_chip"
|
android:id="@+id/unread_filter_chip"
|
||||||
@ -48,6 +48,13 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/mentioned" />
|
android:text="@string/mentioned" />
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/archived_filter_chip"
|
||||||
|
style="@style/Widget.Material3.Chip.Filter"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/archived" />
|
||||||
|
|
||||||
</com.google.android.material.chip.ChipGroup>
|
</com.google.android.material.chip.ChipGroup>
|
||||||
|
|
||||||
<com.google.android.material.divider.MaterialDivider
|
<com.google.android.material.divider.MaterialDivider
|
||||||
|
@ -817,4 +817,9 @@ How to translate with transifex:
|
|||||||
<string name="show_ban_reason">Show ban reason</string>
|
<string name="show_ban_reason">Show ban reason</string>
|
||||||
<string name="error_unbanning">Error occurred when unbanning participant</string>
|
<string name="error_unbanning">Error occurred when unbanning participant</string>
|
||||||
<string name="connection_lost">Connection lost</string>
|
<string name="connection_lost">Connection lost</string>
|
||||||
|
<string name="archive_conversation">Archive Conversation</string>
|
||||||
|
<string name="unarchive_conversation">Unarchive Conversation</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="unarchive_hint">Once a conversation is unarchived, it will be shown by default again.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user