Skip to content

Commit e3984d0

Browse files
feat: Show "Next meeting" in the chat
Resolves #5661 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
1 parent bc348ac commit e3984d0

19 files changed

Lines changed: 414 additions & 15 deletions

app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.nextcloud.talk.models.json.status.StatusOverall
2424
import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall
2525
import com.nextcloud.talk.models.json.threads.ThreadOverall
2626
import com.nextcloud.talk.models.json.threads.ThreadsOverall
27+
import com.nextcloud.talk.models.json.upcomingEvents.UpcomingEventsOverall
2728
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
2829
import okhttp3.MultipartBody
2930
import okhttp3.RequestBody
@@ -270,6 +271,12 @@ interface NcApiCoroutines {
270271
@Url url: String
271272
): UserAbsenceOverall
272273

274+
@GET
275+
suspend fun getUpcomingEvents(
276+
@Header("Authorization") authorization: String,
277+
@Url url: String
278+
): UpcomingEventsOverall
279+
273280
@POST
274281
suspend fun testPushNotifications(
275282
@Header("Authorization") authorization: String,

app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ import java.time.Instant
251251
import java.time.ZoneId
252252
import java.time.ZonedDateTime
253253
import java.time.format.DateTimeFormatter
254+
import java.time.format.FormatStyle
254255
import java.util.Date
255256
import java.util.Locale
256257
import java.util.concurrent.ExecutionException
@@ -790,6 +791,15 @@ class ChatActivity :
790791
}
791792
}
792793

794+
conversationUser?.let { user ->
795+
val credentials = ApiUtils.getCredentials(user.username, user.token)
796+
chatViewModel.fetchUpcomingEvent(
797+
credentials!!,
798+
user.baseUrl!!,
799+
roomToken
800+
)
801+
}
802+
793803
if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT &&
794804
hasSpreedFeatureCapability(
795805
conversationUser?.capabilities!!.spreedCapability!!,
@@ -1384,6 +1394,36 @@ class ChatActivity :
13841394
}
13851395
}
13861396

1397+
chatViewModel.upcomingEventViewState.observe(this) { uiState ->
1398+
when (uiState) {
1399+
is ChatViewModel.UpcomingEventUIState.Success -> {
1400+
binding.upcomingEventCard.visibility = View.VISIBLE
1401+
viewThemeUtils.material.themeCardView(binding.upcomingEventCard)
1402+
1403+
binding.upcomingEventContainer.upcomingEventSummary.text = uiState.event.summary
1404+
1405+
uiState.event.start?.let { start ->
1406+
val startDateTime = Instant.ofEpochSecond(start).atZone(ZoneId.systemDefault())
1407+
val currentTime = ZonedDateTime.now(ZoneId.systemDefault())
1408+
binding.upcomingEventContainer.upcomingEventTime.text =
1409+
DateUtils(context).getStringForMeetingStartDateTime(startDateTime, currentTime)
1410+
}
1411+
1412+
binding.upcomingEventContainer.upcomingEventDismiss.setOnClickListener {
1413+
binding.upcomingEventCard.visibility = View.GONE
1414+
}
1415+
}
1416+
1417+
is ChatViewModel.UpcomingEventUIState.Error -> {
1418+
Log.e(TAG, "Error fetching upcoming events", uiState.exception)
1419+
}
1420+
1421+
ChatViewModel.UpcomingEventUIState.None -> {
1422+
binding.upcomingEventCard.visibility = View.GONE
1423+
}
1424+
}
1425+
}
1426+
13871427
this.lifecycleScope.launch {
13881428
chatViewModel.threadRetrieveState.collect { uiState ->
13891429
when (uiState) {
@@ -2778,6 +2818,12 @@ class ChatActivity :
27782818
bundle.putString(KEY_ROOM_TOKEN, roomToken)
27792819
bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation())
27802820

2821+
val upcomingEvent =
2822+
(chatViewModel.upcomingEventViewState.value as? ChatViewModel.UpcomingEventUIState.Success)?.event
2823+
if (upcomingEvent != null) {
2824+
bundle.putParcelable(BundleKeys.KEY_UPCOMING_EVENT, upcomingEvent)
2825+
}
2826+
27812827
val intent = Intent(this, ConversationInfoActivity::class.java)
27822828
intent.putExtras(bundle)
27832829
startActivity(intent)
@@ -3815,21 +3861,7 @@ class ChatActivity :
38153861

38163862
return when {
38173863
currentTime.isBefore(startDateTime) -> {
3818-
val isToday = startDateTime.toLocalDate().isEqual(currentTime.toLocalDate())
3819-
val isTomorrow = startDateTime.toLocalDate().isEqual(currentTime.toLocalDate().plusDays(1))
3820-
when {
3821-
isToday -> String.format(
3822-
context.resources.getString(R.string.nc_today_meeting),
3823-
startDateTime.format(DateTimeFormatter.ofPattern("HH:mm"))
3824-
)
3825-
3826-
isTomorrow -> String.format(
3827-
context.resources.getString(R.string.nc_tomorrow_meeting),
3828-
startDateTime.format(DateTimeFormatter.ofPattern("HH:mm"))
3829-
)
3830-
3831-
else -> startDateTime.format(DateTimeFormatter.ofPattern("MMM d, yyyy, HH:mm"))
3832-
}
3864+
DateUtils(context).getStringForMeetingStartDateTime(startDateTime, currentTime)
38333865
}
38343866

38353867
currentTime.isAfter(endDateTime) -> context.resources.getString(R.string.nc_meeting_ended)

app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall
1616
import com.nextcloud.talk.models.json.generic.GenericOverall
1717
import com.nextcloud.talk.models.json.opengraph.Reference
1818
import com.nextcloud.talk.models.json.reminder.Reminder
19+
import com.nextcloud.talk.models.json.upcomingEvents.UpcomingEventsOverall
1920
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
2021
import io.reactivex.Observable
2122
import retrofit2.Response
@@ -70,6 +71,7 @@ interface ChatNetworkDataSource {
7071
fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable<GenericOverall>
7172
suspend fun editChatMessage(credentials: String, url: String, text: String): ChatOverallSingleMessage
7273
suspend fun getOutOfOfficeStatusForUser(credentials: String, baseUrl: String, userId: String): UserAbsenceOverall
74+
suspend fun getUpcomingEvents(credentials: String, baseUrl: String, roomToken: String): UpcomingEventsOverall
7375
suspend fun getContextForChatMessage(
7476
credentials: String,
7577
baseUrl: String,

app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall
1717
import com.nextcloud.talk.models.json.generic.GenericOverall
1818
import com.nextcloud.talk.models.json.opengraph.Reference
1919
import com.nextcloud.talk.models.json.reminder.Reminder
20+
import com.nextcloud.talk.models.json.upcomingEvents.UpcomingEventsOverall
2021
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
2122
import com.nextcloud.talk.utils.ApiUtils
2223
import com.nextcloud.talk.utils.message.SendMessageUtils
@@ -194,6 +195,16 @@ class RetrofitChatNetwork(private val ncApi: NcApi, private val ncApiCoroutines:
194195
ApiUtils.getUrlForOutOfOffice(baseUrl, userId)
195196
)
196197

198+
override suspend fun getUpcomingEvents(
199+
credentials: String,
200+
baseUrl: String,
201+
roomToken: String
202+
): UpcomingEventsOverall =
203+
ncApiCoroutines.getUpcomingEvents(
204+
credentials,
205+
ApiUtils.getUrlForUpcomingEvents(baseUrl, roomToken)
206+
)
207+
197208
override suspend fun getContextForChatMessage(
198209
credentials: String,
199210
baseUrl: String,

app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import com.nextcloud.talk.models.json.generic.GenericOverall
4242
import com.nextcloud.talk.models.json.opengraph.Reference
4343
import com.nextcloud.talk.models.json.reminder.Reminder
4444
import com.nextcloud.talk.models.json.threads.ThreadInfo
45+
import com.nextcloud.talk.models.json.upcomingEvents.UpcomingEvent
4546
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceData
4647
import com.nextcloud.talk.repositories.reactions.ReactionsRepository
4748
import com.nextcloud.talk.threadsoverview.data.ThreadsRepository
@@ -164,6 +165,10 @@ class ChatViewModel @Inject constructor(
164165
val outOfOfficeViewState: LiveData<OutOfOfficeUIState>
165166
get() = _outOfOfficeViewState
166167

168+
private val _upcomingEventViewState = MutableLiveData<UpcomingEventUIState>(UpcomingEventUIState.None)
169+
val upcomingEventViewState: LiveData<UpcomingEventUIState>
170+
get() = _upcomingEventViewState
171+
167172
private val _unbindRoomResult = MutableLiveData<UnbindRoomUiState>(UnbindRoomUiState.None)
168173
val unbindRoomResult: LiveData<UnbindRoomUiState>
169174
get() = _unbindRoomResult
@@ -990,6 +995,23 @@ class ChatViewModel @Inject constructor(
990995
}
991996
}
992997

998+
@Suppress("Detekt.TooGenericExceptionCaught")
999+
fun fetchUpcomingEvent(credentials: String, baseUrl: String, roomToken: String) {
1000+
viewModelScope.launch {
1001+
try {
1002+
val response = chatNetworkDataSource.getUpcomingEvents(credentials, baseUrl, roomToken)
1003+
val firstEvent = response.ocs?.data?.events?.firstOrNull()
1004+
if (firstEvent != null) {
1005+
_upcomingEventViewState.value = UpcomingEventUIState.Success(firstEvent)
1006+
} else {
1007+
_upcomingEventViewState.value = UpcomingEventUIState.None
1008+
}
1009+
} catch (exception: Exception) {
1010+
_upcomingEventViewState.value = UpcomingEventUIState.Error(exception)
1011+
}
1012+
}
1013+
}
1014+
9931015
fun deleteTempMessage(chatMessage: ChatMessage) {
9941016
viewModelScope.launch {
9951017
chatRepository.deleteTempMessage(chatMessage)
@@ -1118,4 +1140,10 @@ class ChatViewModel @Inject constructor(
11181140
data class Success(val thread: ThreadInfo?) : ThreadRetrieveUiState()
11191141
data class Error(val exception: Exception) : ThreadRetrieveUiState()
11201142
}
1143+
1144+
sealed class UpcomingEventUIState {
1145+
data object None : UpcomingEventUIState()
1146+
data class Success(val event: UpcomingEvent) : UpcomingEventUIState()
1147+
data class Error(val exception: Exception) : UpcomingEventUIState()
1148+
}
11211149
}

app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import com.nextcloud.talk.databinding.ActivityConversationInfoBinding
6161
import com.nextcloud.talk.databinding.DialogBanParticipantBinding
6262
import com.nextcloud.talk.events.EventStatus
6363
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
64+
import com.nextcloud.talk.extensions.getParcelableExtraProvider
6465
import com.nextcloud.talk.extensions.loadConversationAvatar
6566
import com.nextcloud.talk.extensions.loadNoteToSelfAvatar
6667
import com.nextcloud.talk.extensions.loadSystemAvatar
@@ -73,6 +74,7 @@ import com.nextcloud.talk.models.domain.converters.DomainEnumNotificationLevelCo
7374
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
7475
import com.nextcloud.talk.models.json.capabilities.SpreedCapability
7576
import com.nextcloud.talk.models.json.conversations.ConversationEnums
77+
import com.nextcloud.talk.models.json.upcomingEvents.UpcomingEvent
7678
import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter
7779
import com.nextcloud.talk.models.json.generic.GenericOverall
7880
import com.nextcloud.talk.models.json.participants.Participant
@@ -105,6 +107,7 @@ import kotlinx.coroutines.launch
105107
import org.greenrobot.eventbus.Subscribe
106108
import org.greenrobot.eventbus.ThreadMode
107109
import java.time.Instant
110+
import java.time.ZoneId
108111
import java.time.ZoneOffset
109112
import java.time.ZonedDateTime
110113
import java.time.format.DateTimeFormatter
@@ -200,6 +203,19 @@ class ConversationInfoActivity :
200203

201204
hasAvatarSpacing = intent.getBooleanExtra(BundleKeys.KEY_ROOM_ONE_TO_ONE, false)
202205

206+
val upcomingEvent = intent.getParcelableExtraProvider<UpcomingEvent>(BundleKeys.KEY_UPCOMING_EVENT)
207+
if (upcomingEvent != null && (upcomingEvent.summary != null || upcomingEvent.start != null)) {
208+
binding.upcomingEventCard.visibility = VISIBLE
209+
viewThemeUtils.material.themeCardView(binding.upcomingEventCard)
210+
binding.upcomingEventContainer.upcomingEventSummary.text = upcomingEvent.summary
211+
upcomingEvent.start?.let { start ->
212+
val startDateTime = Instant.ofEpochSecond(start).atZone(ZoneId.systemDefault())
213+
val currentTime = ZonedDateTime.now(ZoneId.systemDefault())
214+
binding.upcomingEventContainer.upcomingEventTime.text =
215+
DateUtils(this).getStringForMeetingStartDateTime(startDateTime, currentTime)
216+
}
217+
}
218+
203219
viewModel = ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
204220

205221
lifecycleScope.launch {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.models.json.upcomingEvents
9+
10+
import android.os.Parcelable
11+
import com.bluelinelabs.logansquare.annotation.JsonField
12+
import com.bluelinelabs.logansquare.annotation.JsonObject
13+
import kotlinx.parcelize.Parcelize
14+
15+
@Parcelize
16+
@JsonObject
17+
data class UpcomingEvent(
18+
@JsonField(name = ["uri"])
19+
var uri: String,
20+
@JsonField(name = ["recurrenceId"])
21+
var recurrenceId: Long?,
22+
@JsonField(name = ["calendarUri"])
23+
var calendarUri: String,
24+
@JsonField(name = ["start"])
25+
var start: Long?,
26+
@JsonField(name = ["summary"])
27+
var summary: String?,
28+
@JsonField(name = ["location"])
29+
var location: String?,
30+
@JsonField(name = ["calendarAppUrl"])
31+
var calendarAppUrl: String?
32+
) : Parcelable {
33+
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
34+
constructor() : this("", null, "", null, null, null, null)
35+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.models.json.upcomingEvents
9+
10+
import android.os.Parcelable
11+
import com.bluelinelabs.logansquare.annotation.JsonField
12+
import com.bluelinelabs.logansquare.annotation.JsonObject
13+
import kotlinx.parcelize.Parcelize
14+
15+
@Parcelize
16+
@JsonObject
17+
data class UpcomingEventsData(
18+
@JsonField(name = ["events"])
19+
var events: List<UpcomingEvent>?
20+
) : Parcelable {
21+
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
22+
constructor() : this(null)
23+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.models.json.upcomingEvents
9+
10+
import android.os.Parcelable
11+
import com.bluelinelabs.logansquare.annotation.JsonField
12+
import com.bluelinelabs.logansquare.annotation.JsonObject
13+
import com.nextcloud.talk.models.json.generic.GenericMeta
14+
import kotlinx.parcelize.Parcelize
15+
16+
@Parcelize
17+
@JsonObject
18+
data class UpcomingEventsOCS(
19+
@JsonField(name = ["meta"])
20+
var meta: GenericMeta?,
21+
@JsonField(name = ["data"])
22+
var data: UpcomingEventsData?
23+
) : Parcelable {
24+
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
25+
constructor() : this(null, null)
26+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.models.json.upcomingEvents
9+
10+
import android.os.Parcelable
11+
import com.bluelinelabs.logansquare.annotation.JsonField
12+
import com.bluelinelabs.logansquare.annotation.JsonObject
13+
import kotlinx.parcelize.Parcelize
14+
15+
@Parcelize
16+
@JsonObject
17+
data class UpcomingEventsOverall(
18+
@JsonField(name = ["ocs"])
19+
var ocs: UpcomingEventsOCS?
20+
) : Parcelable {
21+
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
22+
constructor() : this(null)
23+
}

0 commit comments

Comments
 (0)