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,
"database": {
"version": 11,
"identityHash": "bc802cadfdef41d3eb94ffbb0729eb89",
"identityHash": "7edb537b6987d0de6586a6760c970958",
"entities": [
{
"tableName": "User",
@ -138,7 +138,7 @@
},
{
"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": [
{
"fieldPath": "internalId",
@ -409,6 +409,12 @@
"columnName": "unreadMessages",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasArchived",
"columnName": "hasArchived",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
@ -713,7 +719,7 @@
"views": [],
"setupQueries": [
"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
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.VISIBLE
import androidx.appcompat.app.AlertDialog
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
@ -86,6 +88,7 @@ import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
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) {
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.generic.GenericOverall
import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.repositories.conversations.ConversationsRepository
import com.nextcloud.talk.utils.ApiUtils
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
@ -26,7 +27,8 @@ import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
class ConversationInfoViewModel @Inject constructor(
private val chatNetworkDataSource: ChatNetworkDataSource
private val chatNetworkDataSource: ChatNetworkDataSource,
private val conversationsRepository: ConversationsRepository
) : ViewModel() {
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> {
override fun onSubscribe(d: Disposable) {
// unused atm

View File

@ -210,7 +210,9 @@ class ConversationsListActivity :
private var filterState =
mutableMapOf(
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)
private lateinit var accountIconBadge: BadgeDrawable
@ -380,7 +382,7 @@ class ConversationsListActivity :
sortConversations(conversationItemsWithHeader)
// Filter Conversations
if (!filterState.containsValue(true)) filterableConversationItems = conversationItems
if (!hasFilterEnabled()) filterableConversationItems = conversationItems
filterConversation()
adapter!!.updateDataSet(filterableConversationItems, false)
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() {
val accountId = UserIdUtils.getIdForUser(userManager.currentUser.blockingGet())
filterState[FilterConversationFragment.UNREAD] = (
@ -413,22 +423,24 @@ class ConversationsListActivity :
).blockingGet()?.value ?: ""
) == "true"
filterState[FilterConversationFragment.ARCHIVE] = (
arbitraryStorageManager.getStorageSetting(
accountId,
FilterConversationFragment.ARCHIVE,
""
).blockingGet()?.value ?: ""
) == "true"
val newItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
if (filterState[FilterConversationFragment.UNREAD] == false &&
filterState[FilterConversationFragment.MENTION] == false
) {
adapter!!.updateDataSet(conversationItems, true)
} else {
val items = conversationItems
for (i in items) {
val conversation = (i as ConversationItem).model
if (filter(conversation)) {
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()
}
@ -449,10 +461,19 @@ class ConversationsListActivity :
)
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
}
@ -649,7 +670,7 @@ class ConversationsListActivity :
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
initSearchDisposable()
adapter!!.setHeadersShown(true)
if (!filterState.containsValue(true)) filterableConversationItems = searchableConversationItems
if (!hasFilterEnabled()) filterableConversationItems = searchableConversationItems
adapter!!.updateDataSet(filterableConversationItems, false)
adapter!!.showAllHeaders()
binding.swipeRefreshLayoutView?.isEnabled = false
@ -659,7 +680,7 @@ class ConversationsListActivity :
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
adapter!!.setHeadersShown(false)
if (!filterState.containsValue(true)) filterableConversationItems = conversationItemsWithHeader
if (!hasFilterEnabled()) filterableConversationItems = conversationItemsWithHeader
adapter!!.updateDataSet(filterableConversationItems, false)
adapter!!.hideAllHeaders()
if (searchHelper != null) {
@ -1826,7 +1847,7 @@ class ConversationsListActivity :
}
fun updateFilterConversationButtonColor() {
if (filterState.containsValue(true)) {
if (hasFilterEnabled()) {
binding.filterConversationsButton.let { viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) }
} else {
binding.filterConversationsButton.let {

View File

@ -70,8 +70,12 @@ import okhttp3.OkHttpClient
class RepositoryModule {
@Provides
fun provideConversationsRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ConversationsRepository {
return ConversationsRepositoryImpl(ncApi, userProvider)
fun provideConversationsRepository(
ncApi: NcApi,
ncApiCoroutines: NcApiCoroutines,
userProvider: CurrentUserProviderNew
): ConversationsRepository {
return ConversationsRepositoryImpl(ncApi, ncApiCoroutines, userProvider)
}
@Provides

View File

@ -59,7 +59,8 @@ fun ConversationModel.asEntity() =
callStartTime = callStartTime,
recordingConsentRequired = recordingConsentRequired,
remoteServer = remoteServer,
remoteToken = remoteToken
remoteToken = remoteToken,
hasArchived = hasArchived
)
fun ConversationEntity.asModel() =
@ -109,7 +110,8 @@ fun ConversationEntity.asModel() =
callStartTime = callStartTime,
recordingConsentRequired = recordingConsentRequired,
remoteServer = remoteServer,
remoteToken = remoteToken
remoteToken = remoteToken,
hasArchived = hasArchived
)
fun Conversation.asEntity(accountId: Long) =
@ -158,5 +160,6 @@ fun Conversation.asEntity(accountId: Long) =
callStartTime = callStartTime,
recordingConsentRequired = recordingConsentRequired,
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 = "unreadMention") var unreadMention: Boolean = false,
@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: attendeePin
// missing/not needed: attendeePermissions

View File

@ -9,6 +9,7 @@ package com.nextcloud.talk.data.source.local
import android.util.Log
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import java.sql.SQLException
@Suppress("MagicNumber")
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) {
db.execSQL(
"CREATE TABLE User_new (" +
@ -237,4 +245,15 @@ object Migrations {
"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,
ChatBlockEntity::class
],
version = 11,
version = 12,
autoMigrations = [
AutoMigration(from = 9, to = 10)
AutoMigration(from = 9, to = 11)
],
exportSchema = true
)
@ -113,7 +113,8 @@ abstract class TalkDatabase : RoomDatabase() {
Migrations.MIGRATION_6_8,
Migrations.MIGRATION_7_8,
Migrations.MIGRATION_8_9,
Migrations.MIGRATION_10_11
Migrations.MIGRATION_10_11,
Migrations.MIGRATION_11_12
)
.allowMainThreadQueries()
.addCallback(

View File

@ -59,6 +59,7 @@ class ConversationModel(
var recordingConsentRequired: Int = 0,
var remoteServer: String? = null,
var remoteToken: String? = null,
var hasArchived: Boolean = false,
// attributes that don't come from API. This should be changed?!
var password: String? = null
@ -120,7 +121,8 @@ class ConversationModel(
callStartTime = conversation.callStartTime,
recordingConsentRequired = conversation.recordingConsentRequired,
remoteServer = conversation.remoteServer,
remoteToken = conversation.remoteToken
remoteToken = conversation.remoteToken,
hasArchived = conversation.hasArchived
)
}
}

View File

@ -159,5 +159,8 @@ data class Conversation(
var remoteServer: String? = "",
@JsonField(name = ["remoteToken"])
var remoteToken: String? = ""
var remoteToken: String? = "",
@JsonField(name = ["isArchived"])
var hasArchived: Boolean = false
) : Parcelable

View File

@ -7,6 +7,7 @@
*/
package com.nextcloud.talk.repositories.conversations
import com.nextcloud.talk.models.json.generic.GenericOverall
import io.reactivex.Observable
interface ConversationsRepository {
@ -29,4 +30,8 @@ interface ConversationsRepository {
val successful: Boolean
)
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.nextcloud.talk.api.NcApi
import com.nextcloud.talk.api.NcApiCoroutines
import com.nextcloud.talk.data.user.model.User
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.PasswordResult
import com.nextcloud.talk.repositories.conversations.ConversationsRepository.ResendInvitationsResult
@ -20,6 +22,7 @@ import io.reactivex.Observable
class ConversationsRepositoryImpl(
private val api: NcApi,
private val coroutineApi: NcApiCoroutines,
private val userProvider: CurrentUserProviderNew
) :
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 {
return ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4))
}

View File

@ -69,7 +69,8 @@ class FilterConversationFragment : DialogFragment() {
binding.run {
listOf(
unreadFilterChip,
mentionedFilterChip
mentionedFilterChip,
archivedFilterChip
)
}.forEach(viewThemeUtils.talk::themeChipFilter)
@ -89,6 +90,12 @@ class FilterConversationFragment : DialogFragment() {
processSubmit()
}
binding.archivedFilterChip.setOnCheckedChangeListener { _, isChecked ->
filterState[ARCHIVE] = isChecked
binding.archivedFilterChip.isChecked = isChecked
processSubmit()
}
binding.buttonClose.setOnClickListener {
dismiss()
}
@ -97,6 +104,7 @@ class FilterConversationFragment : DialogFragment() {
private fun setUpChips() {
binding.unreadFilterChip.isChecked = filterState[UNREAD]!!
binding.mentionedFilterChip.isChecked = filterState[MENTION]!!
binding.archivedFilterChip.isChecked = filterState[ARCHIVE]!!
}
private fun processSubmit() {
@ -104,9 +112,11 @@ class FilterConversationFragment : DialogFragment() {
val accountId = UserIdUtils.getIdForUser(userManager.currentUser.blockingGet())
val mentionValue = filterState[MENTION] == true
val unreadValue = filterState[UNREAD] == true
val archivedValue = filterState[ARCHIVE] == true
arbitraryStorageManager.storeStorageSetting(accountId, MENTION, mentionValue.toString(), "")
arbitraryStorageManager.storeStorageSetting(accountId, UNREAD, unreadValue.toString(), "")
arbitraryStorageManager.storeStorageSetting(accountId, ARCHIVE, archivedValue.toString(), "")
(requireActivity() as ConversationsListActivity).filterConversation()
}
@ -126,5 +136,7 @@ class FilterConversationFragment : DialogFragment() {
val TAG: String = FilterConversationFragment::class.java.simpleName
const val MENTION: String = "mention"
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 {
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"),
DELETE_MESSAGES_UNLIMITED("delete-messages-unlimited"),
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")
@ -255,6 +256,10 @@ object CapabilitiesUtil {
user.capabilities!!.spreedCapability!!.config!!["federation"]!!.containsKey("enabled")
}
fun isArchiveConversationsAvailable(spreedCapabilities: SpreedCapability): Boolean {
return hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.ARCHIVE_CONVERSATIONS)
}
// endregion
//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
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
android:id="@+id/danger_zone_options"
android:layout_width="match_parent"

View File

@ -32,7 +32,7 @@
android:layout_marginTop="@dimen/standard_half_margin"
android:layout_marginEnd="@dimen/standard_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
android:id="@+id/unread_filter_chip"
@ -48,6 +48,13 @@
android:layout_height="wrap_content"
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.divider.MaterialDivider

View File

@ -817,4 +817,9 @@ How to translate with transifex:
<string name="show_ban_reason">Show ban reason</string>
<string name="error_unbanning">Error occurred when unbanning participant</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>