@@ -30,6 +30,7 @@ import com.hypergonial.chat.model.TypingStartEvent
30
30
import com.hypergonial.chat.model.exceptions.ClientException
31
31
import com.hypergonial.chat.model.getMimeType
32
32
import com.hypergonial.chat.model.payloads.Attachment
33
+ import com.hypergonial.chat.model.payloads.Channel
33
34
import com.hypergonial.chat.model.payloads.Message
34
35
import com.hypergonial.chat.model.payloads.Snowflake
35
36
import com.hypergonial.chat.model.payloads.User
@@ -46,6 +47,7 @@ import com.hypergonial.chat.view.components.subcomponents.LoadMoreMessagesIndica
46
47
import com.hypergonial.chat.view.components.subcomponents.MessageComponent
47
48
import com.hypergonial.chat.view.components.subcomponents.MessageEntryComponent
48
49
import com.hypergonial.chat.view.content.ChannelContent
50
+ import com.hypergonial.chat.view.modifierStates
49
51
import io.github.vinceglb.filekit.core.FileKit
50
52
import io.github.vinceglb.filekit.core.PickerMode
51
53
import io.github.vinceglb.filekit.core.PickerType
@@ -134,9 +136,21 @@ interface ChannelComponent : MainContentComponent, Displayable {
134
136
*/
135
137
fun onMessageDeleteRequested (messageId : Snowflake )
136
138
139
+ /* *
140
+ * Callback called when the user confirms the deletion of a message.
141
+ *
142
+ * @param messageId The ID of the message to delete.
143
+ */
144
+ fun onMessageDeleteConfirmed (messageId : Snowflake )
145
+
146
+ /* * Callback called when the user cancels the deletion of a message. */
147
+ fun onMessageDeleteCancelled ()
148
+
137
149
@Composable override fun Display () = ChannelContent (this )
138
150
139
151
data class State (
152
+ /* * The channel to display */
153
+ val channel : Channel ,
140
154
/* * The value of the chat bar */
141
155
val chatBarValue : TextFieldValue = TextFieldValue (),
142
156
/* * Attachments awaiting upload */
@@ -156,6 +170,8 @@ interface ChannelComponent : MainContentComponent, Displayable {
156
170
val isJumpingToBottom : Boolean = false ,
157
171
/* * If true, the file upload dropdown is open */
158
172
val isFileUploadDropdownOpen : Boolean = false ,
173
+ /* * The message to be deleted */
174
+ val pendingDeleteMessage : MessageComponent ? = null ,
159
175
/* *
160
176
* If true, the client is currently copying file(s) from the clipboard service The view is expected to show a
161
177
* pending attachment in the attachments list
@@ -187,7 +203,7 @@ interface ChannelComponent : MainContentComponent, Displayable {
187
203
class DefaultChannelComponent (
188
204
private val ctx : ComponentContext ,
189
205
private val client : Client ,
190
- private val channelId : Snowflake ,
206
+ channel : Channel ,
191
207
private val guildId : Snowflake ? = null ,
192
208
initialEditorState : TextFieldValue ? = null ,
193
209
private val onReadMessages : (Snowflake ) -> Unit ,
@@ -196,10 +212,12 @@ class DefaultChannelComponent(
196
212
private val scope = ctx.coroutineScope()
197
213
private val logger = Logger .withTag(" DefaultChannelComponent" )
198
214
private var refreshTakingTooLongJob: Job ? = null
215
+ private val channelId: Snowflake = channel.id
199
216
200
217
override val data =
201
218
MutableValue (
202
219
ChannelComponent .State (
220
+ channel = channel,
203
221
chatBarValue = initialEditorState ? : TextFieldValue (),
204
222
typingIndicators =
205
223
mutableStateMapOf<Snowflake , User >().apply {
@@ -253,7 +271,14 @@ class DefaultChannelComponent(
253
271
isPending : Boolean = false,
254
272
hasUploadingAttachments : Boolean = false,
255
273
): MessageComponent {
256
- return DefaultMessageComponent (ctx, client, message, isPending, hasUploadingAttachments)
274
+ return DefaultMessageComponent (
275
+ ctx,
276
+ client,
277
+ message,
278
+ isPending,
279
+ hasUploadingAttachments,
280
+ onDeleteRequest = { id -> onMessageDeleteRequested(id) },
281
+ )
257
282
}
258
283
259
284
/* *
@@ -701,6 +726,36 @@ class DefaultChannelComponent(
701
726
entry.removeMessage(event.id)
702
727
703
728
if (entry.isEmpty()) {
729
+ // Ensure the end indicators are not lost
730
+ if (entry.data.value.topEndIndicator != null ) {
731
+ val index = data.value.messageEntries.indexOf(entry)
732
+
733
+ // Try the top first, then bottom, or create new entry if needed
734
+ val nextEntry = data.value.messageEntries.getOrNull(index + 1 )
735
+ ? : data.value.messageEntries.getOrNull(index - 1 )
736
+ ? : run {
737
+ messageEntryComponent(mutableStateListOf()).also { newEntry ->
738
+ data.value.messageEntries.add(index, newEntry)
739
+ }
740
+ }
741
+
742
+ nextEntry.setTopEndIndicator(entry.data.value.topEndIndicator)
743
+ }
744
+ if (entry.data.value.bottomEndIndicator != null ) {
745
+ val index = data.value.messageEntries.indexOf(entry)
746
+
747
+ // Try the bottom first, then top, or create new entry if needed
748
+ val nextEntry = data.value.messageEntries.getOrNull(index - 1 )
749
+ ? : data.value.messageEntries.getOrNull(index + 1 )
750
+ ? : run {
751
+ messageEntryComponent(mutableStateListOf()).also { newEntry ->
752
+ data.value.messageEntries.add(index, newEntry)
753
+ }
754
+ }
755
+
756
+ nextEntry.setBottomEndIndicator(entry.data.value.bottomEndIndicator)
757
+ }
758
+
704
759
data.value.messageEntries.remove(entry)
705
760
}
706
761
}
@@ -729,11 +784,12 @@ class DefaultChannelComponent(
729
784
}
730
785
}
731
786
732
- /* * Callback called when the client has resumed after being paused.
787
+ /* *
788
+ * Callback called when the client has resumed after being paused.
733
789
*
734
- * This is notably not identical to the onReady listener,
735
- * as that only handles reconnects that were caused due to connection loss.
736
- * * /
790
+ * This is notably not identical to the onReady listener, as that only handles reconnects that were caused due to
791
+ * connection loss.
792
+ */
737
793
@Suppress(" UNUSED_PARAMETER" )
738
794
private suspend fun onLifecycleResume (event : LifecycleResumedEvent ) {
739
795
client.waitUntilReady()
@@ -793,13 +849,27 @@ class DefaultChannelComponent(
793
849
794
850
override fun onEditLastMessage () {
795
851
val lastMessage =
796
- data.value.messageEntries
797
- .firstOrNull { it.author?.id == client.cache.ownUser?.id }?.lastMessage() ? : return
852
+ data.value.messageEntries.firstOrNull { it.author?.id == client.cache.ownUser?.id }?.lastMessage() ? : return
798
853
799
854
lastMessage.onEditStart()
800
855
}
801
856
802
857
override fun onMessageDeleteRequested (messageId : Snowflake ) {
858
+ if (modifierStates.isShiftHeld) {
859
+ onMessageDeleteConfirmed(messageId)
860
+ return
861
+ }
862
+
863
+ data.value =
864
+ data.value.copy(
865
+ pendingDeleteMessage =
866
+ data.value.messageEntries.firstOrNull { it.containsMessage(messageId) }?.getMessage(messageId)
867
+ )
868
+ }
869
+
870
+ override fun onMessageDeleteConfirmed (messageId : Snowflake ) {
871
+ data.value = data.value.copy(pendingDeleteMessage = null )
872
+
803
873
scope.launch {
804
874
try {
805
875
client.deleteMessage(channelId, messageId)
@@ -811,6 +881,10 @@ class DefaultChannelComponent(
811
881
}
812
882
}
813
883
884
+ override fun onMessageDeleteCancelled () {
885
+ data.value = data.value.copy(pendingDeleteMessage = null )
886
+ }
887
+
814
888
override fun onChatBarContentChanged (value : TextFieldValue ) {
815
889
if (value.text != data.value.chatBarValue.text) {
816
890
scope.launch {
0 commit comments