Skip to content

Commit bfc48fc

Browse files
Merge pull request #6040 from nextcloud/feat/noid/swipte-to-reply
Add swipe to reply (again)
2 parents 6843842 + 605684b commit bfc48fc

11 files changed

Lines changed: 378 additions & 488 deletions

File tree

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

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ import com.nextcloud.talk.ui.PinnedMessageView
155155
import com.nextcloud.talk.ui.PlaybackSpeed
156156
import com.nextcloud.talk.ui.StatusDrawable
157157
import com.nextcloud.talk.ui.chat.ChatView
158+
import com.nextcloud.talk.ui.chat.ChatViewCallbacks
159+
import com.nextcloud.talk.ui.chat.ChatViewState
158160
import com.nextcloud.talk.ui.dialog.DateTimeCompose
159161
import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
160162
import com.nextcloud.talk.ui.dialog.GetPinnedOptionsDialog
@@ -612,22 +614,29 @@ class ChatActivity :
612614
Log.d(TAG, "isOneToOneConversation=" + isOneToOneConversation)
613615

614616
ChatView(
615-
chatItems = uiState.items,
616-
isOneToOneConversation = isOneToOneConversation,
617-
conversationThreadId = conversationThreadId,
618-
onLoadMore = { loadMoreMessagesCompose() },
619-
advanceLocalLastReadMessageIfNeeded = { advanceLocalLastReadMessageIfNeeded(it) },
620-
updateRemoteLastReadMessageIfNeeded = { updateRemoteLastReadMessageIfNeeded() },
621-
onLongClick = { openMessageActionsDialog(it) },
622-
onFileClick = { downloadAndOpenFile(it) },
623-
onPollClick = { pollId, pollName -> openPollDialog(pollId, pollName) },
624-
onVoicePlayPauseClick = { onVoicePlayPauseClickCompose(it) },
625-
onVoiceSeek = { _, progress -> chatViewModel.seekToMediaPlayer(progress) },
626-
onVoiceSpeedClick = { onVoiceSpeedClickCompose(it) },
627-
onReactionClick = { messageId, emoji -> handleReactionClick(messageId, emoji) },
628-
onReactionLongClick = { messageId -> openReactionsDialog(messageId) },
629-
onOpenThreadClick = { messageId -> openThread(messageId.toLong()) },
630-
onLoadQuotedMessageClick = { messageId -> onLoadQuotedMessage(messageId) },
617+
state = ChatViewState(
618+
chatItems = uiState.items,
619+
isOneToOneConversation = isOneToOneConversation,
620+
conversationThreadId = conversationThreadId,
621+
hasChatPermission = this::participantPermissions.isInitialized &&
622+
participantPermissions.hasChatPermission()
623+
),
624+
callbacks = ChatViewCallbacks(
625+
onLoadMore = { loadMoreMessagesCompose() },
626+
advanceLocalLastReadMessageIfNeeded = { advanceLocalLastReadMessageIfNeeded(it) },
627+
updateRemoteLastReadMessageIfNeeded = { updateRemoteLastReadMessageIfNeeded() },
628+
onLongClick = { openMessageActionsDialog(it) },
629+
onSwipeReply = { handleSwipeToReply(it) },
630+
onFileClick = { downloadAndOpenFile(it) },
631+
onPollClick = { pollId, pollName -> openPollDialog(pollId, pollName) },
632+
onVoicePlayPauseClick = { onVoicePlayPauseClickCompose(it) },
633+
onVoiceSeek = { _, progress -> chatViewModel.seekToMediaPlayer(progress) },
634+
onVoiceSpeedClick = { onVoiceSpeedClickCompose(it) },
635+
onReactionClick = { messageId, emoji -> handleReactionClick(messageId, emoji) },
636+
onReactionLongClick = { messageId -> openReactionsDialog(messageId) },
637+
onOpenThreadClick = { messageId -> openThread(messageId.toLong()) },
638+
onLoadQuotedMessageClick = { messageId -> onLoadQuotedMessage(messageId) }
639+
),
631640
listState = listState
632641
)
633642
}
@@ -3277,6 +3286,17 @@ class ChatActivity :
32773286
}
32783287
}
32793288

3289+
private fun handleSwipeToReply(messageId: Int) {
3290+
lifecycleScope.launch {
3291+
val chatMessage = chatViewModel.getMessageById(messageId.toLong()).first()
3292+
if (chatMessage.isThread && conversationThreadId == null) {
3293+
openThread(chatMessage)
3294+
} else {
3295+
messageInputViewModel.reply(chatMessage)
3296+
}
3297+
}
3298+
}
3299+
32803300
private fun openMessageActionsDialog(message: ChatMessage) {
32813301
if (message.isTemporary) {
32823302
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/ChatMessageScaffold.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ private fun RowScope.MessageLeadingDecoration(uiMessage: ChatMessageUi, isOneToO
257257
contentDescription = stringResource(R.string.user_avatar),
258258
modifier = Modifier
259259
.size(48.dp)
260-
.align(Alignment.CenterVertically)
260+
.align(Alignment.Top)
261261
.padding(end = 8.dp)
262262
)
263263
} else if (uiMessage.incoming) {

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

Lines changed: 111 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,32 @@ private val previewWaveform = listOf(
4848
private const val QUOTE_HIGHLIGHT_HOLD_MILLIS = 700L
4949
private const val QUOTE_HIGHLIGHT_FADE_OUT_MILLIS = 1500
5050

51+
data class ChatMessageContext(
52+
val isOneToOneConversation: Boolean = false,
53+
val conversationThreadId: Long? = null,
54+
val hasChatPermission: Boolean = true
55+
)
56+
57+
class ChatMessageCallbacks(
58+
val onLongClick: ((Int) -> Unit?)? = null,
59+
val onSwipeReply: ((Int) -> Unit)? = null,
60+
val onFileClick: (Int) -> Unit = {},
61+
val onPollClick: (String, String) -> Unit = { _, _ -> },
62+
val onVoicePlayPauseClick: (Int) -> Unit = {},
63+
val onVoiceSeek: (Int, Int) -> Unit = { _, _ -> },
64+
val onVoiceSpeedClick: (Int) -> Unit = {},
65+
val onReactionClick: (Int, String) -> Unit = { _, _ -> },
66+
val onReactionLongClick: (Int) -> Unit = {},
67+
val onOpenThreadClick: (Int) -> Unit = {},
68+
val onQuotedMessageClick: (Int) -> Unit = {}
69+
)
70+
5171
@Composable
5272
fun ChatMessageView(
5373
message: ChatMessageUi,
5474
highlightTriggerKey: Long? = null,
55-
isOneToOneConversation: Boolean = false,
56-
conversationThreadId: Long? = null,
57-
onLongClick: ((Int) -> Unit?)? = null,
58-
onFileClick: (Int) -> Unit = {},
59-
onPollClick: (pollId: String, pollName: String) -> Unit = { _, _ -> },
60-
onVoicePlayPauseClick: (Int) -> Unit = {},
61-
onVoiceSeek: (messageId: Int, progress: Int) -> Unit = { _, _ -> },
62-
onVoiceSpeedClick: (Int) -> Unit = {},
63-
onReactionClick: (messageId: Int, emoji: String) -> Unit = { _, _ -> },
64-
onReactionLongClick: (messageId: Int) -> Unit = {},
65-
onOpenThreadClick: (messageId: Int) -> Unit = {},
66-
onQuotedMessageClick: (messageId: Int) -> Unit = {}
75+
context: ChatMessageContext = ChatMessageContext(),
76+
callbacks: ChatMessageCallbacks = ChatMessageCallbacks()
6777
) {
6878
val interactionSource = remember { MutableInteractionSource() }
6979
val lastHandledHighlightKey = rememberSaveable(message.id) { mutableStateOf<Long?>(null) }
@@ -82,102 +92,107 @@ fun ChatMessageView(
8292
}
8393

8494
CompositionLocalProvider(
85-
LocalReactionClickHandler provides onReactionClick,
86-
LocalReactionLongClickHandler provides onReactionLongClick,
87-
LocalOpenThreadHandler provides onOpenThreadClick,
88-
LocalQuotedMessageClickHandler provides onQuotedMessageClick
95+
LocalReactionClickHandler provides callbacks.onReactionClick,
96+
LocalReactionLongClickHandler provides callbacks.onReactionLongClick,
97+
LocalOpenThreadHandler provides callbacks.onOpenThreadClick,
98+
LocalQuotedMessageClickHandler provides callbacks.onQuotedMessageClick
8999
) {
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-
)
100+
SwipeToReplyContainer(
101+
replyable = message.replyable && context.hasChatPermission,
102+
onSwipeReply = { callbacks.onSwipeReply?.invoke(message.id) }
98103
) {
99-
when (val content = message.content) {
100-
MessageTypeContent.RegularText -> {
101-
TextMessage(
102-
uiMessage = message,
103-
isOneToOneConversation = isOneToOneConversation,
104-
conversationThreadId = conversationThreadId
104+
Box(
105+
modifier = Modifier
106+
.combinedClickable(
107+
interactionSource = interactionSource,
108+
indication = ripple(),
109+
onClick = { callbacks.onLongClick?.invoke(message.id) },
110+
onLongClick = { callbacks.onLongClick?.invoke(message.id) }
105111
)
106-
}
112+
) {
113+
when (val content = message.content) {
114+
MessageTypeContent.RegularText -> {
115+
TextMessage(
116+
uiMessage = message,
117+
isOneToOneConversation = context.isOneToOneConversation,
118+
conversationThreadId = context.conversationThreadId
119+
)
120+
}
107121

108-
MessageTypeContent.SystemMessage -> {
109-
SystemMessage(message)
110-
}
122+
MessageTypeContent.SystemMessage -> {
123+
SystemMessage(message)
124+
}
111125

112-
is MessageTypeContent.Media -> {
113-
MediaMessage(
114-
typeContent = content,
115-
message = message,
116-
isOneToOneConversation = isOneToOneConversation,
117-
conversationThreadId = conversationThreadId,
118-
onImageClick = onFileClick
119-
)
120-
}
126+
is MessageTypeContent.Media -> {
127+
MediaMessage(
128+
typeContent = content,
129+
message = message,
130+
isOneToOneConversation = context.isOneToOneConversation,
131+
conversationThreadId = context.conversationThreadId,
132+
onImageClick = callbacks.onFileClick
133+
)
134+
}
121135

122-
is MessageTypeContent.LinkPreview -> {
123-
LinkMessage(
124-
typeContent = content,
125-
message = message,
126-
isOneToOneConversation = isOneToOneConversation,
127-
conversationThreadId = conversationThreadId
128-
)
129-
}
136+
is MessageTypeContent.LinkPreview -> {
137+
LinkMessage(
138+
typeContent = content,
139+
message = message,
140+
isOneToOneConversation = context.isOneToOneConversation,
141+
conversationThreadId = context.conversationThreadId
142+
)
143+
}
130144

131-
is MessageTypeContent.Geolocation -> {
132-
GeolocationMessage(
133-
typeContent = content,
134-
message = message,
135-
isOneToOneConversation = isOneToOneConversation,
136-
conversationThreadId = conversationThreadId
137-
)
138-
}
145+
is MessageTypeContent.Geolocation -> {
146+
GeolocationMessage(
147+
typeContent = content,
148+
message = message,
149+
isOneToOneConversation = context.isOneToOneConversation,
150+
conversationThreadId = context.conversationThreadId
151+
)
152+
}
139153

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-
}
154+
is MessageTypeContent.Voice -> {
155+
VoiceMessage(
156+
typeContent = content,
157+
message = message,
158+
isOneToOneConversation = context.isOneToOneConversation,
159+
conversationThreadId = context.conversationThreadId,
160+
onPlayPauseClick = callbacks.onVoicePlayPauseClick,
161+
onSeek = callbacks.onVoiceSeek,
162+
onSpeedClick = callbacks.onVoiceSpeedClick
163+
)
164+
}
151165

152-
is MessageTypeContent.Poll -> {
153-
PollMessage(
154-
typeContent = content,
155-
message = message,
156-
isOneToOneConversation = isOneToOneConversation,
157-
conversationThreadId = conversationThreadId,
158-
onPollClick = onPollClick
159-
)
160-
}
166+
is MessageTypeContent.Poll -> {
167+
PollMessage(
168+
typeContent = content,
169+
message = message,
170+
isOneToOneConversation = context.isOneToOneConversation,
171+
conversationThreadId = context.conversationThreadId,
172+
onPollClick = callbacks.onPollClick
173+
)
174+
}
161175

162-
is MessageTypeContent.Deck -> {
163-
DeckMessage(
164-
typeContent = content,
165-
message = message,
166-
isOneToOneConversation = isOneToOneConversation,
167-
conversationThreadId = conversationThreadId
168-
)
169-
}
176+
is MessageTypeContent.Deck -> {
177+
DeckMessage(
178+
typeContent = content,
179+
message = message,
180+
isOneToOneConversation = context.isOneToOneConversation,
181+
conversationThreadId = context.conversationThreadId
182+
)
183+
}
170184

171-
else -> {
172-
Log.d("ChatView", "Unknown message type: ${'$'}content")
185+
else -> {
186+
Log.d("ChatView", "Unknown message type: ${'$'}content")
187+
}
188+
}
189+
if (highlightAlpha.value > 0f) {
190+
Box(
191+
modifier = Modifier
192+
.matchParentSize()
193+
.background(MaterialTheme.colorScheme.primary.copy(alpha = highlightAlpha.value))
194+
)
173195
}
174-
}
175-
if (highlightAlpha.value > 0f) {
176-
Box(
177-
modifier = Modifier
178-
.matchParentSize()
179-
.background(MaterialTheme.colorScheme.primary.copy(alpha = highlightAlpha.value))
180-
)
181196
}
182197
}
183198
}

0 commit comments

Comments
 (0)