diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json index 2e2345001..100acd968 100644 --- a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 13, - "identityHash": "a521f027909f69f4c7d1855f84a2e67f", + "identityHash": "506abc931eb3b657cafe6ad1b25f635d", "entities": [ { "tableName": "User", @@ -138,7 +138,7 @@ }, { "tableName": "Conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `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", @@ -320,6 +320,12 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, { "fieldPath": "participantType", "columnName": "participantType", @@ -743,7 +749,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a521f027909f69f4c7d1855f84a2e67f')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '506abc931eb3b657cafe6ad1b25f635d')" ] } } \ No newline at end of file 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 f9ccb39ec..3ca69bf19 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,11 +215,14 @@ 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 @@ -1892,7 +1900,7 @@ class ChatActivity : } } - private fun isEventConversation() { + private fun isEventConversation() { if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT) { if (eventConversationMenuItem != null) { eventConversationMenuItem?.icon?.alpha = FULLY_OPAQUE_INT @@ -2870,10 +2878,11 @@ class ChatActivity : setActionBarTitle() } if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT) { - eventConversationMenuItem = menu.findItem(R.id.conversation_event_icon) + eventConversationMenuItem = menu.findItem(R.id.conversation_event) } else { - menu.removeItem(R.id.conversation_event_icon) + menu.removeItem(R.id.conversation_event) } + return true } @@ -2922,7 +2931,6 @@ class ChatActivity : menu.removeItem(R.id.conversation_voice_call) } } - return true } @@ -2953,9 +2961,139 @@ 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 deleteConversation = popupView.findViewById(R.id.delete_conversation) + + 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() + + titleTextView.text = "Scheduled" + subtitleTextView.text = meetingStatus + + if (meetingStatus == "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) + ) + } + } else { + deleteConversation.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 objectId = currentConversation?.objectId ?: "" + val status = getMeetingSchedule(objectId) + return status + } + + private fun getMeetingSchedule(objectId: String): String { + val timestamps = objectId.split("#") + if (timestamps.size != 2) return "Invalid Time" + + val startEpoch = timestamps[0].toLong() + val endEpoch = timestamps[1].toLong() + + val startDateTime = Instant.ofEpochSecond(startEpoch).atZone(ZoneId.systemDefault()) + val endDateTime = Instant.ofEpochSecond(endEpoch).atZone(ZoneId.systemDefault()) + val now = ZonedDateTime.now(ZoneId.systemDefault()) + + return when { + now.isBefore(startDateTime) -> { + val isToday = startDateTime.toLocalDate().isEqual(now.toLocalDate()) + val isTomorrow = startDateTime.toLocalDate().isEqual(now.toLocalDate().plusDays(1)) + when { + isToday -> "Today at ${startDateTime.format(DateTimeFormatter.ofPattern("HH:mm"))}" + isTomorrow -> "Tomorrow at ${startDateTime.format(DateTimeFormatter.ofPattern("HH:mm"))}" + else -> startDateTime.format(DateTimeFormatter.ofPattern("MMM d, yyyy, HH:mm")) + } + } + now.isAfter(endDateTime) -> "Meeting Ended" + else -> "Ongoing" + } + } + private fun showSharedItems() { val intent = Intent(this, SharedItemsActivity::class.java) intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName) 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..1436a19f7 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() } @@ -266,6 +271,7 @@ class ConversationInfoEditActivity : BaseActivity() { override fun onPrepareOptionsMenu(menu: Menu): Boolean { super.onPrepareOptionsMenu(menu) + return true } diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt index c4b941962..6be702186 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt @@ -18,6 +18,7 @@ import com.nextcloud.talk.data.database.model.ConversationEntity import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.utils.CapabilitiesUtil.isUserStatusAvailable import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import io.reactivex.Observer @@ -121,7 +122,15 @@ class OfflineFirstConversationsRepository @Inject constructor( .observeOn(AndroidSchedulers.mainThread()) .blockingSingle() - conversationsFromSync = conversationsList.map { + val currentTime = System.currentTimeMillis() / 1000 + + val conversationListWithoutEvents = conversationsList.filterNot { conversation -> + + conversation.objectType == ConversationEnums.ObjectType.EVENT && + conversation.objectId.split("#")[0].toLong() - currentTime > + AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS + } + conversationsFromSync = conversationListWithoutEvents.map { it.asEntity(user.id!!) } @@ -156,5 +165,6 @@ class OfflineFirstConversationsRepository @Inject constructor( companion object { val TAG = OfflineFirstConversationsRepository::class.simpleName + private const val AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS: Long = 86400 } } 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/models/domain/ConversationModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt index ebdca4951..d9c30ee22 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 @@ -89,6 +89,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/res/layout/item_event_schedule.xml b/app/src/main/res/layout/item_event_schedule.xml new file mode 100644 index 000000000..e97b7ba6e --- /dev/null +++ b/app/src/main/res/layout/item_event_schedule.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + \ 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 b930a803b..ffcd29932 100644 --- a/app/src/main/res/menu/menu_conversation.xml +++ b/app/src/main/res/menu/menu_conversation.xml @@ -9,23 +9,12 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> - - - - - - Rename conversation Rename Delete conversation + Schedule Delete Delete all If you delete the conversation, it will also be deleted for all other participants.