diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/14.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/14.json new file mode 100644 index 000000000..cc73f1e94 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/14.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt index 6b2b06ecf..8e077f863 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt @@ -215,6 +215,7 @@ class ChatBlocksDaoTest { lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY, lobbyTimer = 0, objectType = ConversationEnums.ObjectType.FILE, + objectId = "", statusIcon = null, description = "", displayName = "", diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt index 7b3f6876d..a6f8386e7 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt @@ -211,6 +211,7 @@ class ChatMessagesDaoTest { lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY, lobbyTimer = 0, objectType = ConversationEnums.ObjectType.FILE, + objectId = "", statusIcon = null, description = "", displayName = "", diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 5c10b4ec4..20382f661 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -38,12 +38,14 @@ import android.view.Gravity import android.view.Menu import android.view.MenuItem import android.view.View +import android.view.ViewGroup import android.view.animation.AccelerateDecelerateInterpolator import android.widget.AbsListView import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.PopupMenu +import android.widget.PopupWindow import android.widget.TextView import android.widget.Toast import androidx.activity.OnBackPressedCallback @@ -51,6 +53,7 @@ import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog import androidx.appcompat.view.ContextThemeWrapper import androidx.cardview.widget.CardView import androidx.compose.runtime.mutableStateOf @@ -82,6 +85,7 @@ import coil.request.CachePolicy import coil.request.ImageRequest import coil.target.Target import coil.transform.CircleCropTransformation +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.ui.color.ColorUtil 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.WebSocketCommunicationEvent import com.nextcloud.talk.extensions.loadAvatarOrImagePreview +import com.nextcloud.talk.jobs.DeleteConversationWorker import com.nextcloud.talk.jobs.DownloadFileToCacheWorker import com.nextcloud.talk.jobs.ShareOperationWorker import com.nextcloud.talk.jobs.UploadAndShareFilesWorker @@ -210,13 +215,18 @@ import java.io.File import java.io.IOException import java.net.HttpURLConnection 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.Locale import java.util.concurrent.ExecutionException import javax.inject.Inject -import kotlin.collections.set import kotlin.math.roundToInt 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) class ChatActivity : @@ -254,6 +264,8 @@ class ChatActivity : lateinit var networkMonitor: NetworkMonitor lateinit var chatViewModel: ChatViewModel + + lateinit var conversationInfoViewModel: ConversationInfoViewModel lateinit var messageInputViewModel: MessageInputViewModel private val startSelectContactForResult = registerForActivityResult( @@ -324,6 +336,7 @@ class ChatActivity : private var conversationVoiceCallMenuItem: MenuItem? = null private var conversationVideoMenuItem: MenuItem? = null + private var eventConversationMenuItem: MenuItem? = null var webSocketInstance: WebSocketInstance? = null var signalingMessageSender: SignalingMessageSender? = null @@ -418,6 +431,8 @@ class ChatActivity : chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java] + conversationInfoViewModel = ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java] + val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken) val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token) chatViewModel.initData( @@ -567,6 +582,7 @@ class ChatActivity : participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!) invalidateOptionsMenu() + isEventConversation() checkShowCallButtons() checkLobbyState() updateRoomTimerHandler() @@ -600,6 +616,7 @@ class ChatActivity : loadAvatarForStatusBar() setupSwipeToReply() setActionBarTitle() + isEventConversation() checkShowCallButtons() checkLobbyState() 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 = currentConversation?.conversationReadOnlyState != null && currentConversation?.conversationReadOnlyState == @@ -2849,12 +2877,19 @@ class ChatActivity : super.onCreateOptionsMenu(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 == "?") { menu.removeItem(R.id.conversation_info) } else { loadAvatarForStatusBar() setActionBarTitle() } + return true } @@ -2871,12 +2906,6 @@ class ChatActivity : searchItem.isVisible = CapabilitiesUtil.isUnifiedSearchAvailable(spreedCapabilities) && currentConversation!!.remoteServer.isNullOrEmpty() - if (currentConversation!!.remoteServer != null || - !CapabilitiesUtil.isSharedItemsAvailable(spreedCapabilities) - ) { - menu.removeItem(R.id.shared_items) - } - if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) { conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call) conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call) @@ -2909,7 +2938,6 @@ class ChatActivity : menu.removeItem(R.id.conversation_voice_call) } } - return true } @@ -2940,9 +2968,205 @@ class ChatActivity : true } + R.id.conversation_event -> { + val anchorView = findViewById(R.id.conversation_event) + showPopupWindow(anchorView) + true + } + else -> super.onOptionsItemSelected(item) } + private fun showPopupWindow(anchorView: View) { + val popupView = layoutInflater.inflate(R.layout.item_event_schedule, null) + + val titleTextView = popupView.findViewById(R.id.event_scheduled) + val subtitleTextView = popupView.findViewById(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(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(R.id.archive_conversation) + val unarchiveConversation = popupView.findViewById(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() { val intent = Intent(this, SharedItemsActivity::class.java) 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_MARK_PLAYED_FACTOR = 20 const val OUT_OF_OFFICE_ALPHA = 76 + const val ZERO_INDEX = 0 + const val ONE_INDEX = 1 } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt index ae333d955..47e007a06 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt @@ -185,6 +185,11 @@ class ConversationInfoEditActivity : BaseActivity() { binding.conversationDescription.isEnabled = false } + if (conversation?.objectType == ConversationEnums.ObjectType.EVENT) { + binding.conversationName.isEnabled = false + binding.conversationDescription.isEnabled = false + } + loadConversationAvatar() } @@ -271,7 +276,9 @@ class ConversationInfoEditActivity : BaseActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.save) { - saveConversationNameAndDescription() + if (conversation?.objectType != ConversationEnums.ObjectType.EVENT) { + saveConversationNameAndDescription() + } } return true } diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 3e5f87ce5..1579eafc6 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -210,6 +210,7 @@ class ConversationsListActivity : private var conversationItemsWithHeader: MutableList> = ArrayList() private val searchableConversationItems: MutableList> = ArrayList() private var filterableConversationItems: MutableList> = ArrayList() + private var nearFutureEventConversationItems: MutableList> = ArrayList() private var searchItem: MenuItem? = null private var chooseAccountItem: MenuItem? = null private var searchView: SearchView? = null @@ -519,16 +520,29 @@ class ConversationsListActivity : // Update Conversations conversationItems.clear() conversationItemsWithHeader.clear() + nearFutureEventConversationItems.clear() + for (conversation in list) { + if (!futureEvent(conversation)) { + addToNearFutureEventConversationItems(conversation) + } addToConversationItems(conversation) } + sortConversations(conversationItems) sortConversations(conversationItemsWithHeader) + sortConversations(nearFutureEventConversationItems) - // Filter Conversations - if (!hasFilterEnabled()) filterableConversationItems = conversationItems - filterConversation() - adapter?.updateDataSet(filterableConversationItems, false) + if (!hasFilterEnabled() && searchBehaviorSubject.value == false) { + adapter?.updateDataSet(nearFutureEventConversationItems, false) + } else { + // Filter Conversations + if (!hasFilterEnabled()) { + filterableConversationItems = conversationItems + } + filterConversation() + adapter?.updateDataSet(filterableConversationItems, false) + } Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong()) // Fetch Open Conversations @@ -550,6 +564,26 @@ class ConversationsListActivity : 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() { val accountId = UserIdUtils.getIdForUser(currentUser) filterState[FilterConversationFragment.UNREAD] = ( @@ -837,15 +871,19 @@ class ConversationsListActivity : override fun onMenuItemActionCollapse(item: MenuItem): Boolean { adapter?.setHeadersShown(false) + searchBehaviorSubject.onNext(false) if (!hasFilterEnabled()) filterableConversationItems = conversationItemsWithHeader - adapter?.updateDataSet(filterableConversationItems, false) + if (!hasFilterEnabled()) { + adapter?.updateDataSet(nearFutureEventConversationItems, false) + } else { + filterableConversationItems = conversationItems + } adapter?.hideAllHeaders() if (searchHelper != null) { // cancel any pending searches searchHelper!!.cancelSearch() } binding.swipeRefreshLayoutView.isRefreshing = false - searchBehaviorSubject.onNext(false) binding.swipeRefreshLayoutView.isEnabled = true searchView!!.onActionViewCollapsed() @@ -2114,5 +2152,7 @@ class ConversationsListActivity : const val NOTIFICATION_WARNING_DATE_NOT_SET = 0L const val OFFSET_HEIGHT_DIVIDER: Int = 3 const val ROOM_TYPE_ONE_ONE = "1" + private const val AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS: Long = 57600 + const val LONG_1000: Long = 1000 } } diff --git a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ConversationMapUtils.kt b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ConversationMapUtils.kt index 90acf6117..23151b7a4 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ConversationMapUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ConversationMapUtils.kt @@ -34,6 +34,7 @@ fun ConversationModel.asEntity() = unreadMention = unreadMention, lastMessage = lastMessage?.let { LoganSquare.serialize(lastMessage) }, objectType = objectType, + objectId = objectId, notificationLevel = notificationLevel, conversationReadOnlyState = conversationReadOnlyState, lobbyState = lobbyState, @@ -85,6 +86,7 @@ fun ConversationEntity.asModel() = lastMessage = lastMessage?.let { LoganSquare.parse(lastMessage, ChatMessageJson::class.java) }, objectType = objectType, + objectId = objectId, notificationLevel = notificationLevel, conversationReadOnlyState = conversationReadOnlyState, lobbyState = lobbyState, @@ -135,6 +137,7 @@ fun Conversation.asEntity(accountId: Long) = unreadMention = unreadMention, lastMessage = lastMessage?.let { LoganSquare.serialize(lastMessage) }, objectType = objectType, + objectId = objectId, notificationLevel = notificationLevel, conversationReadOnlyState = conversationReadOnlyState, lobbyState = lobbyState, diff --git a/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt b/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt index 4f4652264..106c7e7a8 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt @@ -78,6 +78,7 @@ data class ConversationEntity( @ColumnInfo(name = "notificationCalls") var notificationCalls: Int = 0, @ColumnInfo(name = "notificationLevel") var notificationLevel: ConversationEnums.NotificationLevel, @ColumnInfo(name = "objectType") var objectType: ConversationEnums.ObjectType, + @ColumnInfo(name = "objectId") var objectId: String, @ColumnInfo(name = "participantType") var participantType: Participant.ParticipantType, @ColumnInfo(name = "permissions") var permissions: Int = 0, @ColumnInfo(name = "readOnly") var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState, diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt index 85c3239be..af8089edf 100644 --- a/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt @@ -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) { db.execSQL( "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) { try { db.execSQL( diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt index 94f5a3709..d86a9f4c0 100644 --- a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt @@ -49,9 +49,9 @@ import java.util.Locale ChatMessageEntity::class, ChatBlockEntity::class ], - version = 13, + version = 14, autoMigrations = [ - AutoMigration(from = 9, to = 11) + AutoMigration(from = 9, to = 10) ], exportSchema = true ) @@ -115,7 +115,8 @@ abstract class TalkDatabase : RoomDatabase() { Migrations.MIGRATION_8_9, Migrations.MIGRATION_10_11, Migrations.MIGRATION_11_12, - Migrations.MIGRATION_12_13 + Migrations.MIGRATION_12_13, + Migrations.MIGRATION_13_14 ) .allowMainThreadQueries() .addCallback( diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt index e6a036564..5e1f845d3 100644 --- a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt +++ b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt @@ -33,6 +33,7 @@ class ConversationModel( var unreadMention: Boolean = false, var lastMessage: ChatMessageJson? = null, var objectType: ConversationEnums.ObjectType, + var objectId: String = "", var notificationLevel: ConversationEnums.NotificationLevel, var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState, var lobbyState: ConversationEnums.LobbyState, @@ -66,6 +67,7 @@ class ConversationModel( ) { companion object { + @Suppress("LongMethod") fun mapToConversationModel(conversation: Conversation, user: User): ConversationModel { return ConversationModel( internalId = user.id!!.toString() + "@" + conversation.token, @@ -88,6 +90,7 @@ class ConversationModel( unreadMention = conversation.unreadMention, lastMessage = conversation.lastMessage, objectType = conversation.objectType.let { ConversationEnums.ObjectType.valueOf(it.name) }, + objectId = conversation.objectId, notificationLevel = conversation.notificationLevel.let { ConversationEnums.NotificationLevel.valueOf( it.name diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt index 6ef2bd50e..f16db4a78 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt @@ -79,6 +79,9 @@ data class Conversation( @JsonField(name = ["objectType"], typeConverter = ConversationObjectTypeConverter::class) var objectType: ConversationEnums.ObjectType = ConversationEnums.ObjectType.DEFAULT, + @JsonField(name = ["objectId"]) + var objectId: String = "", + @JsonField(name = ["notificationLevel"], typeConverter = EnumNotificationLevelConverter::class) var notificationLevel: ConversationEnums.NotificationLevel = ConversationEnums.NotificationLevel.DEFAULT, diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt index 15be0a664..44dd302b5 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt @@ -43,6 +43,7 @@ class ConversationEnums { DEFAULT, SHARE_PASSWORD, FILE, - ROOM + ROOM, + EVENT } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt index 65ffb639a..817f1b089 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt @@ -15,6 +15,7 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter ConversationEnums.ObjectType.SHARE_PASSWORD "room" -> ConversationEnums.ObjectType.ROOM "file" -> ConversationEnums.ObjectType.FILE + "event" -> ConversationEnums.ObjectType.EVENT else -> ConversationEnums.ObjectType.DEFAULT } } @@ -28,6 +29,7 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter "share:password" ConversationEnums.ObjectType.ROOM -> "room" ConversationEnums.ObjectType.FILE -> "file" + ConversationEnums.ObjectType.EVENT -> "event" else -> "" } } diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt index d78f3d2e8..c95a7464a 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt @@ -7,6 +7,7 @@ package com.nextcloud.talk.ui.dialog import android.app.Dialog +import android.content.DialogInterface import android.os.Build import android.os.Bundle import android.view.LayoutInflater @@ -99,6 +100,14 @@ class FilterConversationFragment : DialogFragment() { } binding.buttonClose.setOnClickListener { + val noFiltersActive = !( + filterState[MENTION] == true || + filterState[UNREAD] == true || + filterState[ARCHIVE] == true + ) + if (noFiltersActive) { + (requireActivity() as ConversationsListActivity).showOnlyNearFutureEvents() + } dismiss() } } @@ -130,6 +139,18 @@ class FilterConversationFragment : DialogFragment() { (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 { private const val FILTER_STATE_ARG = "FILTER_STATE_ARG" diff --git a/app/src/main/res/drawable/baseline_calendar_today_24.xml b/app/src/main/res/drawable/baseline_calendar_today_24.xml new file mode 100644 index 000000000..5a569b829 --- /dev/null +++ b/app/src/main/res/drawable/baseline_calendar_today_24.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/layout/item_event_schedule.xml b/app/src/main/res/layout/item_event_schedule.xml new file mode 100644 index 000000000..ebc69f470 --- /dev/null +++ b/app/src/main/res/layout/item_event_schedule.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_conversation.xml b/app/src/main/res/menu/menu_conversation.xml index bb7799dd5..c7830b0dc 100644 --- a/app/src/main/res/menu/menu_conversation.xml +++ b/app/src/main/res/menu/menu_conversation.xml @@ -8,36 +8,45 @@ + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index e433c4160..df8bcb4d6 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -12,6 +12,7 @@ #006AA3 @color/colorPrimary #ff6F6F6F + #FF37474F #1E1E1E diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7c1c45969..829a9ed53 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -13,6 +13,8 @@ #ff888888 #ffffff #B3FFFFFF + #FF607D8B + @android:color/white diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1440edc32..6fbcb4280 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -242,8 +242,14 @@ How to translate with transifex: Rename conversation Rename Delete conversation + Schedule Delete Delete all + Ongoing meeting + Meeting ended + Invalid time + Today at %1$s + Tomorrow at %1$s If you delete the conversation, it will also be deleted for all other participants. New conversation @@ -403,6 +409,7 @@ How to translate with transifex: Today Voice call Video call + Event conversation menu Conversation info Unread messages %1$s sent a GIF. @@ -843,4 +850,5 @@ How to translate with transifex: No archived conversations Archived %1$s Unarchived %1$s + Conversation is archived