Skip to content

Commit d0cc294

Browse files
authored
Merge pull request #6410 from nextcloud/feature/6409/conversationTags
Feature/6409/conversation tags
2 parents 4d1e72e + f00f78c commit d0cc294

37 files changed

Lines changed: 2544 additions & 87 deletions

app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/26.json

Lines changed: 816 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ import com.nextcloud.talk.models.json.hovercard.HoverCardOverall
2828
import com.nextcloud.talk.models.json.reactions.ReactionsOverall
2929
import com.nextcloud.talk.models.json.status.StatusOverall
3030
import com.nextcloud.talk.models.json.status.predefined.PredefinedStatusOverall
31+
import com.nextcloud.talk.models.json.tags.AssignConversationTagsRequest
32+
import com.nextcloud.talk.models.json.tags.ConversationTagOverall
33+
import com.nextcloud.talk.models.json.tags.ConversationTagsOverall
34+
import com.nextcloud.talk.models.json.tags.CreateConversationTagRequest
35+
import com.nextcloud.talk.models.json.tags.ReorderConversationTagsRequest
36+
import com.nextcloud.talk.models.json.tags.UpdateConversationTagRequest
3137
import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall
3238
import com.nextcloud.talk.models.json.threads.ThreadOverall
3339
import com.nextcloud.talk.models.json.threads.ThreadsOverall
@@ -225,6 +231,43 @@ interface NcApiCoroutines {
225231
@Url url: String
226232
): GenericOverall
227233

234+
@GET
235+
suspend fun getConversationTags(
236+
@Header("Authorization") authorization: String,
237+
@Url url: String
238+
): ConversationTagsOverall
239+
240+
@POST
241+
suspend fun createConversationTag(
242+
@Header("Authorization") authorization: String,
243+
@Url url: String,
244+
@Body body: CreateConversationTagRequest
245+
): ConversationTagOverall
246+
247+
@PUT
248+
suspend fun updateConversationTag(
249+
@Header("Authorization") authorization: String,
250+
@Url url: String,
251+
@Body body: UpdateConversationTagRequest
252+
): ConversationTagOverall
253+
254+
@DELETE
255+
suspend fun deleteConversationTag(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
256+
257+
@PUT
258+
suspend fun reorderConversationTags(
259+
@Header("Authorization") authorization: String,
260+
@Url url: String,
261+
@Body body: ReorderConversationTagsRequest
262+
): ConversationTagsOverall
263+
264+
@POST
265+
suspend fun assignConversationTags(
266+
@Header("Authorization") authorization: String,
267+
@Url url: String,
268+
@Body body: AssignConversationTagsRequest
269+
): RoomOverall
270+
228271
@FormUrlEncoded
229272
@POST
230273
suspend fun notificationCalls(

app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import com.nextcloud.talk.conversationlist.ui.ConversationsListScreen
5353
import com.nextcloud.talk.conversationlist.ui.ConversationsListScreenCallbacks
5454
import com.nextcloud.talk.conversationlist.ui.ConversationsListScreenState
5555
import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel
56+
import com.nextcloud.talk.conversationtags.viewmodels.ConversationTagsViewModel
5657
import com.nextcloud.talk.data.network.NetworkMonitor
5758
import com.nextcloud.talk.data.user.model.User
5859
import com.nextcloud.talk.events.ConversationsListFetchDataEvent
@@ -145,6 +146,8 @@ class ConversationsListActivity : BaseActivity() {
145146

146147
lateinit var conversationsListViewModel: ConversationsListViewModel
147148

149+
lateinit var conversationTagsViewModel: ConversationTagsViewModel
150+
148151
private var currentUser: User? = null
149152
private val snackbarHostState = SnackbarHostState()
150153
private val isMaintenanceModeState = MutableStateFlow(false)
@@ -198,6 +201,7 @@ class ConversationsListActivity : BaseActivity() {
198201
}
199202

200203
conversationsListViewModel = ViewModelProvider(this, viewModelFactory)[ConversationsListViewModel::class.java]
204+
conversationTagsViewModel = ViewModelProvider(this, viewModelFactory)[ConversationTagsViewModel::class.java]
201205

202206
setSupportActionBar(null)
203207
forwardMessageState.value = intent.getBooleanExtra(KEY_FORWARD_MSG_FLAG, false)
@@ -209,6 +213,7 @@ class ConversationsListActivity : BaseActivity() {
209213
setContent {
210214
ConversationsListScreen(
211215
viewModel = conversationsListViewModel,
216+
tagsViewModel = conversationTagsViewModel,
212217
state = buildScreenState(),
213218
callbacks = buildScreenCallbacks()
214219
)
@@ -426,6 +431,7 @@ class ConversationsListActivity : BaseActivity() {
426431

427432
conversationsListViewModel.checkIfThreadsExist()
428433
conversationsListViewModel.reloadFilterFromStorage(UserIdUtils.getIdForUser(currentUser))
434+
conversationTagsViewModel.loadConversationTags()
429435
}
430436

431437
override fun onPause() {
@@ -1140,6 +1146,9 @@ class ConversationsListActivity : BaseActivity() {
11401146
is ConversationOpsAction.AddToHomeScreen -> addConversationToHomeScreen(conversation)
11411147
is ConversationOpsAction.Leave -> leaveConversation(conversation)
11421148
is ConversationOpsAction.Delete -> showDeleteConversationDialog(conversation)
1149+
is ConversationOpsAction.ManageTags -> conversationTagsViewModel.setConversationForTagAssignment(
1150+
conversation
1151+
)
11431152
}
11441153
}
11451154

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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.conversationlist.ui
9+
10+
import android.content.res.Configuration
11+
import androidx.compose.foundation.isSystemInDarkTheme
12+
import androidx.compose.foundation.layout.Column
13+
import androidx.compose.foundation.layout.PaddingValues
14+
import androidx.compose.foundation.layout.Spacer
15+
import androidx.compose.foundation.layout.fillMaxWidth
16+
import androidx.compose.foundation.layout.height
17+
import androidx.compose.foundation.layout.navigationBarsPadding
18+
import androidx.compose.foundation.layout.padding
19+
import androidx.compose.foundation.layout.width
20+
import androidx.compose.foundation.rememberScrollState
21+
import androidx.compose.foundation.verticalScroll
22+
import androidx.compose.material.icons.Icons
23+
import androidx.compose.material.icons.automirrored.outlined.Label
24+
import androidx.compose.material.icons.outlined.Settings
25+
import androidx.compose.material3.ButtonDefaults
26+
import androidx.compose.material3.Checkbox
27+
import androidx.compose.material3.Icon
28+
import androidx.compose.material3.MaterialTheme
29+
import androidx.compose.material3.Surface
30+
import androidx.compose.material3.Text
31+
import androidx.compose.material3.TextButton
32+
import androidx.compose.material3.darkColorScheme
33+
import androidx.compose.material3.lightColorScheme
34+
import androidx.compose.runtime.Composable
35+
import androidx.compose.ui.Modifier
36+
import androidx.compose.ui.graphics.RectangleShape
37+
import androidx.compose.ui.res.dimensionResource
38+
import androidx.compose.ui.res.stringResource
39+
import androidx.compose.ui.text.style.TextAlign
40+
import androidx.compose.ui.tooling.preview.Preview
41+
import com.nextcloud.talk.R
42+
import com.nextcloud.talk.models.domain.ConversationModel
43+
import com.nextcloud.talk.models.json.conversations.ConversationEnums
44+
import com.nextcloud.talk.models.json.participants.Participant
45+
import com.nextcloud.talk.models.json.tags.ConversationTag
46+
47+
@Composable
48+
fun AssignConversationTagsSheetContent(
49+
conversation: ConversationModel,
50+
tags: List<ConversationTag>,
51+
onToggleTag: (String) -> Unit,
52+
onManageTagsClick: () -> Unit
53+
) {
54+
val headerText = conversation.displayName.takeIf { it.isNotEmpty() } ?: conversation.name
55+
Column(
56+
modifier = Modifier
57+
.fillMaxWidth()
58+
.verticalScroll(rememberScrollState())
59+
.navigationBarsPadding()
60+
) {
61+
Text(
62+
text = headerText,
63+
modifier = Modifier
64+
.fillMaxWidth()
65+
.padding(
66+
horizontal = dimensionResource(R.dimen.standard_dialog_padding),
67+
vertical = dimensionResource(R.dimen.standard_half_padding)
68+
),
69+
style = MaterialTheme.typography.titleLarge,
70+
color = MaterialTheme.colorScheme.onSurface
71+
)
72+
if (tags.isEmpty()) {
73+
Text(
74+
text = stringResource(R.string.nc_conversation_tags_empty),
75+
modifier = Modifier
76+
.fillMaxWidth()
77+
.padding(
78+
horizontal = dimensionResource(R.dimen.standard_dialog_padding),
79+
vertical = dimensionResource(R.dimen.standard_half_padding)
80+
),
81+
style = MaterialTheme.typography.bodyMedium,
82+
color = MaterialTheme.colorScheme.onSurfaceVariant
83+
)
84+
} else {
85+
tags.forEach { tag ->
86+
AssignableTagRow(
87+
tag = tag,
88+
isAssigned = conversation.tagIds.contains(tag.id),
89+
onToggleTag = onToggleTag
90+
)
91+
}
92+
}
93+
ManageTagsRow(onManageTagsClick)
94+
}
95+
}
96+
97+
@Composable
98+
private fun AssignableTagRow(tag: ConversationTag, isAssigned: Boolean, onToggleTag: (String) -> Unit) {
99+
TextButton(
100+
onClick = { onToggleTag(tag.id) },
101+
modifier = Modifier
102+
.fillMaxWidth()
103+
.height(dimensionResource(R.dimen.bottom_sheet_item_height)),
104+
shape = RectangleShape,
105+
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.standard_dialog_padding)),
106+
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onSurface)
107+
) {
108+
Icon(
109+
imageVector = Icons.AutoMirrored.Outlined.Label,
110+
contentDescription = null,
111+
tint = MaterialTheme.colorScheme.onSurfaceVariant
112+
)
113+
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.standard_dialog_padding)))
114+
Text(
115+
text = tag.name,
116+
modifier = Modifier.weight(1f),
117+
style = MaterialTheme.typography.bodyLarge,
118+
textAlign = TextAlign.Start
119+
)
120+
Checkbox(checked = isAssigned, onCheckedChange = { onToggleTag(tag.id) })
121+
}
122+
}
123+
124+
@Composable
125+
private fun ManageTagsRow(onManageTagsClick: () -> Unit) {
126+
TextButton(
127+
onClick = onManageTagsClick,
128+
modifier = Modifier
129+
.fillMaxWidth()
130+
.height(dimensionResource(R.dimen.bottom_sheet_item_height)),
131+
shape = RectangleShape,
132+
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.standard_dialog_padding)),
133+
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onSurface)
134+
) {
135+
Icon(
136+
imageVector = Icons.Outlined.Settings,
137+
contentDescription = null,
138+
tint = MaterialTheme.colorScheme.onSurfaceVariant
139+
)
140+
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.standard_dialog_padding)))
141+
Text(
142+
text = stringResource(R.string.nc_conversation_tags_manage),
143+
modifier = Modifier.weight(1f),
144+
style = MaterialTheme.typography.bodyLarge,
145+
textAlign = TextAlign.Start
146+
)
147+
}
148+
}
149+
150+
private fun previewConversation() =
151+
ConversationModel(
152+
internalId = "1@tok",
153+
accountId = 1L,
154+
token = "tok",
155+
name = "alice",
156+
displayName = "Alice",
157+
description = "",
158+
type = ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL,
159+
participantType = Participant.ParticipantType.USER,
160+
sessionId = "",
161+
actorId = "user1",
162+
actorType = "users",
163+
objectType = ConversationEnums.ObjectType.DEFAULT,
164+
notificationLevel = ConversationEnums.NotificationLevel.DEFAULT,
165+
conversationReadOnlyState = ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_WRITE,
166+
lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS,
167+
lobbyTimer = 0L,
168+
canLeaveConversation = true,
169+
canDeleteConversation = true,
170+
unreadMentionDirect = false,
171+
notificationCalls = 0,
172+
avatarVersion = "",
173+
hasCustomAvatar = false,
174+
callStartTime = 0L,
175+
tagIds = listOf("2")
176+
)
177+
178+
private fun previewTags() =
179+
listOf(
180+
ConversationTag(id = "1", name = "Work", sortOrder = 0),
181+
ConversationTag(id = "2", name = "Family", sortOrder = 1)
182+
)
183+
184+
@Preview(showBackground = true, name = "Light")
185+
@Preview(showBackground = true, name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
186+
@Composable
187+
private fun AssignConversationTagsSheetPreview() {
188+
val colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
189+
MaterialTheme(colorScheme = colorScheme) {
190+
Surface(color = MaterialTheme.colorScheme.surfaceContainerLow) {
191+
AssignConversationTagsSheetContent(
192+
conversation = previewConversation(),
193+
tags = previewTags(),
194+
onToggleTag = {},
195+
onManageTagsClick = {}
196+
)
197+
}
198+
}
199+
}

0 commit comments

Comments
 (0)