Merge pull request #4973 from nextcloud/important_conversations

Important conversations
This commit is contained in:
Marcel Hibbe 2025-05-27 13:51:22 +00:00 committed by GitHub
commit b35bfc1ee7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 934 additions and 26 deletions

View File

@ -0,0 +1,731 @@
{
"formatVersion": 1,
"database": {
"version": 16,
"identityHash": "bbf526d5c78a99eb951635cc46f4c59f",
"entities": [
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT"
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT"
},
{
"fieldPath": "baseUrl",
"columnName": "baseUrl",
"affinity": "TEXT"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT"
},
{
"fieldPath": "pushConfigurationState",
"columnName": "pushConfigurationState",
"affinity": "TEXT"
},
{
"fieldPath": "capabilities",
"columnName": "capabilities",
"affinity": "TEXT"
},
{
"fieldPath": "serverVersion",
"columnName": "serverVersion",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "clientCertificate",
"columnName": "clientCertificate",
"affinity": "TEXT"
},
{
"fieldPath": "externalSignalingServer",
"columnName": "externalSignalingServer",
"affinity": "TEXT"
},
{
"fieldPath": "current",
"columnName": "current",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scheduledForDeletion",
"columnName": "scheduledForDeletion",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "ArbitraryStorage",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
"fields": [
{
"fieldPath": "accountIdentifier",
"columnName": "accountIdentifier",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "storageObject",
"columnName": "object",
"affinity": "TEXT"
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"accountIdentifier",
"key"
]
}
},
{
"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, `objectId` 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, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatarVersion",
"columnName": "avatarVersion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "callFlag",
"columnName": "callFlag",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callRecording",
"columnName": "callRecording",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callStartTime",
"columnName": "callStartTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canDeleteConversation",
"columnName": "canDeleteConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canLeaveConversation",
"columnName": "canLeaveConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canStartCall",
"columnName": "canStartCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasCall",
"columnName": "hasCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasPassword",
"columnName": "hasPassword",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasCustomAvatar",
"columnName": "isCustomAvatar",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favorite",
"columnName": "isFavorite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastActivity",
"columnName": "lastActivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastCommonReadMessage",
"columnName": "lastCommonReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessage",
"columnName": "lastMessage",
"affinity": "TEXT"
},
{
"fieldPath": "lastPing",
"columnName": "lastPing",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastReadMessage",
"columnName": "lastReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lobbyState",
"columnName": "lobbyState",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lobbyTimer",
"columnName": "lobbyTimer",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "messageExpiration",
"columnName": "messageExpiration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationCalls",
"columnName": "notificationCalls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLevel",
"columnName": "notificationLevel",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectType",
"columnName": "objectType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectId",
"columnName": "objectId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "participantType",
"columnName": "participantType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "conversationReadOnlyState",
"columnName": "readOnly",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recordingConsentRequired",
"columnName": "recordingConsent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteServer",
"columnName": "remoteServer",
"affinity": "TEXT"
},
{
"fieldPath": "remoteToken",
"columnName": "remoteToken",
"affinity": "TEXT"
},
{
"fieldPath": "sessionId",
"columnName": "sessionId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT"
},
{
"fieldPath": "statusClearAt",
"columnName": "statusClearAt",
"affinity": "INTEGER"
},
{
"fieldPath": "statusIcon",
"columnName": "statusIcon",
"affinity": "TEXT"
},
{
"fieldPath": "statusMessage",
"columnName": "statusMessage",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unreadMention",
"columnName": "unreadMention",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMentionDirect",
"columnName": "unreadMentionDirect",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMessages",
"columnName": "unreadMessages",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasArchived",
"columnName": "hasArchived",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasSensitive",
"columnName": "hasSensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasImportant",
"columnName": "hasImportant",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_Conversations_accountId",
"unique": false,
"columnNames": [
"accountId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
}
],
"foreignKeys": [
{
"table": "User",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ChatMessages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendingFailed` INTEGER NOT NULL, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorDisplayName",
"columnName": "actorDisplayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "deleted",
"columnName": "deleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationTimestamp",
"columnName": "expirationTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "replyable",
"columnName": "isReplyable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTemporary",
"columnName": "isTemporary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastEditActorDisplayName",
"columnName": "lastEditActorDisplayName",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorId",
"columnName": "lastEditActorId",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorType",
"columnName": "lastEditActorType",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditTimestamp",
"columnName": "lastEditTimestamp",
"affinity": "INTEGER"
},
{
"fieldPath": "renderMarkdown",
"columnName": "markdown",
"affinity": "INTEGER"
},
{
"fieldPath": "messageParameters",
"columnName": "messageParameters",
"affinity": "TEXT"
},
{
"fieldPath": "messageType",
"columnName": "messageType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parentMessageId",
"columnName": "parent",
"affinity": "INTEGER"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT"
},
{
"fieldPath": "reactionsSelf",
"columnName": "reactionsSelf",
"affinity": "TEXT"
},
{
"fieldPath": "referenceId",
"columnName": "referenceId",
"affinity": "TEXT"
},
{
"fieldPath": "sendingFailed",
"columnName": "sendingFailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "silent",
"columnName": "silent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "systemMessageType",
"columnName": "systemMessage",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_ChatMessages_internalId",
"unique": true,
"columnNames": [
"internalId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
},
{
"name": "index_ChatMessages_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
},
{
"tableName": "ChatBlocks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "oldestMessageId",
"columnName": "oldestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "newestMessageId",
"columnName": "newestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasHistory",
"columnName": "hasHistory",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ChatBlocks_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
}
],
"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, 'bbf526d5c78a99eb951635cc46f4c59f')"
]
}
}

View File

@ -179,6 +179,18 @@ interface NcApiCoroutines {
@Url url: String @Url url: String
): GenericOverall ): GenericOverall
@POST
suspend fun markConversationAsImportant(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@DELETE
suspend fun markConversationAsUnimportant(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@DELETE @DELETE
suspend fun removeConversationFromFavorites( suspend fun removeConversationFromFavorites(
@Header("Authorization") authorization: String, @Header("Authorization") authorization: String,

View File

@ -252,6 +252,8 @@ class ConversationInfoActivity :
initClearChatHistoryObserver() initClearChatHistoryObserver()
initMarkConversationAsSensitiveObserver() initMarkConversationAsSensitiveObserver()
initMarkConversationAsInsensitiveObserver() initMarkConversationAsInsensitiveObserver()
initMarkConversationAsImportantObserver()
initMarkConversationAsUnimportantObserver()
} }
private fun initMarkConversationAsSensitiveObserver() { private fun initMarkConversationAsSensitiveObserver() {
@ -381,7 +383,46 @@ class ConversationInfoActivity :
} }
} }
@SuppressLint("SetTextI18n") private fun initMarkConversationAsImportantObserver() {
viewModel.markAsImportantResult.observe(this) { uiState ->
when (uiState) {
is ConversationInfoViewModel.MarkConversationAsImportantViewState.Success -> {
Snackbar.make(
binding.root,
context.getString(R.string.nc_mark_conversation_as_important),
Snackbar.LENGTH_LONG
).show()
}
is ConversationInfoViewModel.MarkConversationAsImportantViewState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "failed to mark conversation as important", uiState.exception)
}
else -> {
}
}
}
}
private fun initMarkConversationAsUnimportantObserver() {
viewModel.markAsUnimportantResult.observe(this) { uiState ->
when (uiState) {
is ConversationInfoViewModel.MarkConversationAsUnimportantViewState.Success -> {
Snackbar.make(
binding.root,
context.getString(R.string.nc_mark_conversation_as_unimportant),
Snackbar.LENGTH_LONG
).show()
}
is ConversationInfoViewModel.MarkConversationAsUnimportantViewState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "failed to mark conversation as unimportant", uiState.exception)
}
else -> {
}
}
}
}
private fun initViewStateObserver() { private fun initViewStateObserver() {
viewModel.viewState.observe(this) { state -> viewModel.viewState.observe(this) { state ->
when (state) { when (state) {
@ -1025,6 +1066,31 @@ class ConversationInfoActivity :
} }
} }
binding.notificationSettingsView.importantConversationSwitch.isChecked = conversation!!.hasImportant
binding.notificationSettingsView.notificationSettingsImportantConversation.setOnClickListener {
val isChecked = binding.notificationSettingsView.importantConversationSwitch.isChecked
binding.notificationSettingsView.importantConversationSwitch.isChecked = !isChecked
if (!isChecked) {
viewModel.markConversationAsImportant(
credentials,
conversationUser.baseUrl!!,
conversation?.token!!
)
} else {
viewModel.markConversationAsUnimportant(
credentials,
conversationUser.baseUrl!!,
conversation?.token!!
)
}
}
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.IMPORTANT_CONVERSATIONS)) {
binding.notificationSettingsView.notificationSettingsImportantConversation.visibility = VISIBLE
} else {
binding.notificationSettingsView.notificationSettingsImportantConversation.visibility = GONE
}
if (!hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.ARCHIVE_CONVERSATIONS)) { if (!hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.ARCHIVE_CONVERSATIONS)) {
binding.archiveConversationBtn.visibility = GONE binding.archiveConversationBtn.visibility = GONE
binding.archiveConversationTextHint.visibility = GONE binding.archiveConversationTextHint.visibility = GONE
@ -1756,13 +1822,6 @@ class ConversationInfoActivity :
} }
private fun setUpNotificationSettings(module: DatabaseStorageModule) { private fun setUpNotificationSettings(module: DatabaseStorageModule) {
binding.notificationSettingsView.notificationSettingsImportantConversation.setOnClickListener {
val isChecked = binding.notificationSettingsView.importantConversationSwitch.isChecked
binding.notificationSettingsView.importantConversationSwitch.isChecked = !isChecked
lifecycleScope.launch {
module.saveBoolean("important_conversation_switch", !isChecked)
}
}
binding.notificationSettingsView.notificationSettingsCallNotifications.setOnClickListener { binding.notificationSettingsView.notificationSettingsCallNotifications.setOnClickListener {
val isChecked = binding.notificationSettingsView.callNotificationsSwitch.isChecked val isChecked = binding.notificationSettingsView.callNotificationsSwitch.isChecked
binding.notificationSettingsView.callNotificationsSwitch.isChecked = !isChecked binding.notificationSettingsView.callNotificationsSwitch.isChecked = !isChecked
@ -1780,9 +1839,6 @@ class ConversationInfoActivity :
} }
} }
binding.notificationSettingsView.importantConversationSwitch.isChecked = module
.getBoolean("important_conversation_switch", false)
if (conversation!!.remoteServer.isNullOrEmpty()) { if (conversation!!.remoteServer.isNullOrEmpty()) {
binding.notificationSettingsView.notificationSettingsCallNotifications.visibility = VISIBLE binding.notificationSettingsView.notificationSettingsCallNotifications.visibility = VISIBLE
binding.notificationSettingsView.callNotificationsSwitch.isChecked = module binding.notificationSettingsView.callNotificationsSwitch.isChecked = module

View File

@ -124,6 +124,18 @@ class ConversationInfoViewModel @Inject constructor(
val getConversationReadOnlyState: LiveData<SetConversationReadOnlyViewState> val getConversationReadOnlyState: LiveData<SetConversationReadOnlyViewState>
get() = _getConversationReadOnlyState get() = _getConversationReadOnlyState
@Suppress("PropertyName")
private val _markConversationAsImportantResult =
MutableLiveData<MarkConversationAsImportantViewState>(MarkConversationAsImportantViewState.None)
val markAsImportantResult: LiveData<MarkConversationAsImportantViewState>
get() = _markConversationAsImportantResult
@Suppress("PropertyName")
private val _markConversationAsUnimportantResult =
MutableLiveData<MarkConversationAsUnimportantViewState>(MarkConversationAsUnimportantViewState.None)
val markAsUnimportantResult: LiveData<MarkConversationAsUnimportantViewState>
get() = _markConversationAsUnimportantResult
private val _createRoomViewState = MutableLiveData<CreateRoomUIState>(CreateRoomUIState.None) private val _createRoomViewState = MutableLiveData<CreateRoomUIState>(CreateRoomUIState.None)
val createRoomViewState: LiveData<CreateRoomUIState> val createRoomViewState: LiveData<CreateRoomUIState>
get() = _createRoomViewState get() = _createRoomViewState
@ -356,6 +368,34 @@ class ConversationInfoViewModel @Inject constructor(
conversationsRepository.unarchiveConversation(user.getCredentials(), url) conversationsRepository.unarchiveConversation(user.getCredentials(), url)
} }
@Suppress("Detekt.TooGenericExceptionCaught")
fun markConversationAsImportant(credentials: String, baseUrl: String, roomToken: String) {
viewModelScope.launch {
try {
val response = conversationsRepository.markConversationAsImportant(credentials, baseUrl, roomToken)
_markConversationAsImportantResult.value =
MarkConversationAsImportantViewState.Success(response.ocs?.meta?.statusCode!!)
} catch (exception: Exception) {
_markConversationAsImportantResult.value =
MarkConversationAsImportantViewState.Error(exception)
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun markConversationAsUnimportant(credentials: String, baseUrl: String, roomToken: String) {
viewModelScope.launch {
try {
val response = conversationsRepository.markConversationAsUnImportant(credentials, baseUrl, roomToken)
_markConversationAsUnimportantResult.value =
MarkConversationAsUnimportantViewState.Success(response.ocs?.meta?.statusCode!!)
} catch (exception: Exception) {
_markConversationAsUnimportantResult.value =
MarkConversationAsUnimportantViewState.Error(exception)
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught") @Suppress("Detekt.TooGenericExceptionCaught")
fun clearChatHistory(apiVersion: Int, roomToken: String) { fun clearChatHistory(apiVersion: Int, roomToken: String) {
viewModelScope.launch { viewModelScope.launch {
@ -480,4 +520,16 @@ class ConversationInfoViewModel @Inject constructor(
data object Success : PasswordUiState() data object Success : PasswordUiState()
data class Error(val exception: Exception) : PasswordUiState() data class Error(val exception: Exception) : PasswordUiState()
} }
sealed class MarkConversationAsImportantViewState {
data object None : MarkConversationAsImportantViewState()
data class Success(val statusCode: Int) : MarkConversationAsImportantViewState()
data class Error(val exception: Exception) : MarkConversationAsImportantViewState()
}
sealed class MarkConversationAsUnimportantViewState {
data object None : MarkConversationAsUnimportantViewState()
data class Success(val statusCode: Int) : MarkConversationAsUnimportantViewState()
data class Error(val exception: Exception) : MarkConversationAsUnimportantViewState()
}
} }

View File

@ -62,7 +62,8 @@ fun ConversationModel.asEntity() =
remoteServer = remoteServer, remoteServer = remoteServer,
remoteToken = remoteToken, remoteToken = remoteToken,
hasArchived = hasArchived, hasArchived = hasArchived,
hasSensitive = hasSensitive hasSensitive = hasSensitive,
hasImportant = hasImportant
) )
fun ConversationEntity.asModel() = fun ConversationEntity.asModel() =
@ -115,7 +116,8 @@ fun ConversationEntity.asModel() =
remoteServer = remoteServer, remoteServer = remoteServer,
remoteToken = remoteToken, remoteToken = remoteToken,
hasArchived = hasArchived, hasArchived = hasArchived,
hasSensitive = hasSensitive hasSensitive = hasSensitive,
hasImportant = hasImportant
) )
fun Conversation.asEntity(accountId: Long) = fun Conversation.asEntity(accountId: Long) =
@ -167,5 +169,6 @@ fun Conversation.asEntity(accountId: Long) =
remoteServer = remoteServer, remoteServer = remoteServer,
remoteToken = remoteToken, remoteToken = remoteToken,
hasArchived = hasArchived, hasArchived = hasArchived,
hasSensitive = hasSensitive hasSensitive = hasSensitive,
hasImportant = hasImportant
) )

View File

@ -95,7 +95,8 @@ data class ConversationEntity(
@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, @ColumnInfo(name = "hasArchived") var hasArchived: Boolean = false,
@ColumnInfo(name = "hasSensitive") var hasSensitive: Boolean = false @ColumnInfo(name = "hasSensitive") var hasSensitive: Boolean = false,
@ColumnInfo(name = "hasImportant") var hasImportant: 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

@ -65,7 +65,14 @@ object Migrations {
val MIGRATION_14_15 = object : Migration(14, 15) { val MIGRATION_14_15 = object : Migration(14, 15) {
override fun migrate(db: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 14 to 15") Log.i("Migrations", "Migrating 14 to 15")
addisSensitive(db) addIsSensitive(db)
}
}
val MIGRATION_15_16 = object : Migration(15, 16) {
override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 15 to 16")
addIsImportant(db)
} }
} }
@ -290,7 +297,7 @@ object Migrations {
} }
} }
fun addisSensitive(db: SupportSQLiteDatabase) { fun addIsSensitive(db: SupportSQLiteDatabase) {
try { try {
db.execSQL( db.execSQL(
"ALTER TABLE Conversations " + "ALTER TABLE Conversations " +
@ -301,6 +308,17 @@ object Migrations {
} }
} }
fun addIsImportant(db: SupportSQLiteDatabase) {
try {
db.execSQL(
"ALTER TABLE Conversations " +
"ADD COLUMN hasImportant INTEGER NOT NULL DEFAULT 0;"
)
} catch (e: SQLException) {
Log.i("Migrations", "Something went wrong when adding column hasImportant to table Conversations")
}
}
fun addTempMessagesSupport(db: SupportSQLiteDatabase) { fun addTempMessagesSupport(db: SupportSQLiteDatabase) {
try { try {
db.execSQL( db.execSQL(

View File

@ -49,7 +49,7 @@ import java.util.Locale
ChatMessageEntity::class, ChatMessageEntity::class,
ChatBlockEntity::class ChatBlockEntity::class
], ],
version = 15, version = 16,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 9, to = 10) AutoMigration(from = 9, to = 10)
], ],
@ -117,7 +117,8 @@ abstract class TalkDatabase : RoomDatabase() {
Migrations.MIGRATION_11_12, Migrations.MIGRATION_11_12,
Migrations.MIGRATION_12_13, Migrations.MIGRATION_12_13,
Migrations.MIGRATION_13_14, Migrations.MIGRATION_13_14,
Migrations.MIGRATION_14_15 Migrations.MIGRATION_14_15,
Migrations.MIGRATION_15_16
) )
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback( .addCallback(

View File

@ -62,10 +62,10 @@ class ConversationModel(
var remoteToken: String? = null, var remoteToken: String? = null,
var hasArchived: Boolean = false, var hasArchived: Boolean = false,
var hasSensitive: Boolean = false, var hasSensitive: Boolean = false,
var hasImportant: 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
) { ) {
companion object { companion object {
@ -128,7 +128,8 @@ class ConversationModel(
remoteServer = conversation.remoteServer, remoteServer = conversation.remoteServer,
remoteToken = conversation.remoteToken, remoteToken = conversation.remoteToken,
hasArchived = conversation.hasArchived, hasArchived = conversation.hasArchived,
hasSensitive = conversation.hasSensitive hasSensitive = conversation.hasSensitive,
hasImportant = conversation.hasImportant
) )
} }
} }

View File

@ -168,5 +168,8 @@ data class Conversation(
var hasArchived: Boolean = false, var hasArchived: Boolean = false,
@JsonField(name = ["isSensitive"]) @JsonField(name = ["isSensitive"])
var hasSensitive: Boolean = false var hasSensitive: Boolean = false,
@JsonField(name = ["isImportant"])
var hasImportant: Boolean = false
) : Parcelable ) : Parcelable

View File

@ -53,4 +53,8 @@ interface ConversationsRepository {
suspend fun markConversationAsSensitive(credentials: String, baseUrl: String, roomToken: String): GenericOverall suspend fun markConversationAsSensitive(credentials: String, baseUrl: String, roomToken: String): GenericOverall
suspend fun markConversationAsInsensitive(credentials: String, baseUrl: String, roomToken: String): GenericOverall suspend fun markConversationAsInsensitive(credentials: String, baseUrl: String, roomToken: String): GenericOverall
suspend fun markConversationAsImportant(credentials: String, baseUrl: String, roomToken: String): GenericOverall
suspend fun markConversationAsUnImportant(credentials: String, baseUrl: String, roomToken: String): GenericOverall
} }

View File

@ -139,6 +139,24 @@ class ConversationsRepositoryImpl(
return coroutineApi.markConversationAsInsensitive(credentials, url) return coroutineApi.markConversationAsInsensitive(credentials, url)
} }
override suspend fun markConversationAsImportant(
credentials: String,
baseUrl: String,
roomToken: String
): GenericOverall {
val url = ApiUtils.getUrlForImportantConversation(baseUrl, roomToken)
return coroutineApi.markConversationAsImportant(credentials, url)
}
override suspend fun markConversationAsUnImportant(
credentials: String,
baseUrl: String,
roomToken: String
): GenericOverall {
val url = ApiUtils.getUrlForImportantConversation(baseUrl, roomToken)
return coroutineApi.markConversationAsUnimportant(credentials, url)
}
override suspend fun banActor( override suspend fun banActor(
credentials: String, credentials: String,
url: String, url: String,

View File

@ -205,6 +205,10 @@ object ApiUtils {
return getUrlForParticipants(version, baseUrl, token) + "/active" return getUrlForParticipants(version, baseUrl, token) + "/active"
} }
fun getUrlForImportantConversation(baseUrl: String, roomToken: String): String {
return "$baseUrl$OCS_API_VERSION/apps/spreed/api/v4/room/$roomToken/important"
}
@JvmStatic @JvmStatic
fun getUrlForParticipantsSelf(version: Int, baseUrl: String?, token: String?): String { fun getUrlForParticipantsSelf(version: Int, baseUrl: String?, token: String?): String {
return getUrlForParticipants(version, baseUrl, token) + "/self" return getUrlForParticipants(version, baseUrl, token) + "/self"

View File

@ -59,7 +59,8 @@ enum class SpreedFeatures(val value: String) {
ARCHIVE_CONVERSATIONS("archived-conversations-v2"), ARCHIVE_CONVERSATIONS("archived-conversations-v2"),
CONVERSATION_CREATION_ALL("conversation-creation-all"), CONVERSATION_CREATION_ALL("conversation-creation-all"),
UNBIND_CONVERSATION("unbind-conversation"), UNBIND_CONVERSATION("unbind-conversation"),
SENSITIVE_CONVERSATIONS("sensitive-conversations") SENSITIVE_CONVERSATIONS("sensitive-conversations"),
IMPORTANT_CONVERSATIONS("important-conversations")
} }
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")

View File

@ -62,7 +62,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/standard_margin" android:layout_marginStart="@dimen/standard_margin"
android:clickable="false" /> android:checked="false"
android:clickable="false"/>
</LinearLayout> </LinearLayout>
@ -111,7 +112,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin" android:layout_marginStart="@dimen/standard_margin"
android:checked="true" android:checked="true"
android:clickable="false" /> android:clickable="false"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout

View File

@ -242,6 +242,8 @@ How to translate with transifex:
<string name="nc_clear_history_success">All messages were deleted</string> <string name="nc_clear_history_success">All messages were deleted</string>
<string name="nc_mark_conversation_as_sensitive">Conversation marked as sensitive</string> <string name="nc_mark_conversation_as_sensitive">Conversation marked as sensitive</string>
<string name="nc_mark_conversation_as_insensitive">Conversation unmarked as sensitive</string> <string name="nc_mark_conversation_as_insensitive">Conversation unmarked as sensitive</string>
<string name="nc_mark_conversation_as_important">Conversation marked as important</string>
<string name="nc_mark_conversation_as_unimportant">Conversation unmarked as important</string>
<string name="nc_rename">Rename conversation</string> <string name="nc_rename">Rename conversation</string>
<string name="nc_rename_confirm">Rename</string> <string name="nc_rename_confirm">Rename</string>
<string name="nc_delete_call">Delete conversation</string> <string name="nc_delete_call">Delete conversation</string>
@ -350,7 +352,7 @@ How to translate with transifex:
<string name="nc_sensitive_conversation">Sensitive conversation</string> <string name="nc_sensitive_conversation">Sensitive conversation</string>
<string name="nc_sensitive_conversation_hint">Message preview will be disabled in conversation list and notifications</string> <string name="nc_sensitive_conversation_hint">Message preview will be disabled in conversation list and notifications</string>
<string name="nc_important_conversation">Important conversation</string> <string name="nc_important_conversation">Important conversation</string>
<string name="nc_important_conversation_desc">Notifications in this conversation will override Do Not Disturb settings</string> <string name="nc_important_conversation_desc">\"Do not disturb\" user status is ignored for important conversations</string>
<string name="nc_all_ok_operation">OK, all done!</string> <string name="nc_all_ok_operation">OK, all done!</string>
<string name="nc_ok">OK</string> <string name="nc_ok">OK</string>