diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/24.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/24.json new file mode 100644 index 00000000000..d90999537b3 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/24.json @@ -0,0 +1,806 @@ +{ + "formatVersion": 1, + "database": { + "version": 24, + "identityHash": "95e521276166d9d1499eabf29b19146c", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT" + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT" + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT" + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT" + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT" + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + } + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, `hiddenPinnedId` INTEGER, `lastPinnedId` INTEGER, `messageDraft` TEXT, `hiddenUpcomingEvent` TEXT, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT" + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT" + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasArchived", + "columnName": "hasArchived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasSensitive", + "columnName": "hasSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasImportant", + "columnName": "hasImportant", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hiddenPinnedId", + "columnName": "hiddenPinnedId", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastPinnedId", + "columnName": "lastPinnedId", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageDraft", + "columnName": "messageDraft", + "affinity": "TEXT" + }, + { + "fieldPath": "hiddenUpcomingEvent", + "columnName": "hiddenUpcomingEvent", + "affinity": "TEXT" + } + ], + "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, `threadId` INTEGER, `isThread` INTEGER 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, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `threadTitle` TEXT, `threadReplies` INTEGER, `timestamp` INTEGER NOT NULL, `pinnedActorType` TEXT, `pinnedActorId` TEXT, `pinnedActorDisplayName` TEXT, `pinnedAt` INTEGER, `pinnedUntil` INTEGER, `sendAt` INTEGER, 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": "threadId", + "columnName": "threadId", + "affinity": "INTEGER" + }, + { + "fieldPath": "isThread", + "columnName": "isThread", + "affinity": "INTEGER", + "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": "sendStatus", + "columnName": "sendStatus", + "affinity": "TEXT" + }, + { + "fieldPath": "silent", + "columnName": "silent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "threadTitle", + "columnName": "threadTitle", + "affinity": "TEXT" + }, + { + "fieldPath": "threadReplies", + "columnName": "threadReplies", + "affinity": "INTEGER" + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinnedActorType", + "columnName": "pinnedActorType", + "affinity": "TEXT" + }, + { + "fieldPath": "pinnedActorId", + "columnName": "pinnedActorId", + "affinity": "TEXT" + }, + { + "fieldPath": "pinnedActorDisplayName", + "columnName": "pinnedActorDisplayName", + "affinity": "TEXT" + }, + { + "fieldPath": "pinnedAt", + "columnName": "pinnedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "pinnedUntil", + "columnName": "pinnedUntil", + "affinity": "INTEGER" + }, + { + "fieldPath": "sendAt", + "columnName": "sendAt", + "affinity": "INTEGER" + } + ], + "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, `threadId` INTEGER, `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": "threadId", + "columnName": "threadId", + "affinity": "INTEGER" + }, + { + "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, '95e521276166d9d1499eabf29b19146c')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt index c01d7e3a025..94cfdf4b728 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -24,6 +24,7 @@ import com.nextcloud.talk.models.json.status.StatusOverall import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall import com.nextcloud.talk.models.json.threads.ThreadOverall import com.nextcloud.talk.models.json.threads.ThreadsOverall +import com.nextcloud.talk.models.json.upcomingEvents.UpcomingEventsOverall import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall import okhttp3.MultipartBody import okhttp3.RequestBody @@ -270,6 +271,12 @@ interface NcApiCoroutines { @Url url: String ): UserAbsenceOverall + @GET + suspend fun getUpcomingEvents( + @Header("Authorization") authorization: String, + @Url url: String + ): UpcomingEventsOverall + @POST suspend fun testPushNotifications( @Header("Authorization") authorization: String, 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 ef978b9b843..10c6e5c9194 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -250,7 +250,6 @@ 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 @@ -790,6 +789,15 @@ class ChatActivity : } } + conversationUser?.let { user -> + val credentials = ApiUtils.getCredentials(user.username, user.token) + chatViewModel.fetchUpcomingEvent( + credentials!!, + user.baseUrl!!, + roomToken + ) + } + if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT && hasSpreedFeatureCapability( conversationUser?.capabilities!!.spreedCapability!!, @@ -1384,6 +1392,47 @@ class ChatActivity : } } + chatViewModel.upcomingEventViewState.observe(this) { uiState -> + when (uiState) { + is ChatViewModel.UpcomingEventUIState.Success -> { + val hiddenEventKey = "${uiState.event.uri}${uiState.event.start}${uiState.event.summary}" + if (hiddenEventKey == chatViewModel.hiddenUpcomingEvent) { + binding.upcomingEventCard.visibility = View.GONE + } else { + binding.upcomingEventCard.visibility = View.VISIBLE + viewThemeUtils.material.themeCardView(binding.upcomingEventCard) + + binding.upcomingEventContainer.upcomingEventSummary.text = uiState.event.summary + + uiState.event.start?.let { start -> + val startDateTime = Instant.ofEpochSecond(start).atZone(ZoneId.systemDefault()) + val currentTime = ZonedDateTime.now(ZoneId.systemDefault()) + binding.upcomingEventContainer.upcomingEventTime.text = + DateUtils(context).getStringForMeetingStartDateTime(startDateTime, currentTime) + } + + binding.upcomingEventContainer.upcomingEventDismiss.setOnClickListener { + binding.upcomingEventCard.visibility = View.GONE + chatViewModel.saveHiddenUpcomingEvent(hiddenEventKey) + Snackbar.make( + binding.root, + R.string.nc_upcoming_event_dismissed, + Snackbar.LENGTH_LONG + ).show() + } + } + } + + is ChatViewModel.UpcomingEventUIState.Error -> { + Log.e(TAG, "Error fetching upcoming events", uiState.exception) + } + + ChatViewModel.UpcomingEventUIState.None -> { + binding.upcomingEventCard.visibility = View.GONE + } + } + } + this.lifecycleScope.launch { chatViewModel.threadRetrieveState.collect { uiState -> when (uiState) { @@ -2778,6 +2827,12 @@ class ChatActivity : bundle.putString(KEY_ROOM_TOKEN, roomToken) bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation()) + val upcomingEvent = + (chatViewModel.upcomingEventViewState.value as? ChatViewModel.UpcomingEventUIState.Success)?.event + if (upcomingEvent != null) { + bundle.putParcelable(BundleKeys.KEY_UPCOMING_EVENT, upcomingEvent) + } + val intent = Intent(this, ConversationInfoActivity::class.java) intent.putExtras(bundle) startActivity(intent) @@ -3815,21 +3870,7 @@ class ChatActivity : 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")) - } + DateUtils(context).getStringForMeetingStartDateTime(startDateTime, currentTime) } currentTime.isAfter(endDateTime) -> context.resources.getString(R.string.nc_meeting_ended) diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt index dfd437e24db..ba7c47172ac 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt @@ -16,6 +16,7 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.opengraph.Reference import com.nextcloud.talk.models.json.reminder.Reminder +import com.nextcloud.talk.models.json.upcomingEvents.UpcomingEventsOverall import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall import io.reactivex.Observable import retrofit2.Response @@ -70,6 +71,7 @@ interface ChatNetworkDataSource { fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable suspend fun editChatMessage(credentials: String, url: String, text: String): ChatOverallSingleMessage suspend fun getOutOfOfficeStatusForUser(credentials: String, baseUrl: String, userId: String): UserAbsenceOverall + suspend fun getUpcomingEvents(credentials: String, baseUrl: String, roomToken: String): UpcomingEventsOverall suspend fun getContextForChatMessage( credentials: String, baseUrl: String, diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt index ef42ec91e47..008601d3c41 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt @@ -17,6 +17,7 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.opengraph.Reference import com.nextcloud.talk.models.json.reminder.Reminder +import com.nextcloud.talk.models.json.upcomingEvents.UpcomingEventsOverall import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.message.SendMessageUtils @@ -194,6 +195,16 @@ class RetrofitChatNetwork(private val ncApi: NcApi, private val ncApiCoroutines: ApiUtils.getUrlForOutOfOffice(baseUrl, userId) ) + override suspend fun getUpcomingEvents( + credentials: String, + baseUrl: String, + roomToken: String + ): UpcomingEventsOverall = + ncApiCoroutines.getUpcomingEvents( + credentials, + ApiUtils.getUrlForUpcomingEvents(baseUrl, roomToken) + ) + override suspend fun getContextForChatMessage( credentials: String, baseUrl: String, diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index 41c0dd1b570..2f8d5fbb33b 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -42,6 +42,7 @@ import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.opengraph.Reference import com.nextcloud.talk.models.json.reminder.Reminder import com.nextcloud.talk.models.json.threads.ThreadInfo +import com.nextcloud.talk.models.json.upcomingEvents.UpcomingEvent import com.nextcloud.talk.models.json.userAbsence.UserAbsenceData import com.nextcloud.talk.repositories.reactions.ReactionsRepository import com.nextcloud.talk.threadsoverview.data.ThreadsRepository @@ -101,6 +102,7 @@ class ChatViewModel @Inject constructor( val mediaPlayerPosition = mediaPlayerManager.mediaPlayerPosition var chatRoomToken: String = "" var messageDraft: MessageDraft = MessageDraft() + var hiddenUpcomingEvent: String? = null lateinit var participantPermissions: ParticipantPermissions fun getChatRepository(): ChatMessageRepository = chatRepository @@ -164,6 +166,10 @@ class ChatViewModel @Inject constructor( val outOfOfficeViewState: LiveData get() = _outOfOfficeViewState + private val _upcomingEventViewState = MutableLiveData(UpcomingEventUIState.None) + val upcomingEventViewState: LiveData + get() = _upcomingEventViewState + private val _unbindRoomResult = MutableLiveData(UnbindRoomUiState.None) val unbindRoomResult: LiveData get() = _unbindRoomResult @@ -990,6 +996,24 @@ class ChatViewModel @Inject constructor( } } + @Suppress("Detekt.TooGenericExceptionCaught") + fun fetchUpcomingEvent(credentials: String, baseUrl: String, roomToken: String) { + viewModelScope.launch { + updateHiddenUpcomingEvent() + try { + val response = chatNetworkDataSource.getUpcomingEvents(credentials, baseUrl, roomToken) + val firstEvent = response.ocs?.data?.events?.firstOrNull() + if (firstEvent != null) { + _upcomingEventViewState.value = UpcomingEventUIState.Success(firstEvent) + } else { + _upcomingEventViewState.value = UpcomingEventUIState.None + } + } catch (exception: Exception) { + _upcomingEventViewState.value = UpcomingEventUIState.Error(exception) + } + } + } + fun deleteTempMessage(chatMessage: ChatMessage) { viewModelScope.launch { chatRepository.deleteTempMessage(chatMessage) @@ -1057,6 +1081,30 @@ class ChatViewModel @Inject constructor( } } + suspend fun updateHiddenUpcomingEvent() { + val model = conversationRepository.getLocallyStoredConversation( + currentUser, + chatRoomToken + ) + model?.hiddenUpcomingEvent?.let { + hiddenUpcomingEvent = it + } + } + + fun saveHiddenUpcomingEvent(value: String) { + hiddenUpcomingEvent = value + viewModelScope.launch { + val model = conversationRepository.getLocallyStoredConversation( + currentUser, + chatRoomToken + ) + model?.let { + it.hiddenUpcomingEvent = value + conversationRepository.updateConversation(it) + } + } + } + fun pinMessage(credentials: String, url: String, pinUntil: Int = 0) { viewModelScope.launch { chatRepository.pinMessage(credentials, url, pinUntil).collect { @@ -1118,4 +1166,10 @@ class ChatViewModel @Inject constructor( data class Success(val thread: ThreadInfo?) : ThreadRetrieveUiState() data class Error(val exception: Exception) : ThreadRetrieveUiState() } + + sealed class UpcomingEventUIState { + data object None : UpcomingEventUIState() + data class Success(val event: UpcomingEvent) : UpcomingEventUIState() + data class Error(val exception: Exception) : UpcomingEventUIState() + } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt index e61d9a5b951..98818cd93e2 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt @@ -61,6 +61,7 @@ import com.nextcloud.talk.databinding.ActivityConversationInfoBinding import com.nextcloud.talk.databinding.DialogBanParticipantBinding import com.nextcloud.talk.events.EventStatus import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider +import com.nextcloud.talk.extensions.getParcelableExtraProvider import com.nextcloud.talk.extensions.loadConversationAvatar import com.nextcloud.talk.extensions.loadNoteToSelfAvatar import com.nextcloud.talk.extensions.loadSystemAvatar @@ -73,6 +74,7 @@ import com.nextcloud.talk.models.domain.converters.DomainEnumNotificationLevelCo import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.models.json.capabilities.SpreedCapability import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.upcomingEvents.UpcomingEvent import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.participants.Participant @@ -105,6 +107,7 @@ import kotlinx.coroutines.launch import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import java.time.Instant +import java.time.ZoneId import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.format.DateTimeFormatter @@ -200,6 +203,19 @@ class ConversationInfoActivity : hasAvatarSpacing = intent.getBooleanExtra(BundleKeys.KEY_ROOM_ONE_TO_ONE, false) + val upcomingEvent = intent.getParcelableExtraProvider(BundleKeys.KEY_UPCOMING_EVENT) + if (upcomingEvent != null && (upcomingEvent.summary != null || upcomingEvent.start != null)) { + binding.upcomingEventCard.visibility = VISIBLE + viewThemeUtils.material.themeCardView(binding.upcomingEventCard) + binding.upcomingEventContainer.upcomingEventSummary.text = upcomingEvent.summary + upcomingEvent.start?.let { start -> + val startDateTime = Instant.ofEpochSecond(start).atZone(ZoneId.systemDefault()) + val currentTime = ZonedDateTime.now(ZoneId.systemDefault()) + binding.upcomingEventContainer.upcomingEventTime.text = + DateUtils(this).getStringForMeetingStartDateTime(startDateTime, currentTime) + } + } + viewModel = ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java] lifecycleScope.launch { 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 84b9e2c0045..a9e3a2fac16 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 @@ -93,6 +93,8 @@ class OfflineFirstConversationsRepository @Inject constructor( override fun onNext(model: ConversationModel) { runBlocking { + val existingEntity = dao.getConversationForUser(user.id!!, model.token).first() + model.hiddenUpcomingEvent = existingEntity?.hiddenUpcomingEvent _conversationFlow.emit(model) val entityList = listOf(model.asEntity()) dao.upsertConversations(user.id!!, entityList) diff --git a/app/src/main/java/com/nextcloud/talk/data/database/dao/ConversationsDao.kt b/app/src/main/java/com/nextcloud/talk/data/database/dao/ConversationsDao.kt index 621207cccb9..0e1604eed26 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/dao/ConversationsDao.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/dao/ConversationsDao.kt @@ -32,6 +32,7 @@ interface ConversationsDao { if (existingItem != null) { val mergedItem = serverItem.copy() mergedItem.messageDraft = existingItem.messageDraft + mergedItem.hiddenUpcomingEvent = existingItem.hiddenUpcomingEvent updateConversation(mergedItem) } else { insertConversation(serverItem) 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 7dad703069c..78c8c1254b1 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 @@ -66,7 +66,8 @@ fun ConversationModel.asEntity() = hasImportant = hasImportant, messageDraft = messageDraft, hiddenPinnedId = hiddenPinnedId, - lastPinnedId = lastPinnedId + lastPinnedId = lastPinnedId, + hiddenUpcomingEvent = hiddenUpcomingEvent ) fun ConversationEntity.asModel() = @@ -123,7 +124,8 @@ fun ConversationEntity.asModel() = hasImportant = hasImportant, messageDraft = messageDraft, hiddenPinnedId = hiddenPinnedId, - lastPinnedId = lastPinnedId + lastPinnedId = lastPinnedId, + hiddenUpcomingEvent = hiddenUpcomingEvent ) fun Conversation.asEntity(accountId: Long) = 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 bdc56663813..f341a667cdb 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 @@ -100,7 +100,8 @@ data class ConversationEntity( @ColumnInfo(name = "hasImportant") var hasImportant: Boolean = false, @ColumnInfo(name = "hiddenPinnedId") var hiddenPinnedId: Long? = null, @ColumnInfo(name = "lastPinnedId") var lastPinnedId: Long? = null, - @ColumnInfo(name = "messageDraft") var messageDraft: MessageDraft? = MessageDraft() + @ColumnInfo(name = "messageDraft") var messageDraft: MessageDraft? = MessageDraft(), + @ColumnInfo(name = "hiddenUpcomingEvent") var hiddenUpcomingEvent: String? = null // missing/not needed: attendeeId // missing/not needed: attendeePin // missing/not needed: attendeePermissions 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 8e8822aec5e..86cc518567d 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 @@ -48,14 +48,15 @@ import java.util.Locale ChatMessageEntity::class, ChatBlockEntity::class ], - version = 23, + version = 24, autoMigrations = [ AutoMigration(from = 9, to = 10), AutoMigration(from = 16, to = 17, spec = AutoMigration16To17::class), AutoMigration(from = 19, to = 20), AutoMigration(from = 20, to = 21), AutoMigration(from = 21, to = 22), - AutoMigration(from = 22, to = 23) + AutoMigration(from = 22, to = 23), + AutoMigration(from = 23, to = 24) ], exportSchema = true ) 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 3361030abfd..b0297f6607b 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 @@ -69,7 +69,8 @@ data class ConversationModel( // attributes that don't come from API. This should be changed?! var password: String? = null, - var messageDraft: MessageDraft? = MessageDraft() + var messageDraft: MessageDraft? = MessageDraft(), + var hiddenUpcomingEvent: String? = null ) { companion object { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/upcomingEvents/UpcomingEvent.kt b/app/src/main/java/com/nextcloud/talk/models/json/upcomingEvents/UpcomingEvent.kt new file mode 100644 index 00000000000..e22f85ea6f3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/upcomingEvents/UpcomingEvent.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.upcomingEvents + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UpcomingEvent( + @JsonField(name = ["uri"]) + var uri: String, + @JsonField(name = ["recurrenceId"]) + var recurrenceId: Long?, + @JsonField(name = ["calendarUri"]) + var calendarUri: String, + @JsonField(name = ["start"]) + var start: Long?, + @JsonField(name = ["summary"]) + var summary: String?, + @JsonField(name = ["location"]) + var location: String?, + @JsonField(name = ["calendarAppUrl"]) + var calendarAppUrl: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this("", null, "", null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/upcomingEvents/UpcomingEventsData.kt b/app/src/main/java/com/nextcloud/talk/models/json/upcomingEvents/UpcomingEventsData.kt new file mode 100644 index 00000000000..56c919c62f9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/upcomingEvents/UpcomingEventsData.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.upcomingEvents + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UpcomingEventsData( + @JsonField(name = ["events"]) + var events: List? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/upcomingEvents/UpcomingEventsOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/upcomingEvents/UpcomingEventsOCS.kt new file mode 100644 index 00000000000..60482fd89e1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/upcomingEvents/UpcomingEventsOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.upcomingEvents + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UpcomingEventsOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: UpcomingEventsData? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/upcomingEvents/UpcomingEventsOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/upcomingEvents/UpcomingEventsOverall.kt new file mode 100644 index 00000000000..30697cdb932 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/upcomingEvents/UpcomingEventsOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.upcomingEvents + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UpcomingEventsOverall( + @JsonField(name = ["ocs"]) + var ocs: UpcomingEventsOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt index 5a06092c374..a48ccbfb903 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt @@ -19,6 +19,7 @@ import com.nextcloud.talk.models.RetrofitBucket import com.nextcloud.talk.models.json.capabilities.SpreedCapability import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability import okhttp3.Credentials.basic +import java.net.URLEncoder import java.nio.charset.StandardCharsets @Suppress("TooManyFunctions") @@ -533,6 +534,10 @@ object ApiUtils { fun getUrlForOutOfOffice(baseUrl: String, userId: String): String = "$baseUrl$OCS_API_VERSION/apps/dav/api/v1/outOfOffice/$userId/now" + fun getUrlForUpcomingEvents(baseUrl: String, roomToken: String): String = + "$baseUrl$OCS_API_VERSION/apps/dav/api/v1/events/upcoming" + + "?location=${URLEncoder.encode("$baseUrl/call/$roomToken", "UTF-8")}" + fun getUrlForChatMessageContext(baseUrl: String, token: String, messageId: String): String = "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/chat/$token/$messageId/context" diff --git a/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt index ea5ba90fb4e..69baa86d12c 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt @@ -13,6 +13,9 @@ import android.icu.text.RelativeDateTimeFormatter.Direction import android.icu.text.RelativeDateTimeFormatter.RelativeUnit import com.nextcloud.talk.R import java.text.DateFormat +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle import java.util.Calendar import java.util.Date import kotlin.math.abs @@ -62,6 +65,30 @@ class DateUtils(val context: Context) { return abs(difference) } + fun getStringForMeetingStartDateTime(startDateTime: ZonedDateTime, currentTime: ZonedDateTime): String { + val isToday = startDateTime.toLocalDate().isEqual(currentTime.toLocalDate()) + val isTomorrow = startDateTime.toLocalDate().isEqual(currentTime.toLocalDate().plusDays(1)) + val timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) + return when { + isToday -> String.format( + context.resources.getString(R.string.nc_today_meeting), + startDateTime.format(timeFormatter) + ) + + isTomorrow -> String.format( + context.resources.getString(R.string.nc_tomorrow_meeting), + startDateTime.format(timeFormatter) + ) + + else -> startDateTime.format( + DateTimeFormatter.ofLocalizedDateTime( + FormatStyle.MEDIUM, + FormatStyle.SHORT + ) + ) + } + } + fun relativeStartTimeForLobby(timestampMilliseconds: Long, resources: Resources): String { val fmt = RelativeDateTimeFormatter.getInstance() val timeLeftMillis = timestampMilliseconds - System.currentTimeMillis() diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index d31b7c6e388..8b12a483f03 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -85,4 +85,5 @@ object BundleKeys { const val KEY_THREAD_ID = "KEY_THREAD_ID" const val KEY_FROM_QR: String = "KEY_FROM_QR" const val KEY_OPENED_VIA_NOTIFICATION: String = "KEY_OPENED_VIA_NOTIFICATION" + const val KEY_UPCOMING_EVENT: String = "KEY_UPCOMING_EVENT" } diff --git a/app/src/main/res/drawable/ic_event_24px.xml b/app/src/main/res/drawable/ic_event_24px.xml new file mode 100644 index 00000000000..48232f2835c --- /dev/null +++ b/app/src/main/res/drawable/ic_event_24px.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 52e223c505d..94013003eaa 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -182,6 +182,20 @@ android:layout_height="wrap_content" android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/upcoming_event_view_multi_line.xml b/app/src/main/res/layout/upcoming_event_view_multi_line.xml new file mode 100644 index 00000000000..424fc8c8ba6 --- /dev/null +++ b/app/src/main/res/layout/upcoming_event_view_multi_line.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 0a6e5e603a6..2dee2f43490 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -42,6 +42,7 @@ 16dp 24dp 12dp + 4dp 16dp 16sp 16sp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc9685787aa..2bb33ba3a35 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -263,6 +263,7 @@ How to translate with transifex: Invalid time Today at %1$s Tomorrow at %1$s + Upcoming call can be viewed in conversation info If you delete the conversation, it will also be deleted for all other participants. New conversation diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 406c7eab203..8d6afceb2f1 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -18773,6 +18773,11 @@ + + + + + @@ -18865,6 +18870,11 @@ + + + + + @@ -18929,6 +18939,14 @@ + + + + + + + + @@ -19065,6 +19083,14 @@ + + + + + + + + @@ -19241,6 +19267,14 @@ + + + + + + + + @@ -19345,6 +19379,14 @@ + + + + + + + +