Skip to content

Commit 96a4001

Browse files
feat(chat): Add swipe-to-reply
AI-assistant: Copilot 1.7.1-243 (Claude Sonnet 4.6) Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
1 parent 6843842 commit 96a4001

8 files changed

Lines changed: 264 additions & 382 deletions

File tree

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,9 @@ class ChatActivity :
619619
advanceLocalLastReadMessageIfNeeded = { advanceLocalLastReadMessageIfNeeded(it) },
620620
updateRemoteLastReadMessageIfNeeded = { updateRemoteLastReadMessageIfNeeded() },
621621
onLongClick = { openMessageActionsDialog(it) },
622+
onSwipeReply = { handleSwipeToReply(it) },
623+
hasChatPermission = this::participantPermissions.isInitialized &&
624+
participantPermissions.hasChatPermission(),
622625
onFileClick = { downloadAndOpenFile(it) },
623626
onPollClick = { pollId, pollName -> openPollDialog(pollId, pollName) },
624627
onVoicePlayPauseClick = { onVoicePlayPauseClickCompose(it) },
@@ -3277,6 +3280,17 @@ class ChatActivity :
32773280
}
32783281
}
32793282

3283+
private fun handleSwipeToReply(messageId: Int) {
3284+
lifecycleScope.launch {
3285+
val chatMessage = chatViewModel.getMessageById(messageId.toLong()).first()
3286+
if (chatMessage.isThread && conversationThreadId == null) {
3287+
openThread(chatMessage)
3288+
} else {
3289+
messageInputViewModel.reply(chatMessage)
3290+
}
3291+
}
3292+
}
3293+
32803294
private fun openMessageActionsDialog(message: ChatMessage) {
32813295
if (message.isTemporary) {
32823296
TempMessageActionsDialog(

app/src/main/java/com/nextcloud/talk/chat/ui/model/ChatMessageUi.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ data class ChatMessageUi(
4242
val messageParameters: Map<String, Map<String, String>> = emptyMap(),
4343
val reactions: List<MessageReactionUi> = emptyList(),
4444
val isEdited: Boolean = false,
45-
val parentMessage: ChatMessageUi? = null
45+
val parentMessage: ChatMessageUi? = null,
46+
val replyable: Boolean = false
4647
)
4748

4849
data class MessageReactionUi(val emoji: String, val amount: Int, val isSelfReaction: Boolean)
@@ -123,7 +124,8 @@ fun ChatMessage.toUiModel(
123124
chatMessage = parentMessage,
124125
lastCommonReadMessageId = 0,
125126
parentMessage = null
126-
)
127+
),
128+
replyable = replyable
127129
)
128130

129131
private fun ChatMessage.normalizeMessageParameters(): Map<String, Map<String, String>> =

app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageView.kt

Lines changed: 87 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ fun ChatMessageView(
5555
isOneToOneConversation: Boolean = false,
5656
conversationThreadId: Long? = null,
5757
onLongClick: ((Int) -> Unit?)? = null,
58+
onSwipeReply: ((Int) -> Unit)? = null,
59+
hasChatPermission: Boolean = true,
5860
onFileClick: (Int) -> Unit = {},
5961
onPollClick: (pollId: String, pollName: String) -> Unit = { _, _ -> },
6062
onVoicePlayPauseClick: (Int) -> Unit = {},
@@ -87,97 +89,102 @@ fun ChatMessageView(
8789
LocalOpenThreadHandler provides onOpenThreadClick,
8890
LocalQuotedMessageClickHandler provides onQuotedMessageClick
8991
) {
90-
Box(
91-
modifier = Modifier
92-
.combinedClickable(
93-
interactionSource = interactionSource,
94-
indication = ripple(),
95-
onClick = { onLongClick?.invoke(message.id) },
96-
onLongClick = { onLongClick?.invoke(message.id) }
97-
)
92+
SwipeToReplyContainer(
93+
replyable = message.replyable && hasChatPermission,
94+
onSwipeReply = { onSwipeReply?.invoke(message.id) }
9895
) {
99-
when (val content = message.content) {
100-
MessageTypeContent.RegularText -> {
101-
TextMessage(
102-
uiMessage = message,
103-
isOneToOneConversation = isOneToOneConversation,
104-
conversationThreadId = conversationThreadId
96+
Box(
97+
modifier = Modifier
98+
.combinedClickable(
99+
interactionSource = interactionSource,
100+
indication = ripple(),
101+
onClick = { onLongClick?.invoke(message.id) },
102+
onLongClick = { onLongClick?.invoke(message.id) }
105103
)
106-
}
104+
) {
105+
when (val content = message.content) {
106+
MessageTypeContent.RegularText -> {
107+
TextMessage(
108+
uiMessage = message,
109+
isOneToOneConversation = isOneToOneConversation,
110+
conversationThreadId = conversationThreadId
111+
)
112+
}
107113

108-
MessageTypeContent.SystemMessage -> {
109-
SystemMessage(message)
110-
}
114+
MessageTypeContent.SystemMessage -> {
115+
SystemMessage(message)
116+
}
111117

112-
is MessageTypeContent.Media -> {
113-
MediaMessage(
114-
typeContent = content,
115-
message = message,
116-
isOneToOneConversation = isOneToOneConversation,
117-
conversationThreadId = conversationThreadId,
118-
onImageClick = onFileClick
119-
)
120-
}
118+
is MessageTypeContent.Media -> {
119+
MediaMessage(
120+
typeContent = content,
121+
message = message,
122+
isOneToOneConversation = isOneToOneConversation,
123+
conversationThreadId = conversationThreadId,
124+
onImageClick = onFileClick
125+
)
126+
}
121127

122-
is MessageTypeContent.LinkPreview -> {
123-
LinkMessage(
124-
typeContent = content,
125-
message = message,
126-
isOneToOneConversation = isOneToOneConversation,
127-
conversationThreadId = conversationThreadId
128-
)
129-
}
128+
is MessageTypeContent.LinkPreview -> {
129+
LinkMessage(
130+
typeContent = content,
131+
message = message,
132+
isOneToOneConversation = isOneToOneConversation,
133+
conversationThreadId = conversationThreadId
134+
)
135+
}
130136

131-
is MessageTypeContent.Geolocation -> {
132-
GeolocationMessage(
133-
typeContent = content,
134-
message = message,
135-
isOneToOneConversation = isOneToOneConversation,
136-
conversationThreadId = conversationThreadId
137-
)
138-
}
137+
is MessageTypeContent.Geolocation -> {
138+
GeolocationMessage(
139+
typeContent = content,
140+
message = message,
141+
isOneToOneConversation = isOneToOneConversation,
142+
conversationThreadId = conversationThreadId
143+
)
144+
}
139145

140-
is MessageTypeContent.Voice -> {
141-
VoiceMessage(
142-
typeContent = content,
143-
message = message,
144-
isOneToOneConversation = isOneToOneConversation,
145-
conversationThreadId = conversationThreadId,
146-
onPlayPauseClick = onVoicePlayPauseClick,
147-
onSeek = onVoiceSeek,
148-
onSpeedClick = onVoiceSpeedClick
149-
)
150-
}
146+
is MessageTypeContent.Voice -> {
147+
VoiceMessage(
148+
typeContent = content,
149+
message = message,
150+
isOneToOneConversation = isOneToOneConversation,
151+
conversationThreadId = conversationThreadId,
152+
onPlayPauseClick = onVoicePlayPauseClick,
153+
onSeek = onVoiceSeek,
154+
onSpeedClick = onVoiceSpeedClick
155+
)
156+
}
151157

152-
is MessageTypeContent.Poll -> {
153-
PollMessage(
154-
typeContent = content,
155-
message = message,
156-
isOneToOneConversation = isOneToOneConversation,
157-
conversationThreadId = conversationThreadId,
158-
onPollClick = onPollClick
159-
)
160-
}
158+
is MessageTypeContent.Poll -> {
159+
PollMessage(
160+
typeContent = content,
161+
message = message,
162+
isOneToOneConversation = isOneToOneConversation,
163+
conversationThreadId = conversationThreadId,
164+
onPollClick = onPollClick
165+
)
166+
}
161167

162-
is MessageTypeContent.Deck -> {
163-
DeckMessage(
164-
typeContent = content,
165-
message = message,
166-
isOneToOneConversation = isOneToOneConversation,
167-
conversationThreadId = conversationThreadId
168-
)
169-
}
168+
is MessageTypeContent.Deck -> {
169+
DeckMessage(
170+
typeContent = content,
171+
message = message,
172+
isOneToOneConversation = isOneToOneConversation,
173+
conversationThreadId = conversationThreadId
174+
)
175+
}
170176

171-
else -> {
172-
Log.d("ChatView", "Unknown message type: ${'$'}content")
177+
else -> {
178+
Log.d("ChatView", "Unknown message type: ${'$'}content")
179+
}
180+
}
181+
if (highlightAlpha.value > 0f) {
182+
Box(
183+
modifier = Modifier
184+
.matchParentSize()
185+
.background(MaterialTheme.colorScheme.primary.copy(alpha = highlightAlpha.value))
186+
)
173187
}
174-
}
175-
if (highlightAlpha.value > 0f) {
176-
Box(
177-
modifier = Modifier
178-
.matchParentSize()
179-
.background(MaterialTheme.colorScheme.primary.copy(alpha = highlightAlpha.value))
180-
)
181188
}
182189
}
183190
}

app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ fun ChatView(
102102
onReactionLongClick: (messageId: Int) -> Unit = {},
103103
onOpenThreadClick: (messageId: Int) -> Unit = {},
104104
onLoadQuotedMessageClick: (messageId: Int) -> Unit = {},
105+
onSwipeReply: ((Int) -> Unit)? = null,
106+
hasChatPermission: Boolean = true,
105107
listState: LazyListState = rememberLazyListState(),
106108
initialUnreadCount: Int = 0,
107109
initialShowUnreadPopup: Boolean = false
@@ -310,6 +312,8 @@ fun ChatView(
310312
conversationThreadId = conversationThreadId,
311313
onFileClick = onFileClick,
312314
onLongClick = onLongClick,
315+
onSwipeReply = onSwipeReply,
316+
hasChatPermission = hasChatPermission,
313317
onPollClick = onPollClick,
314318
onVoicePlayPauseClick = onVoicePlayPauseClick,
315319
onVoiceSeek = onVoiceSeek,

0 commit comments

Comments
 (0)