Merge branch 'develop' into feature/fga/advanced_settings_moderation_and_safety
This commit is contained in:
commit
497022291b
278 changed files with 2632 additions and 1649 deletions
|
|
@ -14,14 +14,10 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
|||
import io.element.android.libraries.designsystem.components.Badge
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.badgeInfoBackgroundColor
|
||||
import io.element.android.libraries.designsystem.theme.badgeInfoContentColor
|
||||
import io.element.android.libraries.designsystem.theme.badgeNegativeBackgroundColor
|
||||
import io.element.android.libraries.designsystem.theme.badgeNegativeContentColor
|
||||
import io.element.android.libraries.designsystem.theme.badgeNeutralBackgroundColor
|
||||
import io.element.android.libraries.designsystem.theme.badgeNeutralContentColor
|
||||
import io.element.android.libraries.designsystem.theme.badgePositiveBackgroundColor
|
||||
import io.element.android.libraries.designsystem.theme.badgePositiveContentColor
|
||||
|
||||
object MatrixBadgeAtom {
|
||||
data class MatrixBadgeData(
|
||||
|
|
@ -42,22 +38,22 @@ object MatrixBadgeAtom {
|
|||
data: MatrixBadgeData,
|
||||
) {
|
||||
val backgroundColor = when (data.type) {
|
||||
Type.Positive -> ElementTheme.colors.badgePositiveBackgroundColor
|
||||
Type.Positive -> ElementTheme.colors.bgBadgeAccent
|
||||
Type.Neutral -> ElementTheme.colors.badgeNeutralBackgroundColor
|
||||
Type.Negative -> ElementTheme.colors.badgeNegativeBackgroundColor
|
||||
Type.Info -> ElementTheme.colors.badgeInfoBackgroundColor
|
||||
Type.Info -> ElementTheme.colors.bgBadgeInfo
|
||||
}
|
||||
val textColor = when (data.type) {
|
||||
Type.Positive -> ElementTheme.colors.badgePositiveContentColor
|
||||
Type.Positive -> ElementTheme.colors.textBadgeAccent
|
||||
Type.Neutral -> ElementTheme.colors.badgeNeutralContentColor
|
||||
Type.Negative -> ElementTheme.colors.badgeNegativeContentColor
|
||||
Type.Info -> ElementTheme.colors.badgeInfoContentColor
|
||||
Type.Info -> ElementTheme.colors.textBadgeInfo
|
||||
}
|
||||
val iconColor = when (data.type) {
|
||||
Type.Positive -> ElementTheme.colors.iconSuccessPrimary
|
||||
Type.Positive -> ElementTheme.colors.textBadgeAccent
|
||||
Type.Neutral -> ElementTheme.colors.iconSecondary
|
||||
Type.Negative -> ElementTheme.colors.iconCriticalPrimary
|
||||
Type.Info -> ElementTheme.colors.iconInfoPrimary
|
||||
Type.Info -> ElementTheme.colors.textBadgeInfo
|
||||
}
|
||||
Badge(
|
||||
text = data.text,
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ import io.element.android.compound.theme.ElementTheme
|
|||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.badgePositiveBackgroundColor
|
||||
import io.element.android.libraries.designsystem.theme.badgePositiveContentColor
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
|
@ -76,9 +74,9 @@ internal fun BadgePreview() {
|
|||
Badge(
|
||||
text = "Trusted",
|
||||
icon = CompoundIcons.Verified(),
|
||||
backgroundColor = ElementTheme.colors.badgePositiveBackgroundColor,
|
||||
textColor = ElementTheme.colors.badgePositiveContentColor,
|
||||
iconColor = ElementTheme.colors.iconSuccessPrimary,
|
||||
backgroundColor = ElementTheme.colors.bgBadgeAccent,
|
||||
textColor = ElementTheme.colors.textBadgeAccent,
|
||||
iconColor = ElementTheme.colors.textBadgeAccent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ fun ClickableLinkText(
|
|||
|
||||
fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString {
|
||||
val original = this
|
||||
val spannable = SpannableString(this.text)
|
||||
val spannable = SpannableString.valueOf(this.text)
|
||||
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES)
|
||||
|
||||
val spans = spannable.getSpans(0, spannable.length, URLSpan::class.java)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@
|
|||
package io.element.android.libraries.designsystem.modifiers
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
fun Modifier.clickableIfNotNull(onClick: (() -> Unit)? = null): Modifier = then(
|
||||
if (onClick != null) {
|
||||
|
|
@ -17,3 +21,11 @@ fun Modifier.clickableIfNotNull(onClick: (() -> Unit)? = null): Modifier = then(
|
|||
Modifier
|
||||
}
|
||||
)
|
||||
|
||||
fun Modifier.niceClickable(
|
||||
onClick: () -> Unit,
|
||||
): Modifier {
|
||||
return clip(RoundedCornerShape(4.dp))
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 4.dp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
package io.element.android.libraries.designsystem.text
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannedString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.UnderlineSpan
|
||||
|
|
@ -26,7 +26,7 @@ import io.element.android.compound.theme.LinkColor
|
|||
|
||||
fun String.toAnnotatedString(): AnnotatedString = buildAnnotatedString {
|
||||
append(this@toAnnotatedString)
|
||||
val spannable = SpannableString(this@toAnnotatedString)
|
||||
val spannable = SpannedString.valueOf(this@toAnnotatedString)
|
||||
spannable.getSpans(0, spannable.length, Any::class.java).forEach { span ->
|
||||
val start = spannable.getSpanStart(span)
|
||||
val end = spannable.getSpanEnd(span)
|
||||
|
|
|
|||
|
|
@ -38,77 +38,42 @@ val SemanticColors.placeholderBackground
|
|||
get() = bgSubtleSecondary
|
||||
|
||||
// This color is not present in Semantic color, so put hard-coded value for now
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.messageFromMeBackground
|
||||
get() = if (isLight) {
|
||||
// We want LightDesignTokens.colorGray400
|
||||
Color(0xFFE1E6EC)
|
||||
} else {
|
||||
// We want DarkDesignTokens.colorGray500
|
||||
Color(0xFF323539)
|
||||
}
|
||||
get() = if (isLight) LightColorTokens.colorGray400 else DarkColorTokens.colorGray500
|
||||
|
||||
// This color is not present in Semantic color, so put hard-coded value for now
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.messageFromOtherBackground
|
||||
get() = if (isLight) {
|
||||
// We want LightDesignTokens.colorGray300
|
||||
Color(0xFFF0F2F5)
|
||||
} else {
|
||||
// We want DarkDesignTokens.colorGray400
|
||||
Color(0xFF26282D)
|
||||
}
|
||||
get() = if (isLight) LightColorTokens.colorGray300 else DarkColorTokens.colorGray400
|
||||
|
||||
// This color is not present in Semantic color, so put hard-coded value for now
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.progressIndicatorTrackColor
|
||||
get() = if (isLight) {
|
||||
// We want LightDesignTokens.colorAlphaGray500
|
||||
Color(0x33052448)
|
||||
} else {
|
||||
// We want DarkDesignTokens.colorAlphaGray500
|
||||
Color(0x25F4F7FA)
|
||||
}
|
||||
get() = if (isLight) LightColorTokens.colorAlphaGray500 else DarkColorTokens.colorAlphaGray500
|
||||
|
||||
// This color is not present in Semantic color, so put hard-coded value for now
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.iconSuccessPrimaryBackground
|
||||
get() = if (isLight) {
|
||||
// We want LightDesignTokens.colorGreen300
|
||||
Color(0xffe3f7ed)
|
||||
} else {
|
||||
// We want DarkDesignTokens.colorGreen300
|
||||
Color(0xff002513)
|
||||
}
|
||||
get() = if (isLight) LightColorTokens.colorGreen300 else DarkColorTokens.colorGreen300
|
||||
|
||||
// This color is not present in Semantic color, so put hard-coded value for now
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.bgSubtleTertiary
|
||||
get() = if (isLight) {
|
||||
// We want LightDesignTokens.colorGray100
|
||||
Color(0xfffbfcfd)
|
||||
} else {
|
||||
// We want DarkDesignTokens.colorGray100
|
||||
Color(0xff14171b)
|
||||
}
|
||||
get() = if (isLight) LightColorTokens.colorGray100 else DarkColorTokens.colorGray100
|
||||
|
||||
// Temporary color, which is not in the token right now
|
||||
val SemanticColors.temporaryColorBgSpecial
|
||||
get() = if (isLight) Color(0xFFE4E8F0) else Color(0xFF3A4048)
|
||||
|
||||
// This color is not present in Semantic color, so put hard-coded value for now
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.pinDigitBg
|
||||
get() = if (isLight) {
|
||||
// We want LightDesignTokens.colorGray300
|
||||
Color(0xFFF0F2F5)
|
||||
} else {
|
||||
// We want DarkDesignTokens.colorGray400
|
||||
Color(0xFF26282D)
|
||||
}
|
||||
get() = if (isLight) LightColorTokens.colorGray300 else DarkColorTokens.colorGray400
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.currentUserMentionPillText
|
||||
get() = if (isLight) {
|
||||
// We want LightDesignTokens.colorGreen1100
|
||||
Color(0xff005c45)
|
||||
} else {
|
||||
// We want DarkDesignTokens.colorGreen1100
|
||||
Color(0xff1fc090)
|
||||
}
|
||||
get() = if (isLight) LightColorTokens.colorGreen1100 else DarkColorTokens.colorGreen1100
|
||||
|
||||
val SemanticColors.currentUserMentionPillBackground
|
||||
get() = if (isLight) {
|
||||
|
|
@ -141,14 +106,6 @@ val SemanticColors.highlightedMessageBackgroundColor
|
|||
|
||||
// Badge colors
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.badgePositiveBackgroundColor
|
||||
get() = if (isLight) LightColorTokens.colorAlphaGreen300 else DarkColorTokens.colorAlphaGreen300
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.badgePositiveContentColor
|
||||
get() = if (isLight) LightColorTokens.colorGreen1100 else DarkColorTokens.colorGreen1100
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.badgeNeutralBackgroundColor
|
||||
get() = if (isLight) LightColorTokens.colorAlphaGray300 else DarkColorTokens.colorAlphaGray300
|
||||
|
|
@ -165,14 +122,6 @@ val SemanticColors.badgeNegativeBackgroundColor
|
|||
val SemanticColors.badgeNegativeContentColor
|
||||
get() = if (isLight) LightColorTokens.colorRed1100 else DarkColorTokens.colorRed1100
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.badgeInfoBackgroundColor
|
||||
get() = if (isLight) LightColorTokens.colorAlphaBlue300 else DarkColorTokens.colorAlphaBlue300
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.badgeInfoContentColor
|
||||
get() = if (isLight) LightColorTokens.colorBlue1100 else DarkColorTokens.colorBlue1100
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.pinnedMessageBannerIndicator
|
||||
get() = if (isLight) LightColorTokens.colorAlphaGray600 else DarkColorTokens.colorAlphaGray600
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ package io.element.android.libraries.matrix.api.notification
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||
|
||||
data class NotificationData(
|
||||
val eventId: EventId,
|
||||
val threadId: ThreadId?,
|
||||
val roomId: RoomId,
|
||||
// mxc url
|
||||
val senderAvatarUrl: String?,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibilit
|
|||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||
|
|
@ -138,7 +139,8 @@ interface MatrixRoom : Closeable {
|
|||
imageInfo: ImageInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun sendVideo(
|
||||
|
|
@ -147,7 +149,8 @@ interface MatrixRoom : Closeable {
|
|||
videoInfo: VideoInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun sendAudio(
|
||||
|
|
@ -156,6 +159,7 @@ interface MatrixRoom : Closeable {
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun sendFile(
|
||||
|
|
@ -164,8 +168,36 @@ interface MatrixRoom : Closeable {
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
/**
|
||||
* Share a location message in the room.
|
||||
*
|
||||
* @param body A human readable textual representation of the location.
|
||||
* @param geoUri A geo URI (RFC 5870) representing the location e.g. `geo:51.5008,0.1247;u=35`.
|
||||
* Respectively: latitude, longitude, and (optional) uncertainty.
|
||||
* @param description Optional description of the location to display to the user.
|
||||
* @param zoomLevel Optional zoom level to display the map at.
|
||||
* @param assetType Optional type of the location asset.
|
||||
* Set to SENDER if sharing own location. Set to PIN if sharing any location.
|
||||
*/
|
||||
suspend fun sendLocation(
|
||||
body: String,
|
||||
geoUri: String,
|
||||
description: String? = null,
|
||||
zoomLevel: Int? = null,
|
||||
assetType: AssetType? = null,
|
||||
): Result<Unit>
|
||||
|
||||
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit>
|
||||
|
||||
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
|
||||
|
|
@ -235,25 +267,6 @@ interface MatrixRoom : Closeable {
|
|||
*/
|
||||
suspend fun clearEventCacheStorage(): Result<Unit>
|
||||
|
||||
/**
|
||||
* Share a location message in the room.
|
||||
*
|
||||
* @param body A human readable textual representation of the location.
|
||||
* @param geoUri A geo URI (RFC 5870) representing the location e.g. `geo:51.5008,0.1247;u=35`.
|
||||
* Respectively: latitude, longitude, and (optional) uncertainty.
|
||||
* @param description Optional description of the location to display to the user.
|
||||
* @param zoomLevel Optional zoom level to display the map at.
|
||||
* @param assetType Optional type of the location asset.
|
||||
* Set to SENDER if sharing own location. Set to PIN if sharing any location.
|
||||
*/
|
||||
suspend fun sendLocation(
|
||||
body: String,
|
||||
geoUri: String,
|
||||
description: String? = null,
|
||||
zoomLevel: Int? = null,
|
||||
assetType: AssetType? = null,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Create a poll in the room.
|
||||
*
|
||||
|
|
@ -302,13 +315,6 @@ interface MatrixRoom : Closeable {
|
|||
*/
|
||||
suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit>
|
||||
|
||||
suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
/**
|
||||
* Send a typing notification.
|
||||
* @param isTyping True if the user is typing, false otherwise.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.room.message
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
data class ReplyParameters(
|
||||
val inReplyToEventId: EventId,
|
||||
val enforceThreadReply: Boolean,
|
||||
val replyWithinThread: Boolean,
|
||||
)
|
||||
|
||||
fun replyInThread(eventId: EventId, explicitReply: Boolean = false) = ReplyParameters(
|
||||
inReplyToEventId = eventId,
|
||||
enforceThreadReply = true,
|
||||
replyWithinThread = explicitReply,
|
||||
)
|
||||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.sync
|
||||
|
||||
import io.element.android.libraries.core.coroutine.mapState
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface SyncService {
|
||||
|
|
@ -25,6 +24,6 @@ interface SyncService {
|
|||
* Flow of [SyncState]. Will be updated as soon as the current [SyncState] changes.
|
||||
*/
|
||||
val syncState: StateFlow<SyncState>
|
||||
}
|
||||
|
||||
fun SyncService.isOnline(): StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
|
||||
val isOnline: StateFlow<Boolean>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
|
|||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||
|
|
@ -49,7 +50,10 @@ interface Timeline : AutoCloseable {
|
|||
val membershipChangeEventReceived: Flow<Unit>
|
||||
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
|
||||
suspend fun paginate(direction: PaginationDirection): Result<Boolean>
|
||||
fun paginationStatus(direction: PaginationDirection): StateFlow<PaginationStatus>
|
||||
|
||||
val backwardPaginationStatus: StateFlow<PaginationStatus>
|
||||
val forwardPaginationStatus: StateFlow<PaginationStatus>
|
||||
|
||||
val timelineItems: Flow<List<MatrixTimelineItem>>
|
||||
|
||||
suspend fun sendMessage(
|
||||
|
|
@ -72,7 +76,7 @@ interface Timeline : AutoCloseable {
|
|||
): Result<Unit>
|
||||
|
||||
suspend fun replyMessage(
|
||||
eventId: EventId,
|
||||
replyParameters: ReplyParameters,
|
||||
body: String,
|
||||
htmlBody: String?,
|
||||
intentionalMentions: List<IntentionalMention>,
|
||||
|
|
@ -85,7 +89,8 @@ interface Timeline : AutoCloseable {
|
|||
imageInfo: ImageInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun sendVideo(
|
||||
|
|
@ -94,18 +99,18 @@ interface Timeline : AutoCloseable {
|
|||
videoInfo: VideoInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result<Unit>
|
||||
|
||||
suspend fun sendAudio(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
): Result<MediaUploadHandler>
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun sendFile(
|
||||
file: File,
|
||||
|
|
@ -113,15 +118,9 @@ interface Timeline : AutoCloseable {
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit>
|
||||
|
||||
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
|
||||
|
||||
suspend fun cancelSend(transactionId: TransactionId): Result<Unit> =
|
||||
redactEvent(transactionId.toEventOrTransactionId(), reason = null)
|
||||
|
||||
/**
|
||||
* Share a location message in the room.
|
||||
*
|
||||
|
|
@ -141,6 +140,23 @@ interface Timeline : AutoCloseable {
|
|||
assetType: AssetType? = null,
|
||||
): Result<Unit>
|
||||
|
||||
suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result<Unit>
|
||||
|
||||
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit>
|
||||
|
||||
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
|
||||
|
||||
suspend fun cancelSend(transactionId: TransactionId): Result<Unit> =
|
||||
redactEvent(transactionId.toEventOrTransactionId(), reason = null)
|
||||
|
||||
/**
|
||||
* Create a poll in the room.
|
||||
*
|
||||
|
|
@ -189,13 +205,6 @@ interface Timeline : AutoCloseable {
|
|||
*/
|
||||
suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit>
|
||||
|
||||
suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun loadReplyDetails(eventId: EventId): InReplyTo
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class RustMatrixClientFactory @Inject constructor(
|
|||
cachePath = sessionPaths.cacheDirectory.absolutePath,
|
||||
)
|
||||
.setSessionDelegate(sessionDelegate)
|
||||
.passphrase(passphrase)
|
||||
.sessionPassphrase(passphrase)
|
||||
.userAgent(userAgentProvider.provide())
|
||||
.addRootCertificates(userCertificatesProvider.provides())
|
||||
.autoEnableBackups(true)
|
||||
|
|
|
|||
|
|
@ -9,12 +9,9 @@ package io.element.android.libraries.matrix.impl.auth
|
|||
|
||||
import io.element.android.libraries.matrix.api.auth.OidcConfig
|
||||
import org.matrix.rustcomponents.sdk.OidcConfiguration
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class OidcConfigurationProvider @Inject constructor(
|
||||
private val baseDirectory: File,
|
||||
) {
|
||||
class OidcConfigurationProvider @Inject constructor() {
|
||||
fun get(): OidcConfiguration = OidcConfiguration(
|
||||
clientName = "Element",
|
||||
redirectUri = OidcConfig.REDIRECT_URI,
|
||||
|
|
@ -29,6 +26,5 @@ class OidcConfigurationProvider @Inject constructor(
|
|||
staticRegistrations = mapOf(
|
||||
"https://id.thirdroom.io/realms/thirdroom" to "elementx",
|
||||
),
|
||||
dynamicRegistrationsFile = File(baseDirectory, "oidc/registrations.json").absolutePath,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
passphrase = pendingPassphrase,
|
||||
slidingSyncType = ClientBuilderSlidingSync.Discovered,
|
||||
)
|
||||
.passphrase(passphrase)
|
||||
.sessionPassphrase(passphrase)
|
||||
.buildWithQrCode(qrCodeData, oidcConfiguration, progressListener)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ class NotificationMapper(
|
|||
)
|
||||
NotificationData(
|
||||
eventId = eventId,
|
||||
// FIXME once the `NotificationItem` in the SDK returns the thread id
|
||||
threadId = null,
|
||||
roomId = roomId,
|
||||
senderAvatarUrl = item.senderInfo.avatarUrl,
|
||||
senderDisplayName = item.senderInfo.displayName,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibilit
|
|||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||
|
|
@ -497,8 +498,17 @@ class RustMatrixRoom(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> {
|
||||
return liveTimeline.sendImage(file, thumbnailFile, imageInfo, caption, formattedCaption, progressCallback)
|
||||
return liveTimeline.sendImage(
|
||||
file = file,
|
||||
thumbnailFile = thumbnailFile,
|
||||
imageInfo = imageInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
progressCallback = progressCallback,
|
||||
replyParameters = replyParameters
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun sendVideo(
|
||||
|
|
@ -508,8 +518,17 @@ class RustMatrixRoom(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> {
|
||||
return liveTimeline.sendVideo(file, thumbnailFile, videoInfo, caption, formattedCaption, progressCallback)
|
||||
return liveTimeline.sendVideo(
|
||||
file = file,
|
||||
thumbnailFile = thumbnailFile,
|
||||
videoInfo = videoInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
progressCallback = progressCallback,
|
||||
replyParameters = replyParameters
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun sendAudio(
|
||||
|
|
@ -518,6 +537,7 @@ class RustMatrixRoom(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> {
|
||||
return liveTimeline.sendAudio(
|
||||
file = file,
|
||||
|
|
@ -525,6 +545,7 @@ class RustMatrixRoom(
|
|||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
progressCallback = progressCallback,
|
||||
replyParameters = replyParameters,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -534,16 +555,44 @@ class RustMatrixRoom(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> {
|
||||
return liveTimeline.sendFile(
|
||||
file,
|
||||
fileInfo,
|
||||
caption,
|
||||
formattedCaption,
|
||||
progressCallback,
|
||||
file = file,
|
||||
fileInfo = fileInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
progressCallback = progressCallback,
|
||||
replyParameters = replyParameters,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> {
|
||||
return liveTimeline.sendVoiceMessage(
|
||||
file = file,
|
||||
audioInfo = audioInfo,
|
||||
waveform = waveform,
|
||||
progressCallback = progressCallback,
|
||||
replyParameters = replyParameters,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun sendLocation(
|
||||
body: String,
|
||||
geoUri: String,
|
||||
description: String?,
|
||||
zoomLevel: Int?,
|
||||
assetType: AssetType?,
|
||||
): Result<Unit> {
|
||||
return liveTimeline.sendLocation(body, geoUri, description, zoomLevel, assetType)
|
||||
}
|
||||
|
||||
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> {
|
||||
return liveTimeline.toggleReaction(emoji, eventOrTransactionId)
|
||||
}
|
||||
|
|
@ -631,16 +680,6 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun sendLocation(
|
||||
body: String,
|
||||
geoUri: String,
|
||||
description: String?,
|
||||
zoomLevel: Int?,
|
||||
assetType: AssetType?,
|
||||
): Result<Unit> {
|
||||
return liveTimeline.sendLocation(body, geoUri, description, zoomLevel, assetType)
|
||||
}
|
||||
|
||||
override suspend fun createPoll(
|
||||
question: String,
|
||||
answers: List<String>,
|
||||
|
|
@ -674,15 +713,6 @@ class RustMatrixRoom(
|
|||
return liveTimeline.endPoll(pollStartId, text)
|
||||
}
|
||||
|
||||
override suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?,
|
||||
): Result<MediaUploadHandler> {
|
||||
return liveTimeline.sendVoiceMessage(file, audioInfo, waveform, progressCallback)
|
||||
}
|
||||
|
||||
override suspend fun typingNotice(isTyping: Boolean) = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.typingNotice(isTyping)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.room.message
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
|
||||
|
||||
fun ReplyParameters.map() = org.matrix.rustcomponents.sdk.ReplyParameters(
|
||||
eventId = inReplyToEventId.value,
|
||||
enforceThread = enforceThreadReply,
|
||||
replyWithinThread = replyWithinThread,
|
||||
)
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.sync
|
||||
|
||||
import io.element.android.libraries.core.coroutine.mapState
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
|
@ -73,4 +74,6 @@ class RustSyncService(
|
|||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle)
|
||||
|
||||
override val isOnline: StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.room.IntentionalMention
|
|||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
|
|
@ -35,6 +36,7 @@ import io.element.android.libraries.matrix.impl.media.toMSC3246range
|
|||
import io.element.android.libraries.matrix.impl.poll.toInner
|
||||
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
|
||||
import io.element.android.libraries.matrix.impl.room.location.toInner
|
||||
import io.element.android.libraries.matrix.impl.room.message.map
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
||||
|
|
@ -54,7 +56,6 @@ import kotlinx.coroutines.cancel
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -127,11 +128,11 @@ class RustTimeline(
|
|||
private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode)
|
||||
private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode)
|
||||
|
||||
private val backPaginationStatus = MutableStateFlow(
|
||||
override val backwardPaginationStatus = MutableStateFlow(
|
||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PINNED_EVENTS)
|
||||
)
|
||||
|
||||
private val forwardPaginationStatus = MutableStateFlow(
|
||||
override val forwardPaginationStatus = MutableStateFlow(
|
||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode == Timeline.Mode.FOCUSED_ON_EVENT)
|
||||
)
|
||||
|
||||
|
|
@ -167,7 +168,7 @@ class RustTimeline(
|
|||
|
||||
private fun updatePaginationStatus(direction: Timeline.PaginationDirection, update: (Timeline.PaginationStatus) -> Timeline.PaginationStatus) {
|
||||
when (direction) {
|
||||
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.getAndUpdate(update)
|
||||
Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus.getAndUpdate(update)
|
||||
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.getAndUpdate(update)
|
||||
}
|
||||
}
|
||||
|
|
@ -185,7 +186,7 @@ class RustTimeline(
|
|||
}
|
||||
}.onFailure { error ->
|
||||
if (error is TimelineException.CannotPaginate) {
|
||||
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}")
|
||||
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backwardPaginationStatus.value}")
|
||||
} else {
|
||||
updatePaginationStatus(direction) { it.copy(isPaginating = false) }
|
||||
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
|
||||
|
|
@ -199,21 +200,14 @@ class RustTimeline(
|
|||
private fun canPaginate(direction: Timeline.PaginationDirection): Boolean {
|
||||
if (!isTimelineInitialized.value) return false
|
||||
return when (direction) {
|
||||
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.value.canPaginate
|
||||
Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus.value.canPaginate
|
||||
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.value.canPaginate
|
||||
}
|
||||
}
|
||||
|
||||
override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow<Timeline.PaginationStatus> {
|
||||
return when (direction) {
|
||||
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus
|
||||
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus
|
||||
}
|
||||
}
|
||||
|
||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
|
||||
_timelineItems,
|
||||
backPaginationStatus,
|
||||
backwardPaginationStatus,
|
||||
forwardPaginationStatus,
|
||||
matrixRoom.roomInfoFlow.map { it.creator to it.isDm }.distinctUntilChanged(),
|
||||
isTimelineInitialized,
|
||||
|
|
@ -336,7 +330,7 @@ class RustTimeline(
|
|||
}
|
||||
|
||||
override suspend fun replyMessage(
|
||||
eventId: EventId,
|
||||
replyParameters: ReplyParameters,
|
||||
body: String,
|
||||
htmlBody: String?,
|
||||
intentionalMentions: List<IntentionalMention>,
|
||||
|
|
@ -344,7 +338,10 @@ class RustTimeline(
|
|||
): Result<Unit> = withContext(dispatcher) {
|
||||
runCatching {
|
||||
val msg = MessageEventContent.from(body, htmlBody, intentionalMentions)
|
||||
inner.sendReply(msg, eventId.value)
|
||||
inner.sendReply(
|
||||
msg = msg,
|
||||
replyParams = replyParameters.map(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -355,6 +352,7 @@ class RustTimeline(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> {
|
||||
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
|
||||
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
|
||||
|
|
@ -367,6 +365,7 @@ class RustTimeline(
|
|||
},
|
||||
useSendQueue = useSendQueue,
|
||||
mentions = null,
|
||||
replyParams = replyParameters?.map(),
|
||||
),
|
||||
thumbnailPath = thumbnailFile?.path,
|
||||
imageInfo = imageInfo.map(),
|
||||
|
|
@ -382,6 +381,7 @@ class RustTimeline(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> {
|
||||
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
|
||||
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
|
||||
|
|
@ -394,6 +394,7 @@ class RustTimeline(
|
|||
},
|
||||
useSendQueue = useSendQueue,
|
||||
mentions = null,
|
||||
replyParams = replyParameters?.map(),
|
||||
),
|
||||
thumbnailPath = thumbnailFile?.path,
|
||||
videoInfo = videoInfo.map(),
|
||||
|
|
@ -408,6 +409,7 @@ class RustTimeline(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> {
|
||||
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
|
||||
return sendAttachment(listOf(file)) {
|
||||
|
|
@ -420,6 +422,7 @@ class RustTimeline(
|
|||
},
|
||||
useSendQueue = useSendQueue,
|
||||
mentions = null,
|
||||
replyParams = replyParameters?.map(),
|
||||
),
|
||||
audioInfo = audioInfo.map(),
|
||||
progressWatcher = progressCallback?.toProgressWatcher()
|
||||
|
|
@ -433,6 +436,7 @@ class RustTimeline(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> {
|
||||
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
|
||||
return sendAttachment(listOf(file)) {
|
||||
|
|
@ -445,6 +449,7 @@ class RustTimeline(
|
|||
},
|
||||
useSendQueue = useSendQueue,
|
||||
mentions = null,
|
||||
replyParams = replyParameters?.map(),
|
||||
),
|
||||
fileInfo = fileInfo.map(),
|
||||
progressWatcher = progressCallback?.toProgressWatcher(),
|
||||
|
|
@ -487,6 +492,32 @@ class RustTimeline(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> {
|
||||
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
|
||||
return sendAttachment(listOf(file)) {
|
||||
inner.sendVoiceMessage(
|
||||
params = UploadParameters(
|
||||
filename = file.path,
|
||||
// Maybe allow a caption in the future?
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
useSendQueue = useSendQueue,
|
||||
mentions = null,
|
||||
replyParams = replyParameters?.map(),
|
||||
),
|
||||
audioInfo = audioInfo.map(),
|
||||
waveform = waveform.toMSC3246range(),
|
||||
progressWatcher = progressCallback?.toProgressWatcher(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createPoll(
|
||||
question: String,
|
||||
answers: List<String>,
|
||||
|
|
@ -550,30 +581,6 @@ class RustTimeline(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?,
|
||||
): Result<MediaUploadHandler> {
|
||||
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
|
||||
return sendAttachment(listOf(file)) {
|
||||
inner.sendVoiceMessage(
|
||||
params = UploadParameters(
|
||||
filename = file.path,
|
||||
// Maybe allow a caption in the future?
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
useSendQueue = useSendQueue,
|
||||
mentions = null,
|
||||
),
|
||||
audioInfo = audioInfo.map(),
|
||||
waveform = waveform.toMSC3246range(),
|
||||
progressWatcher = progressCallback?.toProgressWatcher(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendAttachment(files: List<File>, handle: () -> SendAttachmentJoinHandle): Result<MediaUploadHandler> {
|
||||
return runCatching {
|
||||
MediaUploadHandlerImpl(files, handle())
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class VirtualTimelineItemMapper {
|
|||
return when (virtualTimelineItem) {
|
||||
is RustVirtualTimelineItem.DateDivider -> VirtualTimelineItem.DayDivider(virtualTimelineItem.ts.toLong())
|
||||
RustVirtualTimelineItem.ReadMarker -> VirtualTimelineItem.ReadMarker
|
||||
RustVirtualTimelineItem.TimelineStart -> VirtualTimelineItem.RoomBeginning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
|
|
@ -32,55 +30,59 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) {
|
|||
return when {
|
||||
items.isEmpty() -> items
|
||||
mode == Timeline.Mode.PINNED_EVENTS -> items
|
||||
isDm -> processForDM(items, roomCreator)
|
||||
isDm -> processForDM(items, roomCreator, hasMoreToLoadBackwards)
|
||||
hasMoreToLoadBackwards -> items
|
||||
else -> processForRoom(items)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processForRoom(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
|
||||
val roomBeginningItem = createRoomBeginningItem()
|
||||
return listOf(roomBeginningItem) + items
|
||||
// No changes needed, timeline start item is already added by the SDK
|
||||
return items
|
||||
}
|
||||
|
||||
private fun processForDM(items: List<MatrixTimelineItem>, roomCreator: UserId?): List<MatrixTimelineItem> {
|
||||
private fun processForDM(items: List<MatrixTimelineItem>, roomCreator: UserId?, hasMoreToLoadBackwards: Boolean): List<MatrixTimelineItem> {
|
||||
val roomBeginningItemIndex = if (!hasMoreToLoadBackwards) {
|
||||
items.indexOfFirst { it is MatrixTimelineItem.Virtual && it.virtual is VirtualTimelineItem.RoomBeginning }.takeIf { it >= 0 }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Find room creation event.
|
||||
// This is usually the first MatrixTimelineItem.Event (so index 1, index 0 is a date)
|
||||
val roomCreationEventIndex = items.indexOfFirst {
|
||||
val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? StateContent
|
||||
stateEventContent?.content is OtherState.RoomCreate
|
||||
}
|
||||
}.takeIf { it >= 0 }
|
||||
|
||||
// If the parameter roomCreator is null, the creator is the sender of the RoomCreate Event.
|
||||
val roomCreatorUserId = roomCreator ?: (items.getOrNull(roomCreationEventIndex) as? MatrixTimelineItem.Event)?.event?.sender
|
||||
val roomCreatorUserId = roomCreator ?: roomCreationEventIndex?.let {
|
||||
(items.getOrNull(it) as? MatrixTimelineItem.Event)?.event?.sender
|
||||
}
|
||||
// Find self-join event for the room creator.
|
||||
// This is usually the second MatrixTimelineItem.Event (so index 2)
|
||||
val selfUserJoinedEventIndex = roomCreatorUserId?.let { creatorUserId ->
|
||||
items.indexOfFirst {
|
||||
val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? RoomMembershipContent
|
||||
stateEventContent?.change == MembershipChange.JOINED && stateEventContent.userId == creatorUserId
|
||||
}
|
||||
} ?: -1
|
||||
}.takeIf { it >= 0 }
|
||||
}
|
||||
|
||||
if (roomCreationEventIndex == -1 && selfUserJoinedEventIndex == -1) {
|
||||
val indicesToRemove = listOfNotNull(
|
||||
roomBeginningItemIndex,
|
||||
roomCreationEventIndex,
|
||||
selfUserJoinedEventIndex,
|
||||
)
|
||||
if (indicesToRemove.isEmpty()) {
|
||||
// Nothing to do, return the list as is
|
||||
return items
|
||||
}
|
||||
|
||||
// Remove items at the indices we found
|
||||
val newItems = items.toMutableList()
|
||||
if (selfUserJoinedEventIndex in newItems.indices) {
|
||||
newItems.removeAt(selfUserJoinedEventIndex)
|
||||
}
|
||||
if (roomCreationEventIndex in newItems.indices) {
|
||||
newItems.removeAt(roomCreationEventIndex)
|
||||
indicesToRemove.sortedDescending().forEach { index ->
|
||||
newItems.removeAt(index)
|
||||
}
|
||||
return newItems
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun createRoomBeginningItem(): MatrixTimelineItem.Virtual {
|
||||
return MatrixTimelineItem.Virtual(
|
||||
uniqueId = UniqueId("RoomBeginning"),
|
||||
virtual = VirtualTimelineItem.RoomBeginning
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class DefaultCallWidgetSettingsProvider @Inject constructor(
|
|||
private val analyticsService: AnalyticsService,
|
||||
) : CallWidgetSettingsProvider {
|
||||
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings {
|
||||
val isAnalyticsEnabled = analyticsService.getUserConsent().first()
|
||||
val isAnalyticsEnabled = analyticsService.userConsentFlow.first()
|
||||
val options = VirtualElementCallWidgetOptions(
|
||||
elementCallUrl = baseUrl,
|
||||
widgetId = widgetId,
|
||||
|
|
|
|||
|
|
@ -10,12 +10,11 @@ package io.element.android.libraries.matrix.impl.auth
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.auth.OidcConfig
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
||||
class OidcConfigurationProviderTest {
|
||||
@Test
|
||||
fun get() {
|
||||
val result = OidcConfigurationProvider(File("/base")).get()
|
||||
val result = OidcConfigurationProvider().get()
|
||||
assertThat(result.redirectUri).isEqualTo(OidcConfig.REDIRECT_URI)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class RustMatrixAuthenticationServiceTest {
|
|||
sessionStore = sessionStore,
|
||||
rustMatrixClientFactory = rustMatrixClientFactory,
|
||||
passphraseGenerator = FakePassphraseGenerator(),
|
||||
oidcConfigurationProvider = OidcConfigurationProvider(baseDirectory),
|
||||
oidcConfigurationProvider = OidcConfigurationProvider(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class FakeRustClientBuilder : ClientBuilder(NoPointer) {
|
|||
override fun roomDecryptionTrustRequirement(trustRequirement: TrustRequirement) = this
|
||||
override fun disableSslVerification() = this
|
||||
override fun homeserverUrl(url: String) = this
|
||||
override fun passphrase(passphrase: String?) = this
|
||||
override fun sessionPassphrase(passphrase: String?) = this
|
||||
override fun proxy(url: String) = this
|
||||
override fun requestConfig(config: RequestConfig) = this
|
||||
override fun roomKeyRecipientStrategy(strategy: CollectStrategy) = this
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2
|
|||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
|
||||
internal val timelineStartEvent = MatrixTimelineItem.Virtual(
|
||||
uniqueId = UniqueId("timeline_start"),
|
||||
virtual = VirtualTimelineItem.RoomBeginning,
|
||||
)
|
||||
internal val roomCreateEvent = MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("m.room.create"),
|
||||
event = anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))
|
||||
|
|
|
|||
|
|
@ -50,8 +50,9 @@ class RoomBeginningPostProcessorTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `processor removes room creation event and self-join event from DM timeline`() {
|
||||
fun `processor removes timeline start, room creation event and self-join event from DM timeline`() {
|
||||
val timelineItems = listOf(
|
||||
timelineStartEvent,
|
||||
roomCreateEvent,
|
||||
roomCreatorJoinEvent,
|
||||
)
|
||||
|
|
@ -98,43 +99,6 @@ class RoomBeginningPostProcessorTest {
|
|||
assertThat(processedItems).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processor will add beginning of room item if it's not a DM`() {
|
||||
val timelineItems = listOf(
|
||||
roomCreateEvent,
|
||||
roomCreatorJoinEvent,
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = false, roomCreator = A_USER_ID, hasMoreToLoadBackwards = false)
|
||||
assertThat(processedItems).isEqualTo(
|
||||
listOf(processor.createRoomBeginningItem()) + timelineItems
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processor will not add beginning of room item if it's not a DM but the room has more to load`() {
|
||||
val timelineItems = listOf(
|
||||
roomCreateEvent,
|
||||
roomCreatorJoinEvent,
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = false, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processor will add beginning of room item if it's not a DM, when the parameter roomCreator is null`() {
|
||||
val timelineItems = listOf(
|
||||
roomCreateEvent,
|
||||
roomCreatorJoinEvent,
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = false, roomCreator = null, hasMoreToLoadBackwards = false)
|
||||
assertThat(processedItems).isEqualTo(
|
||||
listOf(processor.createRoomBeginningItem()) + timelineItems
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processor removes items event it's not at the start of the timeline`() {
|
||||
val timelineItems = listOf(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.test.notification
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
|
|
@ -19,6 +20,7 @@ fun aNotificationData(
|
|||
content: NotificationContent = NotificationContent.MessageLike.RoomEncrypted,
|
||||
isDirect: Boolean = false,
|
||||
hasMention: Boolean = false,
|
||||
threadId: ThreadId? = null,
|
||||
timestamp: Long = A_TIMESTAMP,
|
||||
senderDisplayName: String? = A_USER_NAME_2,
|
||||
senderIsNameAmbiguous: Boolean = false,
|
||||
|
|
@ -26,6 +28,7 @@ fun aNotificationData(
|
|||
): NotificationData {
|
||||
return NotificationData(
|
||||
eventId = AN_EVENT_ID,
|
||||
threadId = threadId,
|
||||
roomId = A_ROOM_ID,
|
||||
senderAvatarUrl = null,
|
||||
senderDisplayName = senderDisplayName,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibilit
|
|||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||
|
|
@ -87,16 +88,16 @@ class FakeMatrixRoom(
|
|||
private val canRedactOtherResult: (UserId) -> Result<Boolean> = { lambdaError() },
|
||||
private val canSendStateResult: (UserId, StateEventType) -> Result<Boolean> = { _, _ -> lambdaError() },
|
||||
private val canUserSendMessageResult: (UserId, MessageEventType) -> Result<Boolean> = { _, _ -> lambdaError() },
|
||||
private val sendImageResult: (File, File?, ImageInfo, String?, String?, ProgressCallback?) -> Result<FakeMediaUploadHandler> =
|
||||
private val sendImageResult: (File, File?, ImageInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
|
||||
{ _, _, _, _, _, _, _ -> lambdaError() },
|
||||
private val sendVideoResult: (File, File?, VideoInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
|
||||
{ _, _, _, _, _, _, _ -> lambdaError() },
|
||||
private val sendFileResult: (File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
|
||||
{ _, _, _, _, _, _ -> lambdaError() },
|
||||
private val sendVideoResult: (File, File?, VideoInfo, String?, String?, ProgressCallback?) -> Result<FakeMediaUploadHandler> =
|
||||
private val sendAudioResult: (File, AudioInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
|
||||
{ _, _, _, _, _, _ -> lambdaError() },
|
||||
private val sendFileResult: (File, FileInfo, String?, String?, ProgressCallback?) -> Result<FakeMediaUploadHandler> =
|
||||
private val sendVoiceMessageResult: (File, AudioInfo, List<Float>, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
|
||||
{ _, _, _, _, _ -> lambdaError() },
|
||||
private val sendAudioResult: (File, AudioInfo, String?, String?, ProgressCallback?) -> Result<FakeMediaUploadHandler> =
|
||||
{ _, _, _, _, _ -> lambdaError() },
|
||||
private val sendVoiceMessageResult: (File, AudioInfo, List<Float>, ProgressCallback?) -> Result<FakeMediaUploadHandler> =
|
||||
{ _, _, _, _ -> lambdaError() },
|
||||
private val setNameResult: (String) -> Result<Unit> = { lambdaError() },
|
||||
private val setTopicResult: (String) -> Result<Unit> = { lambdaError() },
|
||||
private val updateAvatarResult: (String, ByteArray) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
|
|
@ -332,7 +333,8 @@ class FakeMatrixRoom(
|
|||
imageInfo: ImageInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> = simulateLongTask {
|
||||
simulateSendMediaProgress(progressCallback)
|
||||
sendImageResult(
|
||||
|
|
@ -342,6 +344,7 @@ class FakeMatrixRoom(
|
|||
caption,
|
||||
formattedCaption,
|
||||
progressCallback,
|
||||
replyParameters,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -351,7 +354,8 @@ class FakeMatrixRoom(
|
|||
videoInfo: VideoInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> = simulateLongTask {
|
||||
simulateSendMediaProgress(progressCallback)
|
||||
sendVideoResult(
|
||||
|
|
@ -361,6 +365,7 @@ class FakeMatrixRoom(
|
|||
caption,
|
||||
formattedCaption,
|
||||
progressCallback,
|
||||
replyParameters,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -369,7 +374,8 @@ class FakeMatrixRoom(
|
|||
audioInfo: AudioInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> = simulateLongTask {
|
||||
simulateSendMediaProgress(progressCallback)
|
||||
sendAudioResult(
|
||||
|
|
@ -378,6 +384,7 @@ class FakeMatrixRoom(
|
|||
caption,
|
||||
formattedCaption,
|
||||
progressCallback,
|
||||
replyParameters,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -386,7 +393,8 @@ class FakeMatrixRoom(
|
|||
fileInfo: FileInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> = simulateLongTask {
|
||||
simulateSendMediaProgress(progressCallback)
|
||||
sendFileResult(
|
||||
|
|
@ -395,6 +403,40 @@ class FakeMatrixRoom(
|
|||
caption,
|
||||
formattedCaption,
|
||||
progressCallback,
|
||||
replyParameters,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> = simulateLongTask {
|
||||
simulateSendMediaProgress(progressCallback)
|
||||
sendVoiceMessageResult(
|
||||
file,
|
||||
audioInfo,
|
||||
waveform,
|
||||
progressCallback,
|
||||
replyParameters,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun sendLocation(
|
||||
body: String,
|
||||
geoUri: String,
|
||||
description: String?,
|
||||
zoomLevel: Int?,
|
||||
assetType: AssetType?,
|
||||
): Result<Unit> = simulateLongTask {
|
||||
return sendLocationResult(
|
||||
body,
|
||||
geoUri,
|
||||
description,
|
||||
zoomLevel,
|
||||
assetType,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -464,22 +506,6 @@ class FakeMatrixRoom(
|
|||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun sendLocation(
|
||||
body: String,
|
||||
geoUri: String,
|
||||
description: String?,
|
||||
zoomLevel: Int?,
|
||||
assetType: AssetType?,
|
||||
): Result<Unit> = simulateLongTask {
|
||||
return sendLocationResult(
|
||||
body,
|
||||
geoUri,
|
||||
description,
|
||||
zoomLevel,
|
||||
assetType,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun createPoll(
|
||||
question: String,
|
||||
answers: List<String>,
|
||||
|
|
@ -524,21 +550,6 @@ class FakeMatrixRoom(
|
|||
return endPollResult(pollStartId, text)
|
||||
}
|
||||
|
||||
override suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?
|
||||
): Result<MediaUploadHandler> = simulateLongTask {
|
||||
simulateSendMediaProgress(progressCallback)
|
||||
sendVoiceMessageResult(
|
||||
file,
|
||||
audioInfo,
|
||||
waveform,
|
||||
progressCallback,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun typingNotice(isTyping: Boolean): Result<Unit> {
|
||||
return typingNoticeResult(isTyping)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.test.sync
|
||||
|
||||
import io.element.android.libraries.core.coroutine.mapState
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -29,6 +30,8 @@ class FakeSyncService(
|
|||
|
||||
override val syncState: StateFlow<SyncState> = syncStateFlow
|
||||
|
||||
override val isOnline: StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
|
||||
|
||||
suspend fun emitSyncState(syncState: SyncState) {
|
||||
syncStateFlow.emit(syncState)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
|
|||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
|
|
@ -28,19 +29,18 @@ import io.element.android.tests.testutils.lambda.lambdaError
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.File
|
||||
|
||||
class FakeTimeline(
|
||||
private val name: String = "FakeTimeline",
|
||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = MutableStateFlow(emptyList()),
|
||||
private val backwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
|
||||
override val backwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
|
||||
Timeline.PaginationStatus(
|
||||
isPaginating = false,
|
||||
hasMoreToLoad = true
|
||||
)
|
||||
),
|
||||
private val forwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
|
||||
override val forwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
|
||||
Timeline.PaginationStatus(
|
||||
isPaginating = false,
|
||||
hasMoreToLoad = false
|
||||
|
|
@ -111,7 +111,7 @@ class FakeTimeline(
|
|||
)
|
||||
|
||||
var replyMessageLambda: (
|
||||
eventId: EventId,
|
||||
replyParameters: ReplyParameters,
|
||||
body: String,
|
||||
htmlBody: String?,
|
||||
intentionalMentions: List<IntentionalMention>,
|
||||
|
|
@ -121,13 +121,13 @@ class FakeTimeline(
|
|||
}
|
||||
|
||||
override suspend fun replyMessage(
|
||||
eventId: EventId,
|
||||
replyParameters: ReplyParameters,
|
||||
body: String,
|
||||
htmlBody: String?,
|
||||
intentionalMentions: List<IntentionalMention>,
|
||||
fromNotification: Boolean,
|
||||
): Result<Unit> = replyMessageLambda(
|
||||
eventId,
|
||||
replyParameters,
|
||||
body,
|
||||
htmlBody,
|
||||
intentionalMentions,
|
||||
|
|
@ -141,7 +141,8 @@ class FakeTimeline(
|
|||
body: String?,
|
||||
formattedBody: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
) -> Result<MediaUploadHandler> = { _, _, _, _, _, _ ->
|
||||
replyParameters: ReplyParameters?,
|
||||
) -> Result<MediaUploadHandler> = { _, _, _, _, _, _, _ ->
|
||||
Result.success(FakeMediaUploadHandler())
|
||||
}
|
||||
|
||||
|
|
@ -152,13 +153,15 @@ class FakeTimeline(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> = sendImageLambda(
|
||||
file,
|
||||
thumbnailFile,
|
||||
imageInfo,
|
||||
caption,
|
||||
formattedCaption,
|
||||
progressCallback
|
||||
progressCallback,
|
||||
replyParameters,
|
||||
)
|
||||
|
||||
var sendVideoLambda: (
|
||||
|
|
@ -168,7 +171,8 @@ class FakeTimeline(
|
|||
body: String?,
|
||||
formattedBody: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
) -> Result<MediaUploadHandler> = { _, _, _, _, _, _ ->
|
||||
replyParameters: ReplyParameters?,
|
||||
) -> Result<MediaUploadHandler> = { _, _, _, _, _, _, _ ->
|
||||
Result.success(FakeMediaUploadHandler())
|
||||
}
|
||||
|
||||
|
|
@ -179,13 +183,15 @@ class FakeTimeline(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> = sendVideoLambda(
|
||||
file,
|
||||
thumbnailFile,
|
||||
videoInfo,
|
||||
caption,
|
||||
formattedCaption,
|
||||
progressCallback
|
||||
progressCallback,
|
||||
replyParameters,
|
||||
)
|
||||
|
||||
var sendAudioLambda: (
|
||||
|
|
@ -194,7 +200,8 @@ class FakeTimeline(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
) -> Result<MediaUploadHandler> = { _, _, _, _, _ ->
|
||||
replyParameters: ReplyParameters?,
|
||||
) -> Result<MediaUploadHandler> = { _, _, _, _, _, _ ->
|
||||
Result.success(FakeMediaUploadHandler())
|
||||
}
|
||||
|
||||
|
|
@ -204,12 +211,14 @@ class FakeTimeline(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> = sendAudioLambda(
|
||||
file,
|
||||
audioInfo,
|
||||
caption,
|
||||
formattedCaption,
|
||||
progressCallback
|
||||
progressCallback,
|
||||
replyParameters,
|
||||
)
|
||||
|
||||
var sendFileLambda: (
|
||||
|
|
@ -218,7 +227,8 @@ class FakeTimeline(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
) -> Result<MediaUploadHandler> = { _, _, _, _, _ ->
|
||||
replyParameters: ReplyParameters?,
|
||||
) -> Result<MediaUploadHandler> = { _, _, _, _, _, _ ->
|
||||
Result.success(FakeMediaUploadHandler())
|
||||
}
|
||||
|
||||
|
|
@ -228,22 +238,39 @@ class FakeTimeline(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> = sendFileLambda(
|
||||
file,
|
||||
fileInfo,
|
||||
caption,
|
||||
formattedCaption,
|
||||
progressCallback
|
||||
progressCallback,
|
||||
replyParameters,
|
||||
)
|
||||
|
||||
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = toggleReactionLambda(
|
||||
emoji,
|
||||
eventOrTransactionId
|
||||
)
|
||||
var sendVoiceMessageLambda: (
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
) -> Result<MediaUploadHandler> = { _, _, _, _, _ ->
|
||||
Result.success(FakeMediaUploadHandler())
|
||||
}
|
||||
|
||||
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = forwardEventLambda(eventId, roomIds)
|
||||
override suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<MediaUploadHandler> = sendVoiceMessageLambda(
|
||||
file,
|
||||
audioInfo,
|
||||
waveform,
|
||||
progressCallback,
|
||||
replyParameters,
|
||||
)
|
||||
|
||||
var sendLocationLambda: (
|
||||
body: String,
|
||||
|
|
@ -269,6 +296,17 @@ class FakeTimeline(
|
|||
assetType
|
||||
)
|
||||
|
||||
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||
|
||||
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = toggleReactionLambda(
|
||||
emoji,
|
||||
eventOrTransactionId
|
||||
)
|
||||
|
||||
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||
|
||||
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = forwardEventLambda(eventId, roomIds)
|
||||
|
||||
var createPollLambda: (
|
||||
question: String,
|
||||
answers: List<String>,
|
||||
|
|
@ -338,27 +376,6 @@ class FakeTimeline(
|
|||
text: String,
|
||||
): Result<Unit> = endPollLambda(pollStartId, text)
|
||||
|
||||
var sendVoiceMessageLambda: (
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?,
|
||||
) -> Result<MediaUploadHandler> = { _, _, _, _ ->
|
||||
Result.success(FakeMediaUploadHandler())
|
||||
}
|
||||
|
||||
override suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Float>,
|
||||
progressCallback: ProgressCallback?,
|
||||
): Result<MediaUploadHandler> = sendVoiceMessageLambda(
|
||||
file,
|
||||
audioInfo,
|
||||
waveform,
|
||||
progressCallback
|
||||
)
|
||||
|
||||
var sendReadReceiptLambda: (
|
||||
eventId: EventId,
|
||||
receiptType: ReceiptType,
|
||||
|
|
@ -377,13 +394,6 @@ class FakeTimeline(
|
|||
|
||||
override suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> = paginateLambda(direction)
|
||||
|
||||
override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow<Timeline.PaginationStatus> {
|
||||
return when (direction) {
|
||||
Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus
|
||||
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus
|
||||
}
|
||||
}
|
||||
|
||||
var loadReplyDetailsLambda: (eventId: EventId) -> InReplyTo = {
|
||||
InReplyTo.NotLoaded(it)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import io.element.android.libraries.core.extensions.flatMapCatching
|
|||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -46,12 +47,14 @@ class MediaSender @Inject constructor(
|
|||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
progressCallback: ProgressCallback?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<Unit> {
|
||||
return room.sendMedia(
|
||||
uploadInfo = mediaUploadInfo,
|
||||
progressCallback = progressCallback,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption
|
||||
formattedCaption = formattedCaption,
|
||||
replyParameters = replyParameters,
|
||||
)
|
||||
.handleSendResult()
|
||||
}
|
||||
|
|
@ -61,7 +64,8 @@ class MediaSender @Inject constructor(
|
|||
mimeType: String,
|
||||
caption: String? = null,
|
||||
formattedCaption: String? = null,
|
||||
progressCallback: ProgressCallback? = null
|
||||
progressCallback: ProgressCallback? = null,
|
||||
replyParameters: ReplyParameters? = null,
|
||||
): Result<Unit> {
|
||||
val compressIfPossible = sessionPreferencesStore.doesCompressMedia().first()
|
||||
return preProcessor
|
||||
|
|
@ -76,7 +80,8 @@ class MediaSender @Inject constructor(
|
|||
uploadInfo = info,
|
||||
progressCallback = progressCallback,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption
|
||||
formattedCaption = formattedCaption,
|
||||
replyParameters = replyParameters,
|
||||
)
|
||||
}
|
||||
.handleSendResult()
|
||||
|
|
@ -86,7 +91,8 @@ class MediaSender @Inject constructor(
|
|||
uri: Uri,
|
||||
mimeType: String,
|
||||
waveForm: List<Float>,
|
||||
progressCallback: ProgressCallback? = null
|
||||
progressCallback: ProgressCallback? = null,
|
||||
replyParameters: ReplyParameters? = null,
|
||||
): Result<Unit> {
|
||||
return preProcessor
|
||||
.process(
|
||||
|
|
@ -106,7 +112,8 @@ class MediaSender @Inject constructor(
|
|||
uploadInfo = newInfo,
|
||||
progressCallback = progressCallback,
|
||||
caption = null,
|
||||
formattedCaption = null
|
||||
formattedCaption = null,
|
||||
replyParameters = replyParameters,
|
||||
)
|
||||
}
|
||||
.handleSendResult()
|
||||
|
|
@ -128,6 +135,7 @@ class MediaSender @Inject constructor(
|
|||
progressCallback: ProgressCallback?,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
replyParameters: ReplyParameters?,
|
||||
): Result<Unit> {
|
||||
val handler = when (uploadInfo) {
|
||||
is MediaUploadInfo.Image -> {
|
||||
|
|
@ -137,7 +145,8 @@ class MediaSender @Inject constructor(
|
|||
imageInfo = uploadInfo.imageInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
progressCallback = progressCallback
|
||||
progressCallback = progressCallback,
|
||||
replyParameters = replyParameters,
|
||||
)
|
||||
}
|
||||
is MediaUploadInfo.Video -> {
|
||||
|
|
@ -147,7 +156,8 @@ class MediaSender @Inject constructor(
|
|||
videoInfo = uploadInfo.videoInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
progressCallback = progressCallback
|
||||
progressCallback = progressCallback,
|
||||
replyParameters = replyParameters,
|
||||
)
|
||||
}
|
||||
is MediaUploadInfo.Audio -> {
|
||||
|
|
@ -156,7 +166,8 @@ class MediaSender @Inject constructor(
|
|||
audioInfo = uploadInfo.audioInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
progressCallback = progressCallback
|
||||
progressCallback = progressCallback,
|
||||
replyParameters = replyParameters,
|
||||
)
|
||||
}
|
||||
is MediaUploadInfo.VoiceMessage -> {
|
||||
|
|
@ -164,7 +175,8 @@ class MediaSender @Inject constructor(
|
|||
file = uploadInfo.file,
|
||||
audioInfo = uploadInfo.audioInfo,
|
||||
waveform = uploadInfo.waveform,
|
||||
progressCallback = progressCallback
|
||||
progressCallback = progressCallback,
|
||||
replyParameters = replyParameters,
|
||||
)
|
||||
}
|
||||
is MediaUploadInfo.AnyFile -> {
|
||||
|
|
@ -173,7 +185,8 @@ class MediaSender @Inject constructor(
|
|||
fileInfo = uploadInfo.fileInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
progressCallback = progressCallback
|
||||
progressCallback = progressCallback,
|
||||
replyParameters = replyParameters,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.ProgressCallback
|
|||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
|
||||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
|
|
@ -46,7 +47,7 @@ class MediaSenderTest {
|
|||
@Test
|
||||
fun `given an attachment when sending it the MatrixRoom will call sendMedia`() = runTest {
|
||||
val sendImageResult =
|
||||
lambdaRecorder<File, File?, ImageInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
|
||||
lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? ->
|
||||
Result.success(FakeMediaUploadHandler())
|
||||
}
|
||||
val room = FakeMatrixRoom(
|
||||
|
|
@ -74,8 +75,8 @@ class MediaSenderTest {
|
|||
@Test
|
||||
fun `given a failure in the media upload when sending the whole process fails`() = runTest {
|
||||
val sendImageResult =
|
||||
lambdaRecorder<File, File?, ImageInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
|
||||
Result.failure(Exception())
|
||||
lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? ->
|
||||
Result.failure<FakeMediaUploadHandler>(Exception())
|
||||
}
|
||||
val room = FakeMatrixRoom(
|
||||
sendImageResult = sendImageResult
|
||||
|
|
@ -91,7 +92,8 @@ class MediaSenderTest {
|
|||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `given a cancellation in the media upload when sending the job is cancelled`() = runTest(StandardTestDispatcher()) {
|
||||
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
||||
val sendFileResult =
|
||||
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
|
||||
Result.success(FakeMediaUploadHandler())
|
||||
}
|
||||
val room = FakeMatrixRoom(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
|
@ -56,13 +57,13 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
|
|||
|
||||
// To reset the store: ResetStore()
|
||||
|
||||
val isAlreadyDenied: Boolean by permissionsStore
|
||||
.isPermissionDenied(permission)
|
||||
.collectAsState(initial = false)
|
||||
val isAlreadyDenied: Boolean by remember {
|
||||
permissionsStore.isPermissionDenied(permission)
|
||||
}.collectAsState(initial = false)
|
||||
|
||||
val isAlreadyAsked: Boolean by permissionsStore
|
||||
.isPermissionAsked(permission)
|
||||
.collectAsState(initial = false)
|
||||
val isAlreadyAsked: Boolean by remember {
|
||||
permissionsStore.isPermissionAsked(permission)
|
||||
}.collectAsState(initial = false)
|
||||
|
||||
var permissionState: PermissionState? = null
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
senderId = content.senderId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
threadId = threadId,
|
||||
noisy = isNoisy,
|
||||
timestamp = this.timestamp,
|
||||
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.asEventId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.message.replyInThread
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||
|
|
@ -54,7 +54,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}")
|
||||
when (intent.action) {
|
||||
actionIds.smartReply -> if (roomId != null) {
|
||||
handleSmartReply(sessionId, roomId, threadId, intent)
|
||||
handleSmartReply(sessionId, roomId, eventId, threadId, intent)
|
||||
}
|
||||
actionIds.dismissRoom -> if (roomId != null) {
|
||||
notificationCleaner.clearMessagesForRoom(sessionId, roomId)
|
||||
|
|
@ -106,6 +106,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
|||
private fun handleSmartReply(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
replyToEventId: EventId?,
|
||||
threadId: ThreadId?,
|
||||
intent: Intent,
|
||||
) = appCoroutineScope.launch {
|
||||
|
|
@ -120,6 +121,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
|||
sendMatrixEvent(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
replyToEventId = replyToEventId,
|
||||
threadId = threadId,
|
||||
room = room,
|
||||
message = message,
|
||||
|
|
@ -131,6 +133,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
|||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
threadId: ThreadId?,
|
||||
replyToEventId: EventId?,
|
||||
room: MatrixRoom,
|
||||
message: String,
|
||||
) {
|
||||
|
|
@ -158,13 +161,13 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
|||
)
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent)
|
||||
|
||||
if (threadId != null) {
|
||||
if (threadId != null && replyToEventId != null) {
|
||||
room.liveTimeline.replyMessage(
|
||||
eventId = threadId.asEventId(),
|
||||
body = message,
|
||||
htmlBody = null,
|
||||
intentionalMentions = emptyList(),
|
||||
fromNotification = true,
|
||||
replyParameters = replyInThread(replyToEventId),
|
||||
)
|
||||
} else {
|
||||
room.liveTimeline.sendMessage(
|
||||
|
|
|
|||
|
|
@ -197,7 +197,8 @@ class DefaultNotificationCreator @Inject constructor(
|
|||
addAction(markAsReadActionFactory.create(roomInfo))
|
||||
// Quick reply
|
||||
if (!roomInfo.hasSmartReplyError) {
|
||||
addAction(quickReplyActionFactory.create(roomInfo, threadId))
|
||||
val latestEventId = events.lastOrNull()?.eventId
|
||||
addAction(quickReplyActionFactory.create(roomInfo, latestEventId, threadId))
|
||||
}
|
||||
if (openIntent != null) {
|
||||
setContentIntent(openIntent)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import androidx.core.app.RemoteInput
|
|||
import io.element.android.appconfig.NotificationConfig
|
||||
import io.element.android.libraries.androidutils.uri.createIgnoredUri
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
|
|
@ -33,11 +34,11 @@ class QuickReplyActionFactory @Inject constructor(
|
|||
private val stringProvider: StringProvider,
|
||||
private val clock: SystemClock,
|
||||
) {
|
||||
fun create(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): NotificationCompat.Action? {
|
||||
fun create(roomInfo: RoomEventGroupInfo, eventId: EventId?, threadId: ThreadId?): NotificationCompat.Action? {
|
||||
if (!NotificationConfig.SHOW_QUICK_REPLY_ACTION) return null
|
||||
val sessionId = roomInfo.sessionId
|
||||
val roomId = roomInfo.roomId
|
||||
val replyPendingIntent = buildQuickReplyIntent(sessionId, roomId, threadId)
|
||||
val replyPendingIntent = buildQuickReplyIntent(sessionId, roomId, eventId, threadId)
|
||||
val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
|
||||
.setLabel(stringProvider.getString(R.string.notification_room_action_quick_reply))
|
||||
.build()
|
||||
|
|
@ -63,6 +64,7 @@ class QuickReplyActionFactory @Inject constructor(
|
|||
private fun buildQuickReplyIntent(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
eventId: EventId?,
|
||||
threadId: ThreadId?,
|
||||
): PendingIntent {
|
||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java)
|
||||
|
|
@ -70,9 +72,8 @@ class QuickReplyActionFactory @Inject constructor(
|
|||
intent.data = createIgnoredUri("quickReply/$sessionId/$roomId" + threadId?.let { "/$it" }.orEmpty())
|
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value)
|
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId.value)
|
||||
threadId?.let {
|
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it.value)
|
||||
}
|
||||
eventId?.let { intent.putExtra(NotificationBroadcastReceiver.KEY_EVENT_ID, it.value) }
|
||||
threadId?.let { intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it.value) }
|
||||
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -38,10 +38,8 @@ class PushProvidersTest @Inject constructor(
|
|||
val result = sortedPushProvider.isNotEmpty()
|
||||
if (result) {
|
||||
delegate.updateState(
|
||||
description = stringProvider.getQuantityString(
|
||||
resId = R.plurals.troubleshoot_notifications_test_detect_push_provider_success,
|
||||
quantity = sortedPushProvider.size,
|
||||
sortedPushProvider.size,
|
||||
description = stringProvider.getString(
|
||||
resId = R.string.troubleshoot_notifications_test_detect_push_provider_success_2,
|
||||
sortedPushProvider.joinToString { it.name }
|
||||
),
|
||||
status = NotificationTroubleshootTestState.Status.Success
|
||||
|
|
|
|||
|
|
@ -58,13 +58,14 @@
|
|||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"No push providers selected."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"Current push provider: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_title">"Current push provider"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Ensure that the application has at least one push provider."</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"No push providers found."</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Ensure that the application supports at least one push provider."</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"No push provider support found."</string>
|
||||
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
|
||||
<item quantity="one">"Found %1$d push provider: %2$s"</item>
|
||||
<item quantity="other">"Found %1$d push providers: %2$s"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Detect push providers"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_success_2">"The application was built with support for: %1$s"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Push provider support"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_description">"Check that the application can display notification."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_failure">"The notification has not been clicked."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Cannot display the notification."</string>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.asEventId
|
||||
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
|
||||
import io.element.android.libraries.matrix.api.room.message.replyInThread
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
|
|
@ -330,7 +331,8 @@ class NotificationBroadcastReceiverHandlerTest {
|
|||
@Test
|
||||
fun `Test send reply`() = runTest {
|
||||
val sendMessage = lambdaRecorder<String, String?, List<IntentionalMention>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val replyMessage = lambdaRecorder<EventId, String, String?, List<IntentionalMention>, Boolean, Result<Unit>> { _, _, _, _, _ -> Result.success(Unit) }
|
||||
val replyMessage =
|
||||
lambdaRecorder<ReplyParameters, String, String?, List<IntentionalMention>, Boolean, Result<Unit>> { _, _, _, _, _ -> Result.success(Unit) }
|
||||
val liveTimeline = FakeTimeline().apply {
|
||||
sendMessageLambda = sendMessage
|
||||
replyMessageLambda = replyMessage
|
||||
|
|
@ -396,7 +398,8 @@ class NotificationBroadcastReceiverHandlerTest {
|
|||
@Test
|
||||
fun `Test send reply to thread`() = runTest {
|
||||
val sendMessage = lambdaRecorder<String, String?, List<IntentionalMention>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val replyMessage = lambdaRecorder<EventId, String, String?, List<IntentionalMention>, Boolean, Result<Unit>> { _, _, _, _, _ -> Result.success(Unit) }
|
||||
val replyMessage =
|
||||
lambdaRecorder<ReplyParameters, String, String?, List<IntentionalMention>, Boolean, Result<Unit>> { _, _, _, _, _ -> Result.success(Unit) }
|
||||
val liveTimeline = FakeTimeline().apply {
|
||||
sendMessageLambda = sendMessage
|
||||
replyMessageLambda = replyMessage
|
||||
|
|
@ -423,6 +426,7 @@ class NotificationBroadcastReceiverHandlerTest {
|
|||
createIntent(
|
||||
action = actionIds.smartReply,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
threadId = A_THREAD_ID,
|
||||
),
|
||||
)
|
||||
|
|
@ -433,7 +437,13 @@ class NotificationBroadcastReceiverHandlerTest {
|
|||
.isCalledOnce()
|
||||
replyMessage.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_THREAD_ID.asEventId()), value(A_MESSAGE), value(null), value(emptyList<IntentionalMention>()), value(true))
|
||||
.with(
|
||||
value(replyInThread(eventId = AN_EVENT_ID, explicitReply = false)),
|
||||
value(A_MESSAGE),
|
||||
value(null),
|
||||
value(emptyList<IntentionalMention>()),
|
||||
value(true)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createIntent(
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ class PushProvidersTestTest {
|
|||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
|
||||
assertThat(lastItem.description).contains("2")
|
||||
assertThat(lastItem.description).contains("foo")
|
||||
assertThat(lastItem.description).contains("bar")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import extension.setupAnvil
|
||||
import org.gradle.internal.extensions.stdlib.capitalized
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -52,15 +50,3 @@ sqldelight {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for KSP not picking up the generated files from SqlDelight
|
||||
androidComponents {
|
||||
onVariants(selector().all()) { variant ->
|
||||
afterEvaluate {
|
||||
val variantName = variant.name.capitalized()
|
||||
tasks.getByName<KotlinCompile>("ksp${variantName}Kotlin") {
|
||||
setSource(tasks.getByName("generate${variantName}SessionDatabaseInterface").outputs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ import io.element.android.libraries.core.extensions.orEmpty
|
|||
|
||||
@Stable
|
||||
class StableCharSequence(initialText: CharSequence = "") {
|
||||
private var value by mutableStateOf<SpannableString>(SpannableString(initialText))
|
||||
private var value by mutableStateOf<SpannableString>(SpannableString.valueOf(initialText))
|
||||
private var needsDisplaying by mutableStateOf(false)
|
||||
|
||||
fun update(newText: CharSequence?, needsDisplaying: Boolean) {
|
||||
value = SpannableString(newText.orEmpty())
|
||||
value = SpannableString.valueOf(newText.orEmpty())
|
||||
this.needsDisplaying = needsDisplaying
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package io.element.android.libraries.textcomposer.model
|
|||
|
||||
import android.os.Parcelable
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -118,8 +117,7 @@ class MarkdownTextEditorState(
|
|||
}
|
||||
|
||||
fun getMentions(): List<IntentionalMention> {
|
||||
val text = SpannableString(text.value())
|
||||
val mentionSpans = text.getMentionSpans()
|
||||
val mentionSpans = text.value().getMentionSpans()
|
||||
return mentionSpans.mapNotNull { mentionSpan ->
|
||||
when (mentionSpan.type) {
|
||||
is MentionType.User -> IntentionalMention.User(mentionSpan.type.userId)
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@
|
|||
<string name="action_deactivate">"Eemalda konto"</string>
|
||||
<string name="action_deactivate_account">"Eemalda konto kasutusest"</string>
|
||||
<string name="action_decline">"Keeldu"</string>
|
||||
<string name="action_decline_and_block">"Keeldu ja blokeeri"</string>
|
||||
<string name="action_delete_poll">"Kustuta küsitlus"</string>
|
||||
<string name="action_disable">"Lülita välja"</string>
|
||||
<string name="action_discard">"Loobu"</string>
|
||||
|
|
@ -105,8 +106,10 @@
|
|||
<string name="action_remove_message">"Eemalda sõnum"</string>
|
||||
<string name="action_reply">"Vasta"</string>
|
||||
<string name="action_reply_in_thread">"Vasta jutulõngas"</string>
|
||||
<string name="action_report">"Teata"</string>
|
||||
<string name="action_report_bug">"Teata veast"</string>
|
||||
<string name="action_report_content">"Teata sisust haldurile"</string>
|
||||
<string name="action_report_room">"Teata jututoast"</string>
|
||||
<string name="action_reset">"Lähtesta"</string>
|
||||
<string name="action_reset_identity">"Lähtesta oma identiteet"</string>
|
||||
<string name="action_retry">"Proovi uuesti"</string>
|
||||
|
|
@ -216,6 +219,7 @@ Põhjus: %1$s."</string>
|
|||
<string name="common_permalink">"Püsilink"</string>
|
||||
<string name="common_permission">"Õigus"</string>
|
||||
<string name="common_pinned">"Esiletõstetud"</string>
|
||||
<string name="common_please_check_internet_connection">"Palun kontrolli oma nutiseadme internetiühendust"</string>
|
||||
<string name="common_please_wait">"Palun oota…"</string>
|
||||
<string name="common_poll_end_confirmation">"Kas oled kindel, et soovid selle küsitluse lõpetada?"</string>
|
||||
<string name="common_poll_summary">"Küsitlus: %1$s"</string>
|
||||
|
|
@ -228,6 +232,7 @@ Põhjus: %1$s."</string>
|
|||
<string name="common_privacy_policy">"Privaatsuspoliitika"</string>
|
||||
<string name="common_reaction">"Reaktsioon"</string>
|
||||
<string name="common_reactions">"Reaktsioonid"</string>
|
||||
<string name="common_reason">"Põhjus"</string>
|
||||
<string name="common_recovery_key">"Taastevõti"</string>
|
||||
<string name="common_refreshing">"Värskendame andmeid…"</string>
|
||||
<string name="common_replying_to">"Vastates kasutajale %1$s"</string>
|
||||
|
|
@ -299,6 +304,8 @@ Põhjus: %1$s."</string>
|
|||
|
||||
Kas sa oled kindel, et soovid jätkata?"</string>
|
||||
<string name="dialog_confirm_link_title">"Palun kontrolli seda linki mõttega"</string>
|
||||
<string name="dialog_room_reported">"Teatasid jututoast"</string>
|
||||
<string name="dialog_room_reported_and_left">"Teatasid jututoast ja lahkusid sealt"</string>
|
||||
<string name="dialog_title_confirmation">"Kinnitus"</string>
|
||||
<string name="dialog_title_error">"Viga"</string>
|
||||
<string name="dialog_title_success">"Õnnestus"</string>
|
||||
|
|
@ -330,6 +337,10 @@ Kas sa oled kindel, et soovid jätkata?"</string>
|
|||
<string name="invite_friends_text">"Hei, suhtle minuga %1$s võrgus: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Veast teatamiseks raputa nutiseadet ägedalt"</string>
|
||||
<string name="screen_decline_and_block_block_user_option_description">"Sa ei näe enam selle kasutaja saadetud sõnumeid ja jututubade kutseid"</string>
|
||||
<string name="screen_decline_and_block_block_user_option_title">"Blokeeri kasutaja"</string>
|
||||
<string name="screen_decline_and_block_report_user_option_description">"Teata sellest jututoast oma teenusepakkujale."</string>
|
||||
<string name="screen_decline_and_block_title">"Keeldu ja blokeeri"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Meediafaili valimine ei õnnestunud. Palun proovi uuesti."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Selgitused ja alapealkirjad ei pruugi olla nähtavad vanemate rakenduste kasutajatele."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."</string>
|
||||
|
|
@ -341,6 +352,11 @@ Kas sa oled kindel, et soovid jätkata?"</string>
|
|||
<item quantity="other">"%1$d esiletõstetud sõnumit"</item>
|
||||
</plurals>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Esiletõstetud sõnumid"</string>
|
||||
<string name="screen_report_room_leave_failed_alert_message">"Jututoast haldajale teatamine õnnestus, kuid jututost lahkumisel tekkis viga. Palun proovi uuesti lahkuda."</string>
|
||||
<string name="screen_report_room_leave_failed_alert_title">"Pole võimalik lahkuda jututoast"</string>
|
||||
<string name="screen_report_room_reason_footer">"Teata sellest jututoast süsteemi haldajale. Kui sõnumid on krüptitud, ei saa haldaja neid lugeda."</string>
|
||||
<string name="screen_report_room_reason_placeholder">"Kirjelda põhjust…"</string>
|
||||
<string name="screen_report_room_title">"Teata jututoast"</string>
|
||||
<string name="screen_reset_identity_confirmation_subtitle">"Oma võrguidentiteedi lähtestamiseks suuname sind %1$s kasutajakonto halduse lehele. Hiljem suunatakse sind tagasi sama rakenduse juurde."</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"Sa ei saa seda kinnitada? Ava oma kasutajakonto haldus ja lähtesta oma võrguidentiteet."</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_primary_button_title">"Unusta verifitseerimine ja saada ikkagi"</string>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_avatar">"Profilkép"</string>
|
||||
<string name="a11y_delete">"Törlés"</string>
|
||||
<plurals name="a11y_digits_entered">
|
||||
<item quantity="one">"%1$d megadott számjegy"</item>
|
||||
<item quantity="other">"%1$d megadott számjegy"</item>
|
||||
</plurals>
|
||||
<string name="a11y_hide_password">"Jelszó elrejtése"</string>
|
||||
<string name="a11y_join_call">"Csatlakozás a híváshoz"</string>
|
||||
<string name="a11y_jump_to_bottom">"Ugrás az aljára"</string>
|
||||
<string name="a11y_notifications_mentions_only">"Csak megemlítések"</string>
|
||||
<string name="a11y_notifications_muted">"Némítva"</string>
|
||||
<string name="a11y_page_n">"%1$d. oldal"</string>
|
||||
<string name="a11y_pause">"Szüneteltetés"</string>
|
||||
<string name="a11y_paused_voice_message">"Hangüzenet, időtartam:%1$s, jelenlegi pozíció:%2$s"</string>
|
||||
<string name="a11y_pin_field">"PIN-mező"</string>
|
||||
<string name="a11y_play">"Lejátszás"</string>
|
||||
<string name="a11y_poll">"Szavazás"</string>
|
||||
|
|
@ -30,6 +33,7 @@
|
|||
<string name="a11y_start_call">"Hanghívás indítása"</string>
|
||||
<string name="a11y_user_menu">"Felhasználói menü"</string>
|
||||
<string name="a11y_view_details">"Részletek megtekintése"</string>
|
||||
<string name="a11y_voice_message">"Hangüzenet, időtartam: %1$s"</string>
|
||||
<string name="a11y_voice_message_record">"Hangüzenet felvétele."</string>
|
||||
<string name="a11y_voice_message_stop_recording">"Rögzítés leállítása"</string>
|
||||
<string name="action_accept">"Elfogadás"</string>
|
||||
|
|
@ -224,6 +228,7 @@ Ok: %1$s."</string>
|
|||
<string name="common_privacy_policy">"Adatvédelmi nyilatkozat"</string>
|
||||
<string name="common_reaction">"Reakció"</string>
|
||||
<string name="common_reactions">"Reakciók"</string>
|
||||
<string name="common_reason">"Ok"</string>
|
||||
<string name="common_recovery_key">"Helyreállítási kulcs"</string>
|
||||
<string name="common_refreshing">"Frissítés…"</string>
|
||||
<string name="common_replying_to">"Válasz %1$s számára"</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_avatar">"Obrázok"</string>
|
||||
<string name="a11y_delete">"Vymazať"</string>
|
||||
<plurals name="a11y_digits_entered">
|
||||
<item quantity="one">"%1$d zadaná číslica"</item>
|
||||
|
|
@ -7,11 +8,13 @@
|
|||
<item quantity="other">"%1$d zadaných číslic"</item>
|
||||
</plurals>
|
||||
<string name="a11y_hide_password">"Skryť heslo"</string>
|
||||
<string name="a11y_join_call">"Pripojiť sa k hovoru"</string>
|
||||
<string name="a11y_jump_to_bottom">"Prejsť na spodok"</string>
|
||||
<string name="a11y_notifications_mentions_only">"Iba zmienky"</string>
|
||||
<string name="a11y_notifications_muted">"Stlmené"</string>
|
||||
<string name="a11y_page_n">"Strana %1$d"</string>
|
||||
<string name="a11y_pause">"Pozastaviť"</string>
|
||||
<string name="a11y_paused_voice_message">"Hlasová správa, dĺžka:%1$s, aktuálna pozícia: %2$s"</string>
|
||||
<string name="a11y_pin_field">"Pole PIN"</string>
|
||||
<string name="a11y_play">"Prehrať"</string>
|
||||
<string name="a11y_poll">"Anketa"</string>
|
||||
|
|
@ -32,6 +35,7 @@
|
|||
<string name="a11y_start_call">"Začať hovor"</string>
|
||||
<string name="a11y_user_menu">"Používateľské menu"</string>
|
||||
<string name="a11y_view_details">"Zobraziť podrobnosti"</string>
|
||||
<string name="a11y_voice_message">"Hlasová správa, dĺžka: %1$s"</string>
|
||||
<string name="a11y_voice_message_record">"Nahrať hlasovú správu."</string>
|
||||
<string name="a11y_voice_message_stop_recording">"Zastaviť nahrávanie"</string>
|
||||
<string name="action_accept">"Prijať"</string>
|
||||
|
|
@ -61,6 +65,7 @@
|
|||
<string name="action_delete_poll">"Odstrániť anketu"</string>
|
||||
<string name="action_disable">"Vypnúť"</string>
|
||||
<string name="action_discard">"Zahodiť"</string>
|
||||
<string name="action_dismiss">"Zamietnuť"</string>
|
||||
<string name="action_done">"Hotovo"</string>
|
||||
<string name="action_edit">"Upraviť"</string>
|
||||
<string name="action_edit_caption">"Upraviť titulok"</string>
|
||||
|
|
@ -205,6 +210,7 @@ Dôvod: %1$s."</string>
|
|||
<string name="common_name_and_id">"%1$s (%2$s)"</string>
|
||||
<string name="common_no_results">"Žiadne výsledky"</string>
|
||||
<string name="common_no_room_name">"Žiadny názov miestnosti"</string>
|
||||
<string name="common_not_encrypted">"Nešifrované"</string>
|
||||
<string name="common_offline">"Offline"</string>
|
||||
<string name="common_open_source_licenses">"Licencie s otvoreným zdrojom"</string>
|
||||
<string name="common_or">"alebo"</string>
|
||||
|
|
@ -226,6 +232,7 @@ Dôvod: %1$s."</string>
|
|||
<string name="common_privacy_policy">"Zásady ochrany osobných údajov"</string>
|
||||
<string name="common_reaction">"Reakcia"</string>
|
||||
<string name="common_reactions">"Reakcie"</string>
|
||||
<string name="common_reason">"Dôvod"</string>
|
||||
<string name="common_recovery_key">"Kľúč na obnovenie"</string>
|
||||
<string name="common_refreshing">"Obnovuje sa…"</string>
|
||||
<string name="common_replying_to">"Odpoveď na %1$s"</string>
|
||||
|
|
@ -267,7 +274,7 @@ Dôvod: %1$s."</string>
|
|||
<string name="common_unable_to_decrypt">"Nie je možné dešifrovať"</string>
|
||||
<string name="common_unable_to_decrypt_insecure_device">"Odoslané z nezabezpečeného zariadenia"</string>
|
||||
<string name="common_unable_to_decrypt_no_access">"Nemáte prístup k tejto správe"</string>
|
||||
<string name="common_unable_to_decrypt_verification_violation">"Overená totožnosť odosielateľa sa zmenila"</string>
|
||||
<string name="common_unable_to_decrypt_verification_violation">"Overená totožnosť odosielateľa bola obnovená"</string>
|
||||
<string name="common_unable_to_invite_message">"Pozvánky nebolo možné odoslať jednému alebo viacerým používateľom."</string>
|
||||
<string name="common_unable_to_invite_title">"Nie je možné odoslať pozvánku/ky"</string>
|
||||
<string name="common_unlock">"Odomknúť"</string>
|
||||
|
|
@ -287,11 +294,11 @@ Dôvod: %1$s."</string>
|
|||
<string name="common_waiting">"Čaká sa…"</string>
|
||||
<string name="common_waiting_for_decryption_key">"Čaká sa na dešifrovací kľúč"</string>
|
||||
<string name="common_you">"Vy"</string>
|
||||
<string name="crypto_identity_change_pin_violation">"Zdá sa, že totožnosť používateľa %1$s sa zmenila.%2$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new">"Zdá sa, že identita %2$s používateľa %1$s sa zmenila. %3$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation">"Totožnosť používateľa %1$s sa obnovila.%2$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new">"Totožnosť používateľa %1$s %2$s bola obnovená. %3$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
|
||||
<string name="crypto_identity_change_profile_pin_violation">"Overená totožnosť používateľa %1$s sa zmenila."</string>
|
||||
<string name="crypto_identity_change_verification_violation_new">"Overená identita používateľa %1$s %2$s sa zmenila. %3$s"</string>
|
||||
<string name="crypto_identity_change_profile_pin_violation">"Totožnosť používateľa %1$s bola obnovená."</string>
|
||||
<string name="crypto_identity_change_verification_violation_new">"Totožnosť používateľa %1$s %2$s bola obnovená. %3$s"</string>
|
||||
<string name="crypto_identity_change_withdraw_verification_action">"Zrušiť overenie"</string>
|
||||
<string name="dialog_confirm_link_message">"Odkaz %1$s vás presmeruje na inú stránku %2$s
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_avatar">"Avatar"</string>
|
||||
<string name="a11y_delete">"Radera"</string>
|
||||
<plurals name="a11y_digits_entered">
|
||||
<item quantity="one">"%1$d siffra angiven"</item>
|
||||
<item quantity="other">"%1$d siffror angivna"</item>
|
||||
</plurals>
|
||||
<string name="a11y_hide_password">"Dölj lösenord"</string>
|
||||
<string name="a11y_join_call">"Anslut till samtal"</string>
|
||||
<string name="a11y_jump_to_bottom">"Hoppa till botten"</string>
|
||||
<string name="a11y_notifications_mentions_only">"Endast omnämningar"</string>
|
||||
<string name="a11y_notifications_muted">"Tystad"</string>
|
||||
<string name="a11y_page_n">"Sida %1$d"</string>
|
||||
<string name="a11y_pause">"Pausa"</string>
|
||||
<string name="a11y_paused_voice_message">"Röstmeddelande, varaktighet:%1$s, nuvarande position: %2$s"</string>
|
||||
<string name="a11y_pin_field">"PIN-fält"</string>
|
||||
<string name="a11y_play">"Spela upp"</string>
|
||||
<string name="a11y_poll">"Omröstning"</string>
|
||||
|
|
@ -30,6 +33,7 @@
|
|||
<string name="a11y_start_call">"Starta ett samtal"</string>
|
||||
<string name="a11y_user_menu">"Användarmeny"</string>
|
||||
<string name="a11y_view_details">"Visa detaljer"</string>
|
||||
<string name="a11y_voice_message">"Röstmeddelande, varaktighet: %1$s"</string>
|
||||
<string name="a11y_voice_message_record">"Spela in röstmeddelande."</string>
|
||||
<string name="a11y_voice_message_stop_recording">"Stoppa inspelning"</string>
|
||||
<string name="action_accept">"Godkänn"</string>
|
||||
|
|
@ -101,8 +105,10 @@
|
|||
<string name="action_remove_message">"Ta bort meddelande"</string>
|
||||
<string name="action_reply">"Svara"</string>
|
||||
<string name="action_reply_in_thread">"Svara i tråd"</string>
|
||||
<string name="action_report">"Anmäl"</string>
|
||||
<string name="action_report_bug">"Rapportera bugg"</string>
|
||||
<string name="action_report_content">"Rapportera innehåll"</string>
|
||||
<string name="action_report_room">"Anmäl rum"</string>
|
||||
<string name="action_reset">"Återställ"</string>
|
||||
<string name="action_reset_identity">"Återställ identitet"</string>
|
||||
<string name="action_retry">"Försök igen"</string>
|
||||
|
|
@ -224,6 +230,7 @@ Anledning:%1$s."</string>
|
|||
<string name="common_privacy_policy">"Integritetspolicy"</string>
|
||||
<string name="common_reaction">"Reaktion"</string>
|
||||
<string name="common_reactions">"Reaktioner"</string>
|
||||
<string name="common_reason">"Orsak"</string>
|
||||
<string name="common_recovery_key">"Återställningsnyckel"</string>
|
||||
<string name="common_refreshing">"Uppdaterar …"</string>
|
||||
<string name="common_replying_to">"Svarar till %1$s"</string>
|
||||
|
|
@ -295,6 +302,8 @@ Anledning:%1$s."</string>
|
|||
|
||||
Är du säker på att du vill fortsätta?"</string>
|
||||
<string name="dialog_confirm_link_title">"Dubbelkolla den här länken"</string>
|
||||
<string name="dialog_room_reported">"Rum anmält"</string>
|
||||
<string name="dialog_room_reported_and_left">"Anmälde och lämnade rummet"</string>
|
||||
<string name="dialog_title_confirmation">"Bekräftelse"</string>
|
||||
<string name="dialog_title_error">"Fel"</string>
|
||||
<string name="dialog_title_success">"Lyckades"</string>
|
||||
|
|
@ -337,6 +346,10 @@ Anledning:%1$s."</string>
|
|||
<item quantity="other">"%1$d Fästa meddelanden"</item>
|
||||
</plurals>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Fästa meddelanden"</string>
|
||||
<string name="screen_report_room_leave_failed_alert_title">"Kunde inte lämna rummet"</string>
|
||||
<string name="screen_report_room_reason_footer">"Anmäl det här rummet till din administratör. Om meddelandena är krypterade kommer din administratör inte att kunna läsa dem."</string>
|
||||
<string name="screen_report_room_reason_placeholder">"Beskriv anledningen …"</string>
|
||||
<string name="screen_report_room_title">"Rapportera rum"</string>
|
||||
<string name="screen_reset_identity_confirmation_subtitle">"Du är på väg att gå till ditt %1$s-konto för att återställa din identitet. Därefter kommer du att tas tillbaka till appen."</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"Kan du inte bekräfta? Gå till ditt konto för att återställa din identitet."</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_primary_button_title">"Dra tillbaka verifieringen och skicka"</string>
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@
|
|||
<string name="action_deactivate">"Deactivate"</string>
|
||||
<string name="action_deactivate_account">"Deactivate account"</string>
|
||||
<string name="action_decline">"Decline"</string>
|
||||
<string name="action_decline_and_block">"Decline and block"</string>
|
||||
<string name="action_delete_poll">"Delete Poll"</string>
|
||||
<string name="action_disable">"Disable"</string>
|
||||
<string name="action_discard">"Discard"</string>
|
||||
|
|
@ -105,8 +106,10 @@
|
|||
<string name="action_remove_message">"Remove message"</string>
|
||||
<string name="action_reply">"Reply"</string>
|
||||
<string name="action_reply_in_thread">"Reply in thread"</string>
|
||||
<string name="action_report">"Report"</string>
|
||||
<string name="action_report_bug">"Report bug"</string>
|
||||
<string name="action_report_content">"Report content"</string>
|
||||
<string name="action_report_room">"Report room"</string>
|
||||
<string name="action_reset">"Reset"</string>
|
||||
<string name="action_reset_identity">"Reset identity"</string>
|
||||
<string name="action_retry">"Retry"</string>
|
||||
|
|
@ -216,6 +219,7 @@ Reason: %1$s."</string>
|
|||
<string name="common_permalink">"Permalink"</string>
|
||||
<string name="common_permission">"Permission"</string>
|
||||
<string name="common_pinned">"Pinned"</string>
|
||||
<string name="common_please_check_internet_connection">"Please check your internet connection"</string>
|
||||
<string name="common_please_wait">"Please wait…"</string>
|
||||
<string name="common_poll_end_confirmation">"Are you sure you want to end this poll?"</string>
|
||||
<string name="common_poll_summary">"Poll: %1$s"</string>
|
||||
|
|
@ -300,6 +304,8 @@ Reason: %1$s."</string>
|
|||
|
||||
Are you sure you want to continue?"</string>
|
||||
<string name="dialog_confirm_link_title">"Double-check this link"</string>
|
||||
<string name="dialog_room_reported">"Room reported"</string>
|
||||
<string name="dialog_room_reported_and_left">"Reported and left room"</string>
|
||||
<string name="dialog_title_confirmation">"Confirmation"</string>
|
||||
<string name="dialog_title_error">"Error"</string>
|
||||
<string name="dialog_title_success">"Success"</string>
|
||||
|
|
@ -331,6 +337,10 @@ Are you sure you want to continue?"</string>
|
|||
<string name="invite_friends_text">"Hey, talk to me on %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake to report bug"</string>
|
||||
<string name="screen_decline_and_block_block_user_option_description">"You will not see any messages or room invites from this user"</string>
|
||||
<string name="screen_decline_and_block_block_user_option_title">"Block user"</string>
|
||||
<string name="screen_decline_and_block_report_user_option_description">"Report this room to your account provider."</string>
|
||||
<string name="screen_decline_and_block_title">"Decline and block"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Captions might not be visible to people using older apps."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
||||
|
|
@ -342,6 +352,11 @@ Are you sure you want to continue?"</string>
|
|||
<item quantity="other">"%1$d Pinned messages"</item>
|
||||
</plurals>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Pinned messages"</string>
|
||||
<string name="screen_report_room_leave_failed_alert_message">"Your report was submitted successfully, but we encountered an issue while trying to leave the room. Please try again."</string>
|
||||
<string name="screen_report_room_leave_failed_alert_title">"Unable to Leave Room"</string>
|
||||
<string name="screen_report_room_reason_footer">"Report this room to your admin. If the messages are encrypted, your admin will not be able to read them."</string>
|
||||
<string name="screen_report_room_reason_placeholder">"Describe the reason…"</string>
|
||||
<string name="screen_report_room_title">"Report room"</string>
|
||||
<string name="screen_reset_identity_confirmation_subtitle">"You\'re about to go to your %1$s account to reset your identity. Afterwards you\'ll be taken back to the app."</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"Can\'t confirm? Go to your account to reset your identity."</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_primary_button_title">"Withdraw verification and send"</string>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.ui.utils.time
|
||||
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.key
|
||||
|
||||
/**
|
||||
* Extension property to get the digit character from a KeyEvent.
|
||||
* This handles both regular digit keys and numpad keys.
|
||||
*/
|
||||
val KeyEvent.digit: Char? get() {
|
||||
val char = nativeKeyEvent.unicodeChar.toChar()
|
||||
return when {
|
||||
Character.isDigit(char) -> char
|
||||
key == Key.NumPad0 -> '0'
|
||||
key == Key.NumPad1 -> '1'
|
||||
key == Key.NumPad2 -> '2'
|
||||
key == Key.NumPad3 -> '3'
|
||||
key == Key.NumPad4 -> '4'
|
||||
key == Key.NumPad5 -> '5'
|
||||
key == Key.NumPad6 -> '6'
|
||||
key == Key.NumPad7 -> '7'
|
||||
key == Key.NumPad8 -> '8'
|
||||
key == Key.NumPad9 -> '9'
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue