Merge pull request #4846 from nextcloud/event_conversations

Event conversations
This commit is contained in:
Marcel Hibbe 2025-05-14 16:00:24 +00:00 committed by GitHub
commit 52f58372b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1170 additions and 24 deletions

View File

@ -0,0 +1,719 @@
{
"formatVersion": 1,
"database": {
"version": 14,
"identityHash": "506abc931eb3b657cafe6ad1b25f635d",
"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, 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
}
],
"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, '506abc931eb3b657cafe6ad1b25f635d')"
]
}
}

View File

@ -215,6 +215,7 @@ class ChatBlocksDaoTest {
lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY, lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY,
lobbyTimer = 0, lobbyTimer = 0,
objectType = ConversationEnums.ObjectType.FILE, objectType = ConversationEnums.ObjectType.FILE,
objectId = "",
statusIcon = null, statusIcon = null,
description = "", description = "",
displayName = "", displayName = "",

View File

@ -211,6 +211,7 @@ class ChatMessagesDaoTest {
lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY, lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY,
lobbyTimer = 0, lobbyTimer = 0,
objectType = ConversationEnums.ObjectType.FILE, objectType = ConversationEnums.ObjectType.FILE,
objectId = "",
statusIcon = null, statusIcon = null,
description = "", description = "",
displayName = "", displayName = "",

View File

@ -38,12 +38,14 @@ import android.view.Gravity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.AbsListView import android.widget.AbsListView
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.PopupMenu import android.widget.PopupMenu
import android.widget.PopupWindow
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
@ -51,6 +53,7 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -82,6 +85,7 @@ import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.target.Target import coil.target.Target
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.ui.color.ColorUtil import com.nextcloud.android.common.ui.color.ColorUtil
import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.android.common.ui.theme.utils.ColorRole
@ -127,6 +131,7 @@ import com.nextcloud.talk.databinding.ActivityChatBinding
import com.nextcloud.talk.events.UserMentionClickEvent import com.nextcloud.talk.events.UserMentionClickEvent
import com.nextcloud.talk.events.WebSocketCommunicationEvent import com.nextcloud.talk.events.WebSocketCommunicationEvent
import com.nextcloud.talk.extensions.loadAvatarOrImagePreview import com.nextcloud.talk.extensions.loadAvatarOrImagePreview
import com.nextcloud.talk.jobs.DeleteConversationWorker
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
import com.nextcloud.talk.jobs.ShareOperationWorker import com.nextcloud.talk.jobs.ShareOperationWorker
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
@ -210,13 +215,18 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.set
import kotlin.math.roundToInt import kotlin.math.roundToInt
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.models.json.participants.Participant
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
class ChatActivity : class ChatActivity :
@ -254,6 +264,8 @@ class ChatActivity :
lateinit var networkMonitor: NetworkMonitor lateinit var networkMonitor: NetworkMonitor
lateinit var chatViewModel: ChatViewModel lateinit var chatViewModel: ChatViewModel
lateinit var conversationInfoViewModel: ConversationInfoViewModel
lateinit var messageInputViewModel: MessageInputViewModel lateinit var messageInputViewModel: MessageInputViewModel
private val startSelectContactForResult = registerForActivityResult( private val startSelectContactForResult = registerForActivityResult(
@ -324,6 +336,7 @@ class ChatActivity :
private var conversationVoiceCallMenuItem: MenuItem? = null private var conversationVoiceCallMenuItem: MenuItem? = null
private var conversationVideoMenuItem: MenuItem? = null private var conversationVideoMenuItem: MenuItem? = null
private var eventConversationMenuItem: MenuItem? = null
var webSocketInstance: WebSocketInstance? = null var webSocketInstance: WebSocketInstance? = null
var signalingMessageSender: SignalingMessageSender? = null var signalingMessageSender: SignalingMessageSender? = null
@ -418,6 +431,8 @@ class ChatActivity :
chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java] chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java]
conversationInfoViewModel = ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken) val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token) val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
chatViewModel.initData( chatViewModel.initData(
@ -567,6 +582,7 @@ class ChatActivity :
participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!) participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)
invalidateOptionsMenu() invalidateOptionsMenu()
isEventConversation()
checkShowCallButtons() checkShowCallButtons()
checkLobbyState() checkLobbyState()
updateRoomTimerHandler() updateRoomTimerHandler()
@ -600,6 +616,7 @@ class ChatActivity :
loadAvatarForStatusBar() loadAvatarForStatusBar()
setupSwipeToReply() setupSwipeToReply()
setActionBarTitle() setActionBarTitle()
isEventConversation()
checkShowCallButtons() checkShowCallButtons()
checkLobbyState() checkLobbyState()
if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL &&
@ -1889,6 +1906,17 @@ class ChatActivity :
} }
} }
private fun isEventConversation() {
if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT) {
if (eventConversationMenuItem != null) {
eventConversationMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
eventConversationMenuItem?.isEnabled = true
}
} else {
eventConversationMenuItem?.isEnabled = false
}
}
private fun isReadOnlyConversation(): Boolean = private fun isReadOnlyConversation(): Boolean =
currentConversation?.conversationReadOnlyState != null && currentConversation?.conversationReadOnlyState != null &&
currentConversation?.conversationReadOnlyState == currentConversation?.conversationReadOnlyState ==
@ -2849,12 +2877,19 @@ class ChatActivity :
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.menu_conversation, menu) menuInflater.inflate(R.menu.menu_conversation, menu)
if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT) {
eventConversationMenuItem = menu.findItem(R.id.conversation_event)
} else {
menu.removeItem(R.id.conversation_event)
}
if (conversationUser?.userId == "?") { if (conversationUser?.userId == "?") {
menu.removeItem(R.id.conversation_info) menu.removeItem(R.id.conversation_info)
} else { } else {
loadAvatarForStatusBar() loadAvatarForStatusBar()
setActionBarTitle() setActionBarTitle()
} }
return true return true
} }
@ -2871,12 +2906,6 @@ class ChatActivity :
searchItem.isVisible = CapabilitiesUtil.isUnifiedSearchAvailable(spreedCapabilities) && searchItem.isVisible = CapabilitiesUtil.isUnifiedSearchAvailable(spreedCapabilities) &&
currentConversation!!.remoteServer.isNullOrEmpty() currentConversation!!.remoteServer.isNullOrEmpty()
if (currentConversation!!.remoteServer != null ||
!CapabilitiesUtil.isSharedItemsAvailable(spreedCapabilities)
) {
menu.removeItem(R.id.shared_items)
}
if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) { if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) {
conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call) conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call) conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
@ -2909,7 +2938,6 @@ class ChatActivity :
menu.removeItem(R.id.conversation_voice_call) menu.removeItem(R.id.conversation_voice_call)
} }
} }
return true return true
} }
@ -2940,9 +2968,205 @@ class ChatActivity :
true true
} }
R.id.conversation_event -> {
val anchorView = findViewById<View>(R.id.conversation_event)
showPopupWindow(anchorView)
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
private fun showPopupWindow(anchorView: View) {
val popupView = layoutInflater.inflate(R.layout.item_event_schedule, null)
val titleTextView = popupView.findViewById<TextView>(R.id.event_scheduled)
val subtitleTextView = popupView.findViewById<TextView>(R.id.meetingTime)
val popupWindow = PopupWindow(
popupView,
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
true
)
popupWindow.isOutsideTouchable = true
popupWindow.isFocusable = true
popupWindow.showAsDropDown(anchorView, 0, -anchorView.height)
val meetingStatus = showEventSchedule()
subtitleTextView.text = meetingStatus
deleteEventConversation(meetingStatus, popupWindow, popupView)
archiveEventConversation(meetingStatus, popupWindow, popupView)
}
private fun deleteEventConversation(meetingStatus: String, popupWindow: PopupWindow, popupView: View) {
val deleteConversation = popupView.findViewById<TextView>(R.id.delete_conversation)
if (meetingStatus == context.resources.getString(R.string.nc_meeting_ended) &&
currentConversation?.canDeleteConversation == true
) {
deleteConversation.visibility = View.VISIBLE
deleteConversation.setOnClickListener {
val dialogBuilder = MaterialAlertDialogBuilder(it.context)
.setIcon(
viewThemeUtils.dialog
.colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp)
)
.setTitle(R.string.nc_delete_call)
.setMessage(R.string.nc_delete_conversation_more)
.setPositiveButton(R.string.nc_delete) { _, _ ->
currentConversation?.let { conversation ->
deleteConversation(conversation)
}
}
.setNegativeButton(R.string.nc_cancel) { _, _ ->
}
viewThemeUtils.dialog
.colorMaterialAlertDialogBackground(it.context, dialogBuilder)
val dialog = dialogBuilder.show()
viewThemeUtils.platform.colorTextButtons(
dialog.getButton(AlertDialog.BUTTON_POSITIVE),
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
)
popupWindow.dismiss()
}
} else {
deleteConversation.visibility = View.GONE
}
}
private fun archiveEventConversation(meetingStatus: String, popupWindow: PopupWindow, popupView: View) {
val archiveConversation = popupView.findViewById<TextView>(R.id.archive_conversation)
val unarchiveConversation = popupView.findViewById<TextView>(R.id.unarchive_conversation)
if (meetingStatus == context.resources.getString(R.string.nc_meeting_ended) &&
(
Participant.ParticipantType.MODERATOR == currentConversation?.participantType ||
Participant.ParticipantType.OWNER == currentConversation?.participantType
)
) {
if (currentConversation?.hasArchived == false) {
unarchiveConversation.visibility = View.GONE
archiveConversation.visibility = View.VISIBLE
archiveConversation.setOnClickListener {
this.lifecycleScope.launch {
conversationInfoViewModel.archiveConversation(conversationUser!!, currentConversation?.token!!)
Snackbar.make(
binding.root,
String.format(
context.resources.getString(R.string.archived_conversation),
currentConversation?.displayName
),
Snackbar.LENGTH_LONG
).show()
}
popupWindow.dismiss()
}
} else {
unarchiveConversation.visibility = View.VISIBLE
archiveConversation.visibility = View.GONE
unarchiveConversation.setOnClickListener {
this.lifecycleScope.launch {
conversationInfoViewModel.unarchiveConversation(
conversationUser!!,
currentConversation?.token!!
)
Snackbar.make(
binding.root,
String.format(
context.resources.getString(R.string.unarchived_conversation),
currentConversation?.displayName
),
Snackbar.LENGTH_LONG
).show()
}
popupWindow.dismiss()
}
}
} else {
archiveConversation.visibility = View.GONE
unarchiveConversation.visibility = View.GONE
}
}
private fun deleteConversation(conversation: ConversationModel) {
val data = Data.Builder()
data.putLong(
KEY_INTERNAL_USER_ID,
conversationUser?.id!!
)
data.putString(KEY_ROOM_TOKEN, conversation.token)
val deleteConversationWorker =
OneTimeWorkRequest.Builder(DeleteConversationWorker::class.java).setInputData(data.build()).build()
WorkManager.getInstance().enqueue(deleteConversationWorker)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(deleteConversationWorker.id)
.observeForever { workInfo: WorkInfo? ->
if (workInfo != null) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
val successMessage = String.format(
context.resources.getString(R.string.deleted_conversation),
conversation.displayName
)
Snackbar.make(binding.root, successMessage, Snackbar.LENGTH_LONG).show()
finish()
}
WorkInfo.State.FAILED -> {
val errorMessage = context.resources.getString(R.string.nc_common_error_sorry)
Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_LONG).show()
}
else -> {
}
}
}
}
}
private fun showEventSchedule(): String {
val meetingTimeStamp = currentConversation?.objectId ?: ""
val status = getMeetingSchedule(meetingTimeStamp)
return status
}
private fun getMeetingSchedule(meetingTimeStamp: String): String {
val timestamps = meetingTimeStamp.split("#")
if (timestamps.size != 2) return context.resources.getString(R.string.nc_invalid_time)
val startEpoch = timestamps[ZERO_INDEX].toLong()
val endEpoch = timestamps[ONE_INDEX].toLong()
val startDateTime = Instant.ofEpochSecond(startEpoch).atZone(ZoneId.systemDefault())
val endDateTime = Instant.ofEpochSecond(endEpoch).atZone(ZoneId.systemDefault())
val currentTime = ZonedDateTime.now(ZoneId.systemDefault())
return when {
currentTime.isBefore(startDateTime) -> {
val isToday = startDateTime.toLocalDate().isEqual(currentTime.toLocalDate())
val isTomorrow = startDateTime.toLocalDate().isEqual(currentTime.toLocalDate().plusDays(1))
when {
isToday -> String.format(
context.resources.getString(R.string.nc_today_meeting),
startDateTime.format(DateTimeFormatter.ofPattern("HH:mm"))
)
isTomorrow -> String.format(
context.resources.getString(R.string.nc_tomorrow_meeting),
startDateTime.format(DateTimeFormatter.ofPattern("HH:mm"))
)
else -> startDateTime.format(DateTimeFormatter.ofPattern("MMM d, yyyy, HH:mm"))
}
}
currentTime.isAfter(endDateTime) -> context.resources.getString(R.string.nc_meeting_ended)
else -> context.resources.getString(R.string.nc_ongoing_meeting)
}
}
private fun showSharedItems() { private fun showSharedItems() {
val intent = Intent(this, SharedItemsActivity::class.java) val intent = Intent(this, SharedItemsActivity::class.java)
intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName) intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
@ -3800,5 +4024,7 @@ class ChatActivity :
const val VOICE_MESSAGE_PLAY_ADD_THRESHOLD = 0.1 const val VOICE_MESSAGE_PLAY_ADD_THRESHOLD = 0.1
const val VOICE_MESSAGE_MARK_PLAYED_FACTOR = 20 const val VOICE_MESSAGE_MARK_PLAYED_FACTOR = 20
const val OUT_OF_OFFICE_ALPHA = 76 const val OUT_OF_OFFICE_ALPHA = 76
const val ZERO_INDEX = 0
const val ONE_INDEX = 1
} }
} }

View File

@ -185,6 +185,11 @@ class ConversationInfoEditActivity : BaseActivity() {
binding.conversationDescription.isEnabled = false binding.conversationDescription.isEnabled = false
} }
if (conversation?.objectType == ConversationEnums.ObjectType.EVENT) {
binding.conversationName.isEnabled = false
binding.conversationDescription.isEnabled = false
}
loadConversationAvatar() loadConversationAvatar()
} }
@ -271,8 +276,10 @@ class ConversationInfoEditActivity : BaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.save) { if (item.itemId == R.id.save) {
if (conversation?.objectType != ConversationEnums.ObjectType.EVENT) {
saveConversationNameAndDescription() saveConversationNameAndDescription()
} }
}
return true return true
} }

View File

@ -210,6 +210,7 @@ class ConversationsListActivity :
private var conversationItemsWithHeader: MutableList<AbstractFlexibleItem<*>> = ArrayList() private var conversationItemsWithHeader: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private val searchableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList() private val searchableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var filterableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList() private var filterableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var nearFutureEventConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var searchItem: MenuItem? = null private var searchItem: MenuItem? = null
private var chooseAccountItem: MenuItem? = null private var chooseAccountItem: MenuItem? = null
private var searchView: SearchView? = null private var searchView: SearchView? = null
@ -519,16 +520,29 @@ class ConversationsListActivity :
// Update Conversations // Update Conversations
conversationItems.clear() conversationItems.clear()
conversationItemsWithHeader.clear() conversationItemsWithHeader.clear()
nearFutureEventConversationItems.clear()
for (conversation in list) { for (conversation in list) {
if (!futureEvent(conversation)) {
addToNearFutureEventConversationItems(conversation)
}
addToConversationItems(conversation) addToConversationItems(conversation)
} }
sortConversations(conversationItems) sortConversations(conversationItems)
sortConversations(conversationItemsWithHeader) sortConversations(conversationItemsWithHeader)
sortConversations(nearFutureEventConversationItems)
if (!hasFilterEnabled() && searchBehaviorSubject.value == false) {
adapter?.updateDataSet(nearFutureEventConversationItems, false)
} else {
// Filter Conversations // Filter Conversations
if (!hasFilterEnabled()) 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())
// Fetch Open Conversations // Fetch Open Conversations
@ -550,6 +564,26 @@ class ConversationsListActivity :
return false return false
} }
private fun futureEvent(conversation: ConversationModel): Boolean {
if (!conversation.objectId.contains("#")) {
return false
}
return conversation.objectType == ConversationEnums.ObjectType.EVENT &&
(conversation.objectId.split("#")[0].toLong() - (System.currentTimeMillis() / LONG_1000)) >
AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS
}
fun showOnlyNearFutureEvents() {
sortConversations(nearFutureEventConversationItems)
adapter?.updateDataSet(nearFutureEventConversationItems, false)
adapter?.smoothScrollToPosition(0)
}
private fun addToNearFutureEventConversationItems(conversation: ConversationModel) {
val conversationItem = ConversationItem(conversation, currentUser!!, this, null, viewThemeUtils)
nearFutureEventConversationItems.add(conversationItem)
}
fun filterConversation() { fun filterConversation() {
val accountId = UserIdUtils.getIdForUser(currentUser) val accountId = UserIdUtils.getIdForUser(currentUser)
filterState[FilterConversationFragment.UNREAD] = ( filterState[FilterConversationFragment.UNREAD] = (
@ -837,15 +871,19 @@ class ConversationsListActivity :
override fun onMenuItemActionCollapse(item: MenuItem): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
adapter?.setHeadersShown(false) adapter?.setHeadersShown(false)
searchBehaviorSubject.onNext(false)
if (!hasFilterEnabled()) filterableConversationItems = conversationItemsWithHeader if (!hasFilterEnabled()) filterableConversationItems = conversationItemsWithHeader
adapter?.updateDataSet(filterableConversationItems, false) if (!hasFilterEnabled()) {
adapter?.updateDataSet(nearFutureEventConversationItems, false)
} else {
filterableConversationItems = conversationItems
}
adapter?.hideAllHeaders() adapter?.hideAllHeaders()
if (searchHelper != null) { if (searchHelper != null) {
// cancel any pending searches // cancel any pending searches
searchHelper!!.cancelSearch() searchHelper!!.cancelSearch()
} }
binding.swipeRefreshLayoutView.isRefreshing = false binding.swipeRefreshLayoutView.isRefreshing = false
searchBehaviorSubject.onNext(false)
binding.swipeRefreshLayoutView.isEnabled = true binding.swipeRefreshLayoutView.isEnabled = true
searchView!!.onActionViewCollapsed() searchView!!.onActionViewCollapsed()
@ -2114,5 +2152,7 @@ class ConversationsListActivity :
const val NOTIFICATION_WARNING_DATE_NOT_SET = 0L const val NOTIFICATION_WARNING_DATE_NOT_SET = 0L
const val OFFSET_HEIGHT_DIVIDER: Int = 3 const val OFFSET_HEIGHT_DIVIDER: Int = 3
const val ROOM_TYPE_ONE_ONE = "1" const val ROOM_TYPE_ONE_ONE = "1"
private const val AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS: Long = 57600
const val LONG_1000: Long = 1000
} }
} }

View File

@ -34,6 +34,7 @@ fun ConversationModel.asEntity() =
unreadMention = unreadMention, unreadMention = unreadMention,
lastMessage = lastMessage?.let { LoganSquare.serialize(lastMessage) }, lastMessage = lastMessage?.let { LoganSquare.serialize(lastMessage) },
objectType = objectType, objectType = objectType,
objectId = objectId,
notificationLevel = notificationLevel, notificationLevel = notificationLevel,
conversationReadOnlyState = conversationReadOnlyState, conversationReadOnlyState = conversationReadOnlyState,
lobbyState = lobbyState, lobbyState = lobbyState,
@ -85,6 +86,7 @@ fun ConversationEntity.asModel() =
lastMessage = lastMessage?.let lastMessage = lastMessage?.let
{ LoganSquare.parse(lastMessage, ChatMessageJson::class.java) }, { LoganSquare.parse(lastMessage, ChatMessageJson::class.java) },
objectType = objectType, objectType = objectType,
objectId = objectId,
notificationLevel = notificationLevel, notificationLevel = notificationLevel,
conversationReadOnlyState = conversationReadOnlyState, conversationReadOnlyState = conversationReadOnlyState,
lobbyState = lobbyState, lobbyState = lobbyState,
@ -135,6 +137,7 @@ fun Conversation.asEntity(accountId: Long) =
unreadMention = unreadMention, unreadMention = unreadMention,
lastMessage = lastMessage?.let { LoganSquare.serialize(lastMessage) }, lastMessage = lastMessage?.let { LoganSquare.serialize(lastMessage) },
objectType = objectType, objectType = objectType,
objectId = objectId,
notificationLevel = notificationLevel, notificationLevel = notificationLevel,
conversationReadOnlyState = conversationReadOnlyState, conversationReadOnlyState = conversationReadOnlyState,
lobbyState = lobbyState, lobbyState = lobbyState,

View File

@ -78,6 +78,7 @@ data class ConversationEntity(
@ColumnInfo(name = "notificationCalls") var notificationCalls: Int = 0, @ColumnInfo(name = "notificationCalls") var notificationCalls: Int = 0,
@ColumnInfo(name = "notificationLevel") var notificationLevel: ConversationEnums.NotificationLevel, @ColumnInfo(name = "notificationLevel") var notificationLevel: ConversationEnums.NotificationLevel,
@ColumnInfo(name = "objectType") var objectType: ConversationEnums.ObjectType, @ColumnInfo(name = "objectType") var objectType: ConversationEnums.ObjectType,
@ColumnInfo(name = "objectId") var objectId: String,
@ColumnInfo(name = "participantType") var participantType: Participant.ParticipantType, @ColumnInfo(name = "participantType") var participantType: Participant.ParticipantType,
@ColumnInfo(name = "permissions") var permissions: Int = 0, @ColumnInfo(name = "permissions") var permissions: Int = 0,
@ColumnInfo(name = "readOnly") var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState, @ColumnInfo(name = "readOnly") var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState,

View File

@ -55,6 +55,13 @@ object Migrations {
} }
} }
val MIGRATION_13_14 = object : Migration(13, 14) {
override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 13 to 14")
addObjectId(db)
}
}
fun migrateToRoom(db: SupportSQLiteDatabase) { fun migrateToRoom(db: SupportSQLiteDatabase) {
db.execSQL( db.execSQL(
"CREATE TABLE User_new (" + "CREATE TABLE User_new (" +
@ -265,6 +272,17 @@ object Migrations {
} }
} }
fun addObjectId(db: SupportSQLiteDatabase) {
try {
db.execSQL(
"ALTER TABLE Conversations " +
"ADD COLUMN objectId TEXT NOT NULL DEFAULT '';"
)
} catch (e: SQLException) {
Log.i("Migrations", "Something went wrong when adding column objectId to table Conversations")
}
}
fun addTempMessagesSupport(db: SupportSQLiteDatabase) { fun addTempMessagesSupport(db: SupportSQLiteDatabase) {
try { try {
db.execSQL( db.execSQL(

View File

@ -49,9 +49,9 @@ import java.util.Locale
ChatMessageEntity::class, ChatMessageEntity::class,
ChatBlockEntity::class ChatBlockEntity::class
], ],
version = 13, version = 14,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 9, to = 11) AutoMigration(from = 9, to = 10)
], ],
exportSchema = true exportSchema = true
) )
@ -115,7 +115,8 @@ abstract class TalkDatabase : RoomDatabase() {
Migrations.MIGRATION_8_9, Migrations.MIGRATION_8_9,
Migrations.MIGRATION_10_11, Migrations.MIGRATION_10_11,
Migrations.MIGRATION_11_12, Migrations.MIGRATION_11_12,
Migrations.MIGRATION_12_13 Migrations.MIGRATION_12_13,
Migrations.MIGRATION_13_14
) )
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback( .addCallback(

View File

@ -33,6 +33,7 @@ class ConversationModel(
var unreadMention: Boolean = false, var unreadMention: Boolean = false,
var lastMessage: ChatMessageJson? = null, var lastMessage: ChatMessageJson? = null,
var objectType: ConversationEnums.ObjectType, var objectType: ConversationEnums.ObjectType,
var objectId: String = "",
var notificationLevel: ConversationEnums.NotificationLevel, var notificationLevel: ConversationEnums.NotificationLevel,
var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState, var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState,
var lobbyState: ConversationEnums.LobbyState, var lobbyState: ConversationEnums.LobbyState,
@ -66,6 +67,7 @@ class ConversationModel(
) { ) {
companion object { companion object {
@Suppress("LongMethod")
fun mapToConversationModel(conversation: Conversation, user: User): ConversationModel { fun mapToConversationModel(conversation: Conversation, user: User): ConversationModel {
return ConversationModel( return ConversationModel(
internalId = user.id!!.toString() + "@" + conversation.token, internalId = user.id!!.toString() + "@" + conversation.token,
@ -88,6 +90,7 @@ class ConversationModel(
unreadMention = conversation.unreadMention, unreadMention = conversation.unreadMention,
lastMessage = conversation.lastMessage, lastMessage = conversation.lastMessage,
objectType = conversation.objectType.let { ConversationEnums.ObjectType.valueOf(it.name) }, objectType = conversation.objectType.let { ConversationEnums.ObjectType.valueOf(it.name) },
objectId = conversation.objectId,
notificationLevel = conversation.notificationLevel.let { notificationLevel = conversation.notificationLevel.let {
ConversationEnums.NotificationLevel.valueOf( ConversationEnums.NotificationLevel.valueOf(
it.name it.name

View File

@ -79,6 +79,9 @@ data class Conversation(
@JsonField(name = ["objectType"], typeConverter = ConversationObjectTypeConverter::class) @JsonField(name = ["objectType"], typeConverter = ConversationObjectTypeConverter::class)
var objectType: ConversationEnums.ObjectType = ConversationEnums.ObjectType.DEFAULT, var objectType: ConversationEnums.ObjectType = ConversationEnums.ObjectType.DEFAULT,
@JsonField(name = ["objectId"])
var objectId: String = "",
@JsonField(name = ["notificationLevel"], typeConverter = EnumNotificationLevelConverter::class) @JsonField(name = ["notificationLevel"], typeConverter = EnumNotificationLevelConverter::class)
var notificationLevel: ConversationEnums.NotificationLevel = ConversationEnums.NotificationLevel.DEFAULT, var notificationLevel: ConversationEnums.NotificationLevel = ConversationEnums.NotificationLevel.DEFAULT,

View File

@ -43,6 +43,7 @@ class ConversationEnums {
DEFAULT, DEFAULT,
SHARE_PASSWORD, SHARE_PASSWORD,
FILE, FILE,
ROOM ROOM,
EVENT
} }
} }

View File

@ -15,6 +15,7 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter<ConversationEnu
"share:password" -> ConversationEnums.ObjectType.SHARE_PASSWORD "share:password" -> ConversationEnums.ObjectType.SHARE_PASSWORD
"room" -> ConversationEnums.ObjectType.ROOM "room" -> ConversationEnums.ObjectType.ROOM
"file" -> ConversationEnums.ObjectType.FILE "file" -> ConversationEnums.ObjectType.FILE
"event" -> ConversationEnums.ObjectType.EVENT
else -> ConversationEnums.ObjectType.DEFAULT else -> ConversationEnums.ObjectType.DEFAULT
} }
} }
@ -28,6 +29,7 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter<ConversationEnu
ConversationEnums.ObjectType.SHARE_PASSWORD -> "share:password" ConversationEnums.ObjectType.SHARE_PASSWORD -> "share:password"
ConversationEnums.ObjectType.ROOM -> "room" ConversationEnums.ObjectType.ROOM -> "room"
ConversationEnums.ObjectType.FILE -> "file" ConversationEnums.ObjectType.FILE -> "file"
ConversationEnums.ObjectType.EVENT -> "event"
else -> "" else -> ""
} }
} }

View File

@ -7,6 +7,7 @@
package com.nextcloud.talk.ui.dialog package com.nextcloud.talk.ui.dialog
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -99,6 +100,14 @@ class FilterConversationFragment : DialogFragment() {
} }
binding.buttonClose.setOnClickListener { binding.buttonClose.setOnClickListener {
val noFiltersActive = !(
filterState[MENTION] == true ||
filterState[UNREAD] == true ||
filterState[ARCHIVE] == true
)
if (noFiltersActive) {
(requireActivity() as ConversationsListActivity).showOnlyNearFutureEvents()
}
dismiss() dismiss()
} }
} }
@ -130,6 +139,18 @@ class FilterConversationFragment : DialogFragment() {
(requireActivity() as ConversationsListActivity).filterConversation() (requireActivity() as ConversationsListActivity).filterConversation()
} }
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
val noFiltersActive = !(
filterState[MENTION] == true ||
filterState[UNREAD] == true ||
filterState[ARCHIVE] == true
)
if (noFiltersActive) {
(requireActivity() as ConversationsListActivity).showOnlyNearFutureEvents()
}
}
companion object { companion object {
private const val FILTER_STATE_ARG = "FILTER_STATE_ARG" private const val FILTER_STATE_ARG = "FILTER_STATE_ARG"

View File

@ -0,0 +1,19 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kch@gmail.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="20dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="20dp">
<path android:fillColor="@android:color/white"
android:pathData="M20,3h-1L19,1h-2v2L7,3L7,1L5,1v2L4,3c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,5c0,-1.1 -0.9,-2 -2,-2zM20,21L4,21L4,8h16v13z"/>
</vector>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kota@gmail.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/popup_menu_color">
<TextView
android:id="@+id/event_scheduled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nc_event_schedule"
android:textStyle="bold"
android:textSize="18sp" />
<TextView
android:id="@+id/meetingTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp"
tools:text="Meeting at 8:00 pm"/>
<TextView
android:id="@+id/delete_conversation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nc_delete_call"
android:textColor="@android:color/holo_red_dark"
android:visibility = "gone"
android:textSize="18sp"
android:paddingTop="24dp"/>
<TextView
android:id="@+id/archive_conversation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/archive_conversation"
android:visibility = "gone"
android:textSize="18sp"
android:paddingTop="24dp"/>
<TextView
android:id="@+id/unarchive_conversation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/unarchive_conversation"
android:visibility = "gone"
android:textSize="18sp"
android:paddingTop="24dp"/>
</LinearLayout>

View File

@ -8,36 +8,45 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/conversation_event"
android:icon="@drawable/baseline_calendar_today_24"
android:orderInCategory="0"
android:title="@string/nc_event_conversation_menu"
app:showAsAction="ifRoom">
</item>
<item <item
android:id="@+id/conversation_voice_call" android:id="@+id/conversation_voice_call"
android:icon="@drawable/ic_call_white_24dp" android:icon="@drawable/ic_call_white_24dp"
android:orderInCategory="0" android:orderInCategory="1"
android:title="@string/nc_conversation_menu_voice_call" android:title="@string/nc_conversation_menu_voice_call"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/conversation_video_call" android:id="@+id/conversation_video_call"
android:icon="@drawable/ic_videocam_white_24px" android:icon="@drawable/ic_videocam_white_24px"
android:orderInCategory="1" android:orderInCategory="2"
android:title="@string/nc_conversation_menu_video_call" android:title="@string/nc_conversation_menu_video_call"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/conversation_search" android:id="@+id/conversation_search"
android:icon="@drawable/ic_search_white_24dp" android:icon="@drawable/ic_search_white_24dp"
android:orderInCategory="2" android:orderInCategory="3"
android:title="@string/nc_search" android:title="@string/nc_search"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/conversation_info" android:id="@+id/conversation_info"
android:orderInCategory="3" android:orderInCategory="4"
android:title="@string/nc_conversation_menu_conversation_info" android:title="@string/nc_conversation_menu_conversation_info"
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/shared_items" android:id="@+id/shared_items"
android:orderInCategory="4" android:orderInCategory="5"
android:title="@string/nc_shared_items" android:title="@string/nc_shared_items"
app:showAsAction="never" /> app:showAsAction="never" />
</menu> </menu>

View File

@ -12,6 +12,7 @@
<color name="colorPrimaryDark">#006AA3</color> <color name="colorPrimaryDark">#006AA3</color>
<color name="colorAccent">@color/colorPrimary</color> <color name="colorAccent">@color/colorPrimary</color>
<color name="disabled_text">#ff6F6F6F</color> <color name="disabled_text">#ff6F6F6F</color>
<color name="popup_menu_color">#FF37474F</color>
<!-- App bar --> <!-- App bar -->
<color name="appbar">#1E1E1E</color> <color name="appbar">#1E1E1E</color>

View File

@ -13,6 +13,8 @@
<color name="disabled_text">#ff888888</color> <color name="disabled_text">#ff888888</color>
<color name="textColorOnPrimaryBackground">#ffffff</color> <!-- white/black depending on primary color --> <color name="textColorOnPrimaryBackground">#ffffff</color> <!-- white/black depending on primary color -->
<color name="nc_login_text_color">#B3FFFFFF</color> <color name="nc_login_text_color">#B3FFFFFF</color>
<color name="popup_menu_color">#FF607D8B</color>
<!-- App bar --> <!-- App bar -->
<color name="appbar">@android:color/white</color> <color name="appbar">@android:color/white</color>

View File

@ -242,8 +242,14 @@ How to translate with transifex:
<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>
<string name="nc_event_schedule">Schedule</string>
<string name="nc_delete">Delete</string> <string name="nc_delete">Delete</string>
<string name="nc_delete_all">Delete all</string> <string name="nc_delete_all">Delete all</string>
<string name="nc_ongoing_meeting">Ongoing meeting</string>
<string name="nc_meeting_ended">Meeting ended</string>
<string name="nc_invalid_time">Invalid time</string>
<string name="nc_today_meeting">Today at %1$s</string>
<string name="nc_tomorrow_meeting">Tomorrow at %1$s</string>
<string name="nc_delete_conversation_more">If you delete the conversation, it will also be deleted for all other participants.</string> <string name="nc_delete_conversation_more">If you delete the conversation, it will also be deleted for all other participants.</string>
<string name="nc_new_conversation">New conversation</string> <string name="nc_new_conversation">New conversation</string>
@ -403,6 +409,7 @@ How to translate with transifex:
<string name="nc_date_header_today">Today</string> <string name="nc_date_header_today">Today</string>
<string name="nc_conversation_menu_voice_call">Voice call</string> <string name="nc_conversation_menu_voice_call">Voice call</string>
<string name="nc_conversation_menu_video_call">Video call</string> <string name="nc_conversation_menu_video_call">Video call</string>
<string name="nc_event_conversation_menu">Event conversation menu</string>
<string name="nc_conversation_menu_conversation_info">Conversation info</string> <string name="nc_conversation_menu_conversation_info">Conversation info</string>
<string name="nc_new_messages">Unread messages</string> <string name="nc_new_messages">Unread messages</string>
<string name="nc_sent_a_gif" formatted="true">%1$s sent a GIF.</string> <string name="nc_sent_a_gif" formatted="true">%1$s sent a GIF.</string>
@ -843,4 +850,5 @@ How to translate with transifex:
<string name="no_conversations_archived">No archived conversations</string> <string name="no_conversations_archived">No archived conversations</string>
<string name="archived_conversation">Archived %1$s</string> <string name="archived_conversation">Archived %1$s</string>
<string name="unarchived_conversation">Unarchived %1$s</string> <string name="unarchived_conversation">Unarchived %1$s</string>
<string name="conversation_archived">Conversation is archived</string>
</resources> </resources>