Skip to content

Commit b9385ce

Browse files
fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.4.7 (#4548)
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.4.8 * Fix API breaks: - Add `ReplyParameters` class and parameters to send functions. - Remove outdated OIDC related values. - Stop pre-processing the timeline to add the timeline start item, this is already done by the SDK. * Use the new function to reply to messages in a quick reply from a notification, however: 1. We don't have the thread id value at the moment since the SDK does not provide it yet. 2. The replied to event id wasn't being passed from the notification info. * Remove also timeline start virtual item for DMs, since this wasn't present before either --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jorge Martín <[email protected]>
1 parent 7bcfb20 commit b9385ce

File tree

35 files changed

+454
-296
lines changed

35 files changed

+454
-296
lines changed

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
3232
import io.element.android.libraries.featureflag.api.FeatureFlags
3333
import io.element.android.libraries.matrix.api.core.ProgressCallback
3434
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
35+
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
3536
import io.element.android.libraries.mediaupload.api.MediaSender
3637
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
3738
import io.element.android.libraries.mediaupload.api.allFiles
@@ -127,6 +128,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
127128
caption = caption,
128129
sendActionState = sendActionState,
129130
dismissAfterSend = !useSendQueue,
131+
replyParameters = null,
130132
)
131133
}
132134
}
@@ -237,6 +239,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
237239
caption: String?,
238240
sendActionState: MutableState<SendActionState>,
239241
dismissAfterSend: Boolean,
242+
replyParameters: ReplyParameters?,
240243
) = runCatching {
241244
val context = coroutineContext
242245
val progressCallback = object : ProgressCallback {
@@ -251,7 +254,8 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
251254
mediaUploadInfo = mediaUploadInfo,
252255
caption = caption,
253256
formattedCaption = null,
254-
progressCallback = progressCallback
257+
progressCallback = progressCallback,
258+
replyParameters = replyParameters,
255259
).getOrThrow()
256260
}.fold(
257261
onSuccess = {

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
5353
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
5454
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
5555
import io.element.android.libraries.matrix.api.room.isDm
56+
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
5657
import io.element.android.libraries.matrix.api.timeline.TimelineException
5758
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
5859
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
@@ -452,7 +453,19 @@ class MessageComposerPresenter @AssistedInject constructor(
452453
}
453454
is MessageComposerMode.Reply -> {
454455
timelineController.invokeOnCurrentTimeline {
455-
replyMessage(capturedMode.eventId, message.markdown, message.html, message.intentionalMentions)
456+
with(capturedMode) {
457+
replyMessage(
458+
body = message.markdown,
459+
htmlBody = message.html,
460+
intentionalMentions = message.intentionalMentions,
461+
replyParameters = ReplyParameters(
462+
inReplyToEventId = eventId,
463+
enforceThreadReply = inThread,
464+
// This should be false until we add a way to make a reply in a thread an explicit reply to the provided eventId
465+
replyWithinThread = false,
466+
),
467+
)
468+
}
456469
}
457470
}
458471
}

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
3030
import io.element.android.libraries.matrix.api.media.VideoInfo
3131
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
3232
import io.element.android.libraries.matrix.api.room.MatrixRoom
33+
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
3334
import io.element.android.libraries.matrix.test.A_CAPTION
3435
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
3536
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
@@ -105,7 +106,8 @@ class AttachmentsPreviewPresenterTest {
105106

106107
@Test
107108
fun `present - send media success scenario`() = runTest {
108-
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
109+
val sendFileResult =
110+
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
109111
Result.success(FakeMediaUploadHandler())
110112
}
111113
val room = FakeMatrixRoom(
@@ -142,7 +144,8 @@ class AttachmentsPreviewPresenterTest {
142144

143145
@Test
144146
fun `present - send media after pre-processing success scenario`() = runTest {
145-
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
147+
val sendFileResult =
148+
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
146149
Result.success(FakeMediaUploadHandler())
147150
}
148151
val room = FakeMatrixRoom(
@@ -177,7 +180,8 @@ class AttachmentsPreviewPresenterTest {
177180

178181
@Test
179182
fun `present - send media before pre-processing success scenario`() = runTest {
180-
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
183+
val sendFileResult =
184+
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
181185
Result.success(FakeMediaUploadHandler())
182186
}
183187
val room = FakeMatrixRoom(
@@ -287,7 +291,7 @@ class AttachmentsPreviewPresenterTest {
287291
@Test
288292
fun `present - send image with caption success scenario`() = runTest {
289293
val sendImageResult =
290-
lambdaRecorder<File, File?, ImageInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
294+
lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? ->
291295
Result.success(FakeMediaUploadHandler())
292296
}
293297
val mediaPreProcessor = FakeMediaPreProcessor().apply {
@@ -320,6 +324,7 @@ class AttachmentsPreviewPresenterTest {
320324
value(A_CAPTION),
321325
any(),
322326
any(),
327+
any(),
323328
)
324329
onDoneListener.assertions().isCalledOnce()
325330
}
@@ -328,7 +333,7 @@ class AttachmentsPreviewPresenterTest {
328333
@Test
329334
fun `present - send video with caption success scenario`() = runTest {
330335
val sendVideoResult =
331-
lambdaRecorder<File, File?, VideoInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
336+
lambdaRecorder { _: File, _: File?, _: VideoInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? ->
332337
Result.success(FakeMediaUploadHandler())
333338
}
334339
val mediaPreProcessor = FakeMediaPreProcessor().apply {
@@ -361,6 +366,7 @@ class AttachmentsPreviewPresenterTest {
361366
value(A_CAPTION),
362367
any(),
363368
any(),
369+
any(),
364370
)
365371
onDoneListener.assertions().isCalledOnce()
366372
}
@@ -369,7 +375,7 @@ class AttachmentsPreviewPresenterTest {
369375
@Test
370376
fun `present - send audio with caption success scenario`() = runTest {
371377
val sendAudioResult =
372-
lambdaRecorder<File, AudioInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
378+
lambdaRecorder<File, AudioInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
373379
Result.success(FakeMediaUploadHandler())
374380
}
375381
val mediaPreProcessor = FakeMediaPreProcessor().apply {
@@ -399,6 +405,7 @@ class AttachmentsPreviewPresenterTest {
399405
value(A_CAPTION),
400406
any(),
401407
any(),
408+
any(),
402409
)
403410
onDoneListener.assertions().isCalledOnce()
404411
}
@@ -407,7 +414,8 @@ class AttachmentsPreviewPresenterTest {
407414
@Test
408415
fun `present - send media failure scenario without media queue`() = runTest {
409416
val failure = MediaPreProcessor.Failure(null)
410-
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
417+
val sendFileResult =
418+
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
411419
Result.failure(failure)
412420
}
413421
val room = FakeMatrixRoom(
@@ -435,7 +443,8 @@ class AttachmentsPreviewPresenterTest {
435443
@Test
436444
fun `present - send media failure scenario with media queue`() = runTest {
437445
val failure = MediaPreProcessor.Failure(null)
438-
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
446+
val sendFileResult =
447+
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
439448
Result.failure(failure)
440449
}
441450
val onDoneListenerResult = lambdaRecorder<Unit> {}

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
4747
import io.element.android.libraries.matrix.api.room.RoomMembershipState
4848
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
4949
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
50+
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
5051
import io.element.android.libraries.matrix.api.timeline.TimelineException
5152
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
5253
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
@@ -611,7 +612,7 @@ class MessageComposerPresenterTest {
611612

612613
@Test
613614
fun `present - reply message`() = runTest {
614-
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<IntentionalMention>, _: Boolean ->
615+
val replyMessageLambda = lambdaRecorder { _: ReplyParameters, _: String, _: String?, _: List<IntentionalMention>, _: Boolean ->
615616
Result.success(Unit)
616617
}
617618
val timeline = FakeTimeline().apply {
@@ -1110,7 +1111,7 @@ class MessageComposerPresenterTest {
11101111
@OptIn(ExperimentalCoroutinesApi::class)
11111112
@Test
11121113
fun `present - send messages with intentional mentions`() = runTest {
1113-
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<IntentionalMention>, _: Boolean ->
1114+
val replyMessageLambda = lambdaRecorder { _: ReplyParameters, _: String, _: String?, _: List<IntentionalMention>, _: Boolean ->
11141115
Result.success(Unit)
11151116
}
11161117
val editMessageLambda = lambdaRecorder { _: EventOrTransactionId, _: String, _: String?, _: List<IntentionalMention> ->

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.messagecomposer.aReplyMode
2121
import io.element.android.features.messages.test.FakeMessageComposerContext
2222
import io.element.android.libraries.matrix.api.core.ProgressCallback
2323
import io.element.android.libraries.matrix.api.media.AudioInfo
24+
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
2425
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
2526
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
2627
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
@@ -60,7 +61,7 @@ class VoiceMessageComposerPresenterTest {
6061
)
6162
private val analyticsService = FakeAnalyticsService()
6263
private val sendVoiceMessageResult =
63-
lambdaRecorder<File, AudioInfo, List<Float>, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _ ->
64+
lambdaRecorder<File, AudioInfo, List<Float>, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
6465
Result.success(FakeMediaUploadHandler())
6566
}
6667
private val matrixRoom = FakeMatrixRoom(

features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes
1818
import io.element.android.libraries.matrix.api.MatrixClient
1919
import io.element.android.libraries.matrix.api.core.ProgressCallback
2020
import io.element.android.libraries.matrix.api.media.FileInfo
21+
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
2122
import io.element.android.libraries.matrix.test.A_MESSAGE
2223
import io.element.android.libraries.matrix.test.A_ROOM_ID
2324
import io.element.android.libraries.matrix.test.FakeMatrixClient
@@ -116,7 +117,8 @@ class SharePresenterTest {
116117

117118
@Test
118119
fun `present - send media ok`() = runTest {
119-
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
120+
val sendFileResult =
121+
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
120122
Result.success(FakeMediaUploadHandler())
121123
}
122124
val matrixRoom = FakeMatrixRoom(

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ jsoup = "org.jsoup:jsoup:1.19.1"
174174
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
175175
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
176176
timber = "com.jakewharton.timber:timber:5.0.1"
177-
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.3.24"
177+
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.4.8"
178178
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
179179
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
180180
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }

libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ package io.element.android.libraries.matrix.api.notification
99

1010
import io.element.android.libraries.matrix.api.core.EventId
1111
import io.element.android.libraries.matrix.api.core.RoomId
12+
import io.element.android.libraries.matrix.api.core.ThreadId
1213
import io.element.android.libraries.matrix.api.core.UserId
1314
import io.element.android.libraries.matrix.api.room.RoomMembershipState
1415
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
1516

1617
data class NotificationData(
1718
val eventId: EventId,
19+
val threadId: ThreadId?,
1820
val roomId: RoomId,
1921
// mxc url
2022
val senderAvatarUrl: String?,

libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibilit
2828
import io.element.android.libraries.matrix.api.room.join.JoinRule
2929
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
3030
import io.element.android.libraries.matrix.api.room.location.AssetType
31+
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
3132
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
3233
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
3334
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
@@ -138,7 +139,8 @@ interface MatrixRoom : Closeable {
138139
imageInfo: ImageInfo,
139140
caption: String?,
140141
formattedCaption: String?,
141-
progressCallback: ProgressCallback?
142+
progressCallback: ProgressCallback?,
143+
replyParameters: ReplyParameters?,
142144
): Result<MediaUploadHandler>
143145

144146
suspend fun sendVideo(
@@ -147,7 +149,8 @@ interface MatrixRoom : Closeable {
147149
videoInfo: VideoInfo,
148150
caption: String?,
149151
formattedCaption: String?,
150-
progressCallback: ProgressCallback?
152+
progressCallback: ProgressCallback?,
153+
replyParameters: ReplyParameters?,
151154
): Result<MediaUploadHandler>
152155

153156
suspend fun sendAudio(
@@ -156,6 +159,7 @@ interface MatrixRoom : Closeable {
156159
caption: String?,
157160
formattedCaption: String?,
158161
progressCallback: ProgressCallback?,
162+
replyParameters: ReplyParameters?,
159163
): Result<MediaUploadHandler>
160164

161165
suspend fun sendFile(
@@ -164,8 +168,36 @@ interface MatrixRoom : Closeable {
164168
caption: String?,
165169
formattedCaption: String?,
166170
progressCallback: ProgressCallback?,
171+
replyParameters: ReplyParameters?,
172+
): Result<MediaUploadHandler>
173+
174+
suspend fun sendVoiceMessage(
175+
file: File,
176+
audioInfo: AudioInfo,
177+
waveform: List<Float>,
178+
progressCallback: ProgressCallback?,
179+
replyParameters: ReplyParameters?,
167180
): Result<MediaUploadHandler>
168181

182+
/**
183+
* Share a location message in the room.
184+
*
185+
* @param body A human readable textual representation of the location.
186+
* @param geoUri A geo URI (RFC 5870) representing the location e.g. `geo:51.5008,0.1247;u=35`.
187+
* Respectively: latitude, longitude, and (optional) uncertainty.
188+
* @param description Optional description of the location to display to the user.
189+
* @param zoomLevel Optional zoom level to display the map at.
190+
* @param assetType Optional type of the location asset.
191+
* Set to SENDER if sharing own location. Set to PIN if sharing any location.
192+
*/
193+
suspend fun sendLocation(
194+
body: String,
195+
geoUri: String,
196+
description: String? = null,
197+
zoomLevel: Int? = null,
198+
assetType: AssetType? = null,
199+
): Result<Unit>
200+
169201
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit>
170202

171203
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
@@ -235,25 +267,6 @@ interface MatrixRoom : Closeable {
235267
*/
236268
suspend fun clearEventCacheStorage(): Result<Unit>
237269

238-
/**
239-
* Share a location message in the room.
240-
*
241-
* @param body A human readable textual representation of the location.
242-
* @param geoUri A geo URI (RFC 5870) representing the location e.g. `geo:51.5008,0.1247;u=35`.
243-
* Respectively: latitude, longitude, and (optional) uncertainty.
244-
* @param description Optional description of the location to display to the user.
245-
* @param zoomLevel Optional zoom level to display the map at.
246-
* @param assetType Optional type of the location asset.
247-
* Set to SENDER if sharing own location. Set to PIN if sharing any location.
248-
*/
249-
suspend fun sendLocation(
250-
body: String,
251-
geoUri: String,
252-
description: String? = null,
253-
zoomLevel: Int? = null,
254-
assetType: AssetType? = null,
255-
): Result<Unit>
256-
257270
/**
258271
* Create a poll in the room.
259272
*
@@ -302,13 +315,6 @@ interface MatrixRoom : Closeable {
302315
*/
303316
suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit>
304317

305-
suspend fun sendVoiceMessage(
306-
file: File,
307-
audioInfo: AudioInfo,
308-
waveform: List<Float>,
309-
progressCallback: ProgressCallback?
310-
): Result<MediaUploadHandler>
311-
312318
/**
313319
* Send a typing notification.
314320
* @param isTyping True if the user is typing, false otherwise.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.matrix.api.room.message
9+
10+
import io.element.android.libraries.matrix.api.core.EventId
11+
12+
data class ReplyParameters(
13+
val inReplyToEventId: EventId,
14+
val enforceThreadReply: Boolean,
15+
val replyWithinThread: Boolean,
16+
)
17+
18+
fun replyInThread(eventId: EventId, explicitReply: Boolean = false) = ReplyParameters(
19+
inReplyToEventId = eventId,
20+
enforceThreadReply = true,
21+
replyWithinThread = explicitReply,
22+
)

0 commit comments

Comments
 (0)