Merge pull request #4333 from nextcloud/issue-4257-archive-conversation

Archived Conversations 🗃️
This commit is contained in:
Julius Linus 2024-10-28 08:30:03 -05:00 committed by GitHub
commit 085711d077
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 258 additions and 36 deletions

View File

@ -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')"
] ]
} }
} }

View File

@ -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
} }

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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
) )

View File

@ -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

View File

@ -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")
}
}
} }

View File

@ -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(

View File

@ -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
) )
} }
} }

View File

@ -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

View File

@ -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
} }

View File

@ -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))
} }

View File

@ -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"
} }
} }

View File

@ -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"
}
} }

View File

@ -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

View 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>

View File

@ -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"

View File

@ -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

View File

@ -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>