Update dependency org.matrix.rustcomponents:sdk-android to v0.2.50 (#3565)
* Adapt to changes in the SDK * Update dependency org.matrix.rustcomponents:sdk-android to v0.2.50 * Use lambda instead of overriding the `EventDebugInfoProvider` interface * Fix test proposal. We may find a better way to compare data class instance if we need to do more comparison in the future... --------- Co-authored-by: Jorge Martín <jorgem@element.io> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Benoit Marty <benoit@matrix.org>
This commit is contained in:
parent
249104bf38
commit
3001efb525
26 changed files with 216 additions and 175 deletions
|
|
@ -114,7 +114,8 @@ internal class RustEncryptionService(
|
|||
override fun onUpdate(status: RustEnableRecoveryProgress) {
|
||||
enableRecoveryProgressStateFlow.value = enableRecoveryProgressMapper.map(status)
|
||||
}
|
||||
}
|
||||
},
|
||||
passphrase = null,
|
||||
)
|
||||
// enableRecovery returns the encryption key, but we read it from the state flow
|
||||
.let { }
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import kotlinx.coroutines.CancellationException
|
|||
import kotlinx.coroutines.withTimeout
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import org.matrix.rustcomponents.sdk.Timeline
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
||||
import org.matrix.rustcomponents.sdk.contentWithoutRelationFromMessage
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
|
|
@ -40,11 +42,7 @@ class RoomContentForwarder(
|
|||
toRoomIds: List<RoomId>,
|
||||
timeoutMs: Long = 5000L
|
||||
) {
|
||||
val content = fromTimeline
|
||||
.getEventTimelineItemByEventId(eventId.value)
|
||||
.content()
|
||||
.asMessage()
|
||||
?.content()
|
||||
val content = (fromTimeline.getEventTimelineItemByEventId(eventId.value).content as? TimelineItemContent.Message)?.content
|
||||
?: throw ForwardEventException(toRoomIds)
|
||||
|
||||
val targetSlidingSyncRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
|
||||
|
|
@ -58,7 +56,7 @@ class RoomContentForwarder(
|
|||
// Sending a message requires a registered timeline listener
|
||||
targetRoom.timeline().runWithTimelineListenerRegistered {
|
||||
withTimeout(timeoutMs.milliseconds) {
|
||||
targetRoom.timeline().send(content)
|
||||
targetRoom.timeline().send(contentWithoutRelationFromMessage(content))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ class RustMatrixRoom(
|
|||
innerRoom.tryResend(transactionId.value)
|
||||
}
|
||||
|
||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> {
|
||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> {
|
||||
return liveTimeline.cancelSend(transactionId)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import kotlinx.coroutines.flow.onStart
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.EditedContent
|
||||
import org.matrix.rustcomponents.sdk.EventOrTransactionId
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
||||
import org.matrix.rustcomponents.sdk.FormattedBody
|
||||
import org.matrix.rustcomponents.sdk.MessageFormat
|
||||
|
|
@ -274,11 +275,14 @@ class RustTimeline(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun redactEvent(eventId: EventId?, transactionId: TransactionId?, reason: String?): Result<Boolean> = withContext(dispatcher) {
|
||||
override suspend fun redactEvent(eventId: EventId?, transactionId: TransactionId?, reason: String?): Result<Unit> = withContext(dispatcher) {
|
||||
runCatching {
|
||||
getEventTimelineItem(eventId, transactionId).use { item ->
|
||||
inner.redactEvent(item = item, reason = reason)
|
||||
val eventOrTransactionId = if (eventId != null) {
|
||||
EventOrTransactionId.EventId(eventId.value)
|
||||
} else {
|
||||
EventOrTransactionId.TransactionId(transactionId!!.value)
|
||||
}
|
||||
inner.redactEvent(eventOrTransactionId = eventOrTransactionId, reason = reason)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -291,19 +295,22 @@ class RustTimeline(
|
|||
): Result<Unit> =
|
||||
withContext(dispatcher) {
|
||||
runCatching<Unit> {
|
||||
getEventTimelineItem(originalEventId, transactionId).use { item ->
|
||||
val editedContent = EditedContent.RoomMessage(
|
||||
content = MessageEventContent.from(
|
||||
body = body,
|
||||
htmlBody = htmlBody,
|
||||
intentionalMentions = intentionalMentions
|
||||
),
|
||||
)
|
||||
inner.edit(
|
||||
newContent = editedContent,
|
||||
item = item,
|
||||
)
|
||||
val eventOrTransactionId = if (originalEventId != null) {
|
||||
EventOrTransactionId.EventId(originalEventId.value)
|
||||
} else {
|
||||
EventOrTransactionId.TransactionId(transactionId!!.value)
|
||||
}
|
||||
val editedContent = EditedContent.RoomMessage(
|
||||
content = MessageEventContent.from(
|
||||
body = body,
|
||||
htmlBody = htmlBody,
|
||||
intentionalMentions = intentionalMentions
|
||||
),
|
||||
)
|
||||
inner.edit(
|
||||
newContent = editedContent,
|
||||
eventOrTransactionId = eventOrTransactionId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -343,6 +350,7 @@ class RustTimeline(
|
|||
}
|
||||
|
||||
@Throws
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private suspend fun getEventTimelineItem(eventId: EventId?, transactionId: TransactionId?): EventTimelineItem {
|
||||
return try {
|
||||
when {
|
||||
|
|
@ -411,7 +419,8 @@ class RustTimeline(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> = redactEvent(eventId = null, transactionId = transactionId, reason = null)
|
||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> =
|
||||
redactEvent(eventId = null, transactionId = transactionId, reason = null)
|
||||
|
||||
override suspend fun sendLocation(
|
||||
body: String,
|
||||
|
|
@ -455,10 +464,6 @@ class RustTimeline(
|
|||
pollKind: PollKind,
|
||||
): Result<Unit> = withContext(dispatcher) {
|
||||
runCatching {
|
||||
val pollStartEvent =
|
||||
inner.getEventTimelineItemByEventId(
|
||||
eventId = pollStartId.value
|
||||
)
|
||||
val editedContent = EditedContent.PollStart(
|
||||
pollData = PollData(
|
||||
question = question,
|
||||
|
|
@ -467,12 +472,10 @@ class RustTimeline(
|
|||
pollKind = pollKind.toInner(),
|
||||
),
|
||||
)
|
||||
pollStartEvent.use {
|
||||
inner.edit(
|
||||
newContent = editedContent,
|
||||
item = it,
|
||||
)
|
||||
}
|
||||
inner.edit(
|
||||
newContent = editedContent,
|
||||
eventOrTransactionId = EventOrTransactionId.EventId(pollStartId.value),
|
||||
)
|
||||
}.map { }
|
||||
}
|
||||
|
||||
|
|
@ -482,7 +485,7 @@ class RustTimeline(
|
|||
): Result<Unit> = withContext(dispatcher) {
|
||||
runCatching {
|
||||
inner.sendPollResponse(
|
||||
pollStartId = pollStartId.value,
|
||||
pollStartEventId = pollStartId.value,
|
||||
answers = answers,
|
||||
)
|
||||
}
|
||||
|
|
@ -494,7 +497,7 @@ class RustTimeline(
|
|||
): Result<Unit> = withContext(dispatcher) {
|
||||
runCatching {
|
||||
inner.endPoll(
|
||||
pollStartId = pollStartId.value,
|
||||
pollStartEventId = pollStartId.value,
|
||||
text = text,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,5 +41,5 @@ internal fun TimelineDiff.eventOrigin(): EventItemOrigin? {
|
|||
}
|
||||
|
||||
private fun TimelineItem.eventOrigin(): EventItemOrigin? {
|
||||
return asEvent()?.origin()
|
||||
return asEvent()?.origin
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
||||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import io.element.android.libraries.matrix.impl.timeline.reply.InReplyToMapper
|
||||
import org.matrix.rustcomponents.sdk.Message
|
||||
import org.matrix.rustcomponents.sdk.MessageType
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody
|
||||
import org.matrix.rustcomponents.sdk.MessageContent as Message
|
||||
import org.matrix.rustcomponents.sdk.MessageFormat as RustMessageFormat
|
||||
import org.matrix.rustcomponents.sdk.MessageType as RustMessageType
|
||||
|
||||
|
|
@ -34,13 +34,13 @@ class EventMessageMapper {
|
|||
private val inReplyToMapper by lazy { InReplyToMapper(TimelineEventContentMapper()) }
|
||||
|
||||
fun map(message: Message): MessageContent = message.use {
|
||||
val type = it.msgtype().use(this::mapMessageType)
|
||||
val inReplyToEvent: InReplyTo? = it.inReplyTo()?.use(inReplyToMapper::map)
|
||||
val type = it.msgType.use(this::mapMessageType)
|
||||
val inReplyToEvent: InReplyTo? = it.inReplyTo?.use(inReplyToMapper::map)
|
||||
MessageContent(
|
||||
body = it.body(),
|
||||
body = it.body,
|
||||
inReplyTo = inReplyToEvent,
|
||||
isEdited = it.isEdited(),
|
||||
isThreaded = it.isThreaded(),
|
||||
isEdited = it.isEdited,
|
||||
isThreaded = it.threadRoot != null,
|
||||
type = type
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventDebugInfoProvider
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventShieldsProvider
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||
|
|
@ -23,11 +25,14 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemE
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.matrix.rustcomponents.sdk.EventOrTransactionId
|
||||
import org.matrix.rustcomponents.sdk.EventSendState
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfoProvider
|
||||
import org.matrix.rustcomponents.sdk.Reaction
|
||||
import org.matrix.rustcomponents.sdk.ShieldState
|
||||
import uniffi.matrix_sdk_common.ShieldStateCode
|
||||
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
|
||||
import org.matrix.rustcomponents.sdk.EventShieldsProvider as RustEventShieldsProvider
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo
|
||||
import org.matrix.rustcomponents.sdk.ProfileDetails as RustProfileDetails
|
||||
|
|
@ -37,25 +42,25 @@ import uniffi.matrix_sdk_ui.EventItemOrigin as RustEventItemOrigin
|
|||
class EventTimelineItemMapper(
|
||||
private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper(),
|
||||
) {
|
||||
fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use {
|
||||
fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.run {
|
||||
EventTimelineItem(
|
||||
eventId = it.eventId()?.let(::EventId),
|
||||
transactionId = it.transactionId()?.let(::TransactionId),
|
||||
isEditable = it.isEditable(),
|
||||
canBeRepliedTo = it.canBeRepliedTo(),
|
||||
isLocal = it.isLocal(),
|
||||
isOwn = it.isOwn(),
|
||||
isRemote = it.isRemote(),
|
||||
localSendState = it.localSendState()?.map(),
|
||||
reactions = it.reactions().map(),
|
||||
receipts = it.readReceipts().map(),
|
||||
sender = UserId(it.sender()),
|
||||
senderProfile = it.senderProfile().map(),
|
||||
timestamp = it.timestamp().toLong(),
|
||||
content = contentMapper.map(it.content()),
|
||||
debugInfo = it.debugInfo().map(),
|
||||
origin = it.origin()?.map(),
|
||||
messageShield = it.getShield(false)?.map(),
|
||||
eventId = eventOrTransactionId.eventId(),
|
||||
transactionId = eventOrTransactionId.transactionId(),
|
||||
isEditable = isEditable,
|
||||
canBeRepliedTo = canBeRepliedTo,
|
||||
isLocal = isLocal,
|
||||
isOwn = isOwn,
|
||||
isRemote = isRemote,
|
||||
localSendState = localSendState?.map(),
|
||||
reactions = reactions.map(),
|
||||
receipts = readReceipts.map(),
|
||||
sender = UserId(sender),
|
||||
senderProfile = senderProfile.map(),
|
||||
timestamp = timestamp.toLong(),
|
||||
content = contentMapper.map(content),
|
||||
debugInfoProvider = RustEventDebugInfoProvider(debugInfoProvider),
|
||||
origin = origin?.map(),
|
||||
messageShieldProvider = RustEventShieldsProvider(shieldsProvider)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -162,3 +167,23 @@ private fun ShieldState?.map(): MessageShield? {
|
|||
ShieldStateCode.PREVIOUSLY_VERIFIED -> MessageShield.PreviouslyVerified(isCritical)
|
||||
}
|
||||
}
|
||||
|
||||
class RustEventDebugInfoProvider(private val debugInfoProvider: EventTimelineItemDebugInfoProvider) : EventDebugInfoProvider {
|
||||
override fun get(): TimelineItemDebugInfo {
|
||||
return debugInfoProvider.get().map()
|
||||
}
|
||||
}
|
||||
|
||||
class RustEventShieldsProvider(private val shieldsProvider: RustEventShieldsProvider) : EventShieldsProvider {
|
||||
override fun getShield(strict: Boolean): MessageShield? {
|
||||
return shieldsProvider.getShields(strict)?.map()
|
||||
}
|
||||
}
|
||||
|
||||
private fun EventOrTransactionId.eventId(): EventId? {
|
||||
return (this as? EventOrTransactionId.EventId)?.let { EventId(it.eventId) }
|
||||
}
|
||||
|
||||
private fun EventOrTransactionId.transactionId(): TransactionId? {
|
||||
return (this as? EventOrTransactionId.TransactionId)?.let { TransactionId(it.transactionId) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import io.element.android.libraries.matrix.impl.poll.map
|
|||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import uniffi.matrix_sdk_ui.RoomPinnedEventsChange
|
||||
import org.matrix.rustcomponents.sdk.EncryptedMessage as RustEncryptedMessage
|
||||
|
|
@ -42,87 +41,78 @@ class TimelineEventContentMapper(
|
|||
) {
|
||||
fun map(content: TimelineItemContent): EventContent {
|
||||
return content.use {
|
||||
content.kind().use { kind ->
|
||||
map(content, kind)
|
||||
when (it) {
|
||||
is TimelineItemContent.FailedToParseMessageLike -> {
|
||||
FailedToParseMessageLikeContent(
|
||||
eventType = it.eventType,
|
||||
error = it.error
|
||||
)
|
||||
}
|
||||
is TimelineItemContent.FailedToParseState -> {
|
||||
FailedToParseStateContent(
|
||||
eventType = it.eventType,
|
||||
stateKey = it.stateKey,
|
||||
error = it.error
|
||||
)
|
||||
}
|
||||
is TimelineItemContent.Message -> {
|
||||
eventMessageMapper.map(it.content)
|
||||
}
|
||||
is TimelineItemContent.ProfileChange -> {
|
||||
ProfileChangeContent(
|
||||
displayName = it.displayName,
|
||||
prevDisplayName = it.prevDisplayName,
|
||||
avatarUrl = it.avatarUrl,
|
||||
prevAvatarUrl = it.prevAvatarUrl
|
||||
)
|
||||
}
|
||||
TimelineItemContent.RedactedMessage -> {
|
||||
RedactedContent
|
||||
}
|
||||
is TimelineItemContent.RoomMembership -> {
|
||||
RoomMembershipContent(
|
||||
userId = UserId(it.userId),
|
||||
userDisplayName = it.userDisplayName,
|
||||
change = it.change?.map()
|
||||
)
|
||||
}
|
||||
is TimelineItemContent.State -> {
|
||||
StateContent(
|
||||
stateKey = it.stateKey,
|
||||
content = it.content.map()
|
||||
)
|
||||
}
|
||||
is TimelineItemContent.Sticker -> {
|
||||
StickerContent(
|
||||
body = it.body,
|
||||
info = it.info.map(),
|
||||
source = it.source.map(),
|
||||
)
|
||||
}
|
||||
is TimelineItemContent.Poll -> {
|
||||
PollContent(
|
||||
question = it.question,
|
||||
kind = it.kind.map(),
|
||||
maxSelections = it.maxSelections,
|
||||
answers = it.answers.map { answer -> answer.map() }.toImmutableList(),
|
||||
votes = it.votes.mapValues { vote ->
|
||||
vote.value.map { userId -> UserId(userId) }.toImmutableList()
|
||||
}.toImmutableMap(),
|
||||
endTime = it.endTime,
|
||||
isEdited = it.hasBeenEdited,
|
||||
)
|
||||
}
|
||||
is TimelineItemContent.UnableToDecrypt -> {
|
||||
UnableToDecryptContent(
|
||||
data = it.msg.map()
|
||||
)
|
||||
}
|
||||
is TimelineItemContent.CallInvite -> LegacyCallInviteContent
|
||||
is TimelineItemContent.CallNotify -> CallNotifyContent
|
||||
else -> UnknownContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun map(content: TimelineItemContent, kind: TimelineItemContentKind) = when (kind) {
|
||||
is TimelineItemContentKind.FailedToParseMessageLike -> {
|
||||
FailedToParseMessageLikeContent(
|
||||
eventType = kind.eventType,
|
||||
error = kind.error
|
||||
)
|
||||
}
|
||||
is TimelineItemContentKind.FailedToParseState -> {
|
||||
FailedToParseStateContent(
|
||||
eventType = kind.eventType,
|
||||
stateKey = kind.stateKey,
|
||||
error = kind.error
|
||||
)
|
||||
}
|
||||
TimelineItemContentKind.Message -> {
|
||||
val message = content.asMessage()
|
||||
if (message == null) {
|
||||
UnknownContent
|
||||
} else {
|
||||
eventMessageMapper.map(message)
|
||||
}
|
||||
}
|
||||
is TimelineItemContentKind.ProfileChange -> {
|
||||
ProfileChangeContent(
|
||||
displayName = kind.displayName,
|
||||
prevDisplayName = kind.prevDisplayName,
|
||||
avatarUrl = kind.avatarUrl,
|
||||
prevAvatarUrl = kind.prevAvatarUrl
|
||||
)
|
||||
}
|
||||
TimelineItemContentKind.RedactedMessage -> {
|
||||
RedactedContent
|
||||
}
|
||||
is TimelineItemContentKind.RoomMembership -> {
|
||||
RoomMembershipContent(
|
||||
userId = UserId(kind.userId),
|
||||
userDisplayName = kind.userDisplayName,
|
||||
change = kind.change?.map()
|
||||
)
|
||||
}
|
||||
is TimelineItemContentKind.State -> {
|
||||
StateContent(
|
||||
stateKey = kind.stateKey,
|
||||
content = kind.content.map()
|
||||
)
|
||||
}
|
||||
is TimelineItemContentKind.Sticker -> {
|
||||
StickerContent(
|
||||
body = kind.body,
|
||||
info = kind.info.map(),
|
||||
source = kind.source.map(),
|
||||
)
|
||||
}
|
||||
is TimelineItemContentKind.Poll -> {
|
||||
PollContent(
|
||||
question = kind.question,
|
||||
kind = kind.kind.map(),
|
||||
maxSelections = kind.maxSelections,
|
||||
answers = kind.answers.map { answer -> answer.map() }.toImmutableList(),
|
||||
votes = kind.votes.mapValues { vote ->
|
||||
vote.value.map { userId -> UserId(userId) }.toImmutableList()
|
||||
}.toImmutableMap(),
|
||||
endTime = kind.endTime,
|
||||
isEdited = kind.hasBeenEdited,
|
||||
)
|
||||
}
|
||||
is TimelineItemContentKind.UnableToDecrypt -> {
|
||||
UnableToDecryptContent(
|
||||
data = kind.msg.map()
|
||||
)
|
||||
}
|
||||
is TimelineItemContentKind.CallInvite -> LegacyCallInviteContent
|
||||
is TimelineItemContentKind.CallNotify -> CallNotifyContent
|
||||
else -> UnknownContent
|
||||
}
|
||||
}
|
||||
|
||||
private fun RustMembershipChange.map(): MembershipChange {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ class InReplyToMapper(
|
|||
private val timelineEventContentMapper: TimelineEventContentMapper,
|
||||
) {
|
||||
fun map(inReplyToDetails: InReplyToDetails): InReplyTo {
|
||||
val inReplyToId = EventId(inReplyToDetails.eventId)
|
||||
return when (val event = inReplyToDetails.event) {
|
||||
val inReplyToId = EventId(inReplyToDetails.eventId())
|
||||
return when (val event = inReplyToDetails.event()) {
|
||||
is RepliedToEventDetails.Ready -> {
|
||||
InReplyTo.Ready(
|
||||
eventId = inReplyToId,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue