Merge branch 'develop' into renovate/io.nlopez.compose.rules-detekt-0.x

This commit is contained in:
Benoit Marty 2024-05-28 08:59:36 +02:00 committed by GitHub
commit 683f7d4748
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
587 changed files with 9299 additions and 2438 deletions

View file

@ -28,6 +28,7 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.RequiresApi
import androidx.core.content.pm.PackageInfoCompat
import io.element.android.libraries.androidutils.R
import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat
import io.element.android.libraries.core.mimetype.MimeTypes
@ -47,6 +48,19 @@ fun Context.getApplicationLabel(packageName: String): String {
}
}
/**
* Retrieve the versionCode from the Manifest.
* The value is more accurate than BuildConfig.VERSION_CODE, as it is correct according to the
* computation in the `androidComponents` block of the app build.gradle.kts file.
* In other words, the last digit (for the architecture) will be set, whereas BuildConfig.VERSION_CODE
* last digit will always be 0.
*/
fun Context.getVersionCodeFromManifest(): Long {
return PackageInfoCompat.getLongVersionCode(
packageManager.getPackageInfo(packageName, 0)
)
}
// ==============================================================================================================
// Clipboard helper
// ==============================================================================================================

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="error_no_compatible_app_found">"თავსებადი აპლიკაცია ვერ მოიძებნა ამ მოქმედების შესასრულებლად."</string>
</resources>

View file

@ -86,6 +86,8 @@ sealed interface AsyncAction<out T> {
fun isFailure(): Boolean = this is Failure
fun isSuccess(): Boolean = this is Success
fun isReady() = isSuccess() || isFailure()
}
suspend inline fun <T> MutableState<AsyncAction<T>>.runCatchingUpdatingState(

View file

@ -25,7 +25,7 @@ data class BuildMeta(
val applicationId: String,
val lowPrivacyLoggingEnabled: Boolean,
val versionName: String,
val versionCode: Int,
val versionCode: Long,
val gitRevision: String,
val gitBranchName: String,
val flavorDescription: String,

View file

@ -19,22 +19,19 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
@Composable
fun PreferenceCategory(
modifier: Modifier = Modifier,
title: String? = null,
showDivider: Boolean = true,
showTopDivider: Boolean = true,
content: @Composable ColumnScope.() -> Unit,
) {
Column(
@ -42,30 +39,17 @@ fun PreferenceCategory(
.fillMaxWidth()
) {
if (title != null) {
PreferenceCategoryTitle(title = title)
}
content()
if (showDivider) {
ListSectionHeader(
title = title,
hasDivider = showTopDivider,
)
} else if (showTopDivider) {
PreferenceDivider()
}
content()
}
}
@Composable
private fun PreferenceCategoryTitle(title: String) {
Text(
modifier = Modifier.padding(
top = 20.dp,
bottom = 8.dp,
start = preferencePaddingHorizontal,
end = preferencePaddingHorizontal,
),
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.materialColors.primary,
text = title,
)
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceCategoryPreview() = ElementThemedPreview {

View file

@ -17,25 +17,18 @@
package io.element.android.libraries.designsystem.components.preferences
import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.components.preferenceIcon
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Checkbox
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
@ -52,45 +45,36 @@ fun PreferenceCheckbox(
@DrawableRes iconResourceId: Int? = null,
showIconAreaIfNoIcon: Boolean = false,
) {
Row(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight)
.clickable { onCheckedChange(!isChecked) }
.padding(vertical = 4.dp, horizontal = preferencePaddingHorizontal),
verticalAlignment = Alignment.CenterVertically
) {
PreferenceIcon(
ListItem(
modifier = modifier,
onClick = onCheckedChange.takeIf { enabled }?.let { { onCheckedChange(!isChecked) } },
leadingContent = preferenceIcon(
icon = icon,
iconResourceId = iconResourceId,
enabled = enabled,
isVisible = showIconAreaIfNoIcon
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
showIconAreaIfNoIcon = showIconAreaIfNoIcon,
),
headlineContent = {
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = enabled.toEnabledColor(),
)
if (supportingText != null) {
},
supportingContent = supportingText?.let {
{
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = supportingText,
text = it,
color = enabled.toSecondaryEnabledColor(),
)
}
}
Checkbox(
modifier = Modifier
.align(Alignment.CenterVertically),
},
trailingContent = ListItemContent.Checkbox(
checked = isChecked,
enabled = enabled,
onCheckedChange = onCheckedChange
)
}
),
)
}
@Preview(group = PreviewGroup.Preferences)
@ -112,5 +96,31 @@ internal fun PreferenceCheckboxPreview() = ElementThemedPreview {
isChecked = true,
onCheckedChange = {},
)
PreferenceCheckbox(
title = "Checkbox with supporting text",
supportingText = "Supporting text",
iconResourceId = CompoundDrawables.ic_compound_threads,
enabled = false,
isChecked = true,
onCheckedChange = {},
)
PreferenceCheckbox(
title = "Checkbox with supporting text",
supportingText = "Supporting text",
iconResourceId = null,
showIconAreaIfNoIcon = true,
enabled = true,
isChecked = true,
onCheckedChange = {},
)
PreferenceCheckbox(
title = "Checkbox with supporting text",
supportingText = "Supporting text",
iconResourceId = null,
showIconAreaIfNoIcon = false,
enabled = true,
isChecked = true,
onCheckedChange = {},
)
}
}

View file

@ -19,7 +19,6 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
@ -28,10 +27,7 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalDivi
fun PreferenceDivider(
modifier: Modifier = Modifier,
) {
HorizontalDivider(
modifier = modifier,
color = ElementTheme.colors.borderDisabled,
)
HorizontalDivider(modifier = modifier)
}
@Preview(group = PreviewGroup.Preferences)

View file

@ -19,14 +19,13 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
/**
@ -37,15 +36,17 @@ fun PreferenceRow(
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit,
) {
Row(
modifier = modifier
.padding(horizontal = preferencePaddingHorizontal)
.heightIn(min = preferenceMinHeight)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
content()
}
ListItem(
modifier = modifier,
headlineContent = {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
content()
}
}
)
}
@Preview(group = PreviewGroup.Preferences)

View file

@ -19,23 +19,18 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.annotation.DrawableRes
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.components.preferences.components.preferenceIcon
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Slider
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
@Composable
fun PreferenceSlide(
@ -51,51 +46,57 @@ fun PreferenceSlide(
summary: String? = null,
steps: Int = 0,
) {
Row(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight)
.padding(vertical = 4.dp, horizontal = preferencePaddingHorizontal),
) {
PreferenceIcon(
ListItem(
modifier = modifier,
enabled = enabled,
leadingContent = preferenceIcon(
icon = icon,
iconResourceId = iconResourceId,
isVisible = showIconAreaIfNoIcon,
)
Column(
modifier = Modifier
.weight(1f),
) {
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = enabled.toEnabledColor(),
)
summary?.let {
enabled = enabled,
showIconAreaIfNoIcon = showIconAreaIfNoIcon,
),
headlineContent = {
Column {
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = summary,
color = enabled.toEnabledColor(),
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
)
summary?.let {
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = summary,
)
}
Slider(
value = value,
steps = steps,
onValueChange = onValueChange,
enabled = enabled,
)
}
Slider(
value = value,
steps = steps,
onValueChange = onValueChange,
enabled = enabled,
)
}
}
)
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceSlidePreview() = ElementThemedPreview {
PreferenceSlide(
icon = CompoundIcons.UserProfile(),
title = "Slide",
summary = "Summary",
value = 0.75F,
onValueChange = {},
)
Column {
PreferenceSlide(
icon = CompoundIcons.UserProfile(),
title = "Slide",
summary = "Summary",
enabled = true,
value = 0.75F,
onValueChange = {},
)
PreferenceSlide(
icon = CompoundIcons.UserProfile(),
title = "Slide",
summary = "Summary",
enabled = false,
value = 0.75F,
onValueChange = {},
)
}
}

View file

@ -17,30 +17,19 @@
package io.element.android.libraries.designsystem.components.preferences
import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.components.preferenceIcon
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Switch
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
@Composable
fun PreferenceSwitch(
@ -53,62 +42,65 @@ fun PreferenceSwitch(
icon: ImageVector? = null,
@DrawableRes iconResourceId: Int? = null,
showIconAreaIfNoIcon: Boolean = false,
switchAlignment: Alignment.Vertical = Alignment.CenterVertically
) {
Row(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight)
.clickable { onCheckedChange(!isChecked) }
.padding(vertical = 4.dp, horizontal = preferencePaddingHorizontal),
verticalAlignment = Alignment.CenterVertically
) {
PreferenceIcon(
ListItem(
modifier = modifier,
enabled = enabled,
onClick = onCheckedChange.takeIf { enabled }?.let { { onCheckedChange(!isChecked) } },
leadingContent = preferenceIcon(
icon = icon,
iconResourceId = iconResourceId,
enabled = enabled,
isVisible = showIconAreaIfNoIcon
)
Column(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
) {
showIconAreaIfNoIcon = showIconAreaIfNoIcon,
),
headlineContent = {
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = enabled.toEnabledColor(),
)
if (subtitle != null) {
Spacer(modifier = Modifier.height(4.dp))
},
supportingContent = subtitle?.let {
{
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = subtitle,
color = enabled.toSecondaryEnabledColor(),
)
}
}
Spacer(modifier = Modifier.width(16.dp))
// TODO Create a wrapper for Switch
Switch(
modifier = Modifier
.align(switchAlignment),
},
trailingContent = ListItemContent.Switch(
checked = isChecked,
enabled = enabled,
onCheckedChange = onCheckedChange
)
}
)
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceSwitchPreview() = ElementThemedPreview {
PreferenceSwitch(
title = "Switch",
subtitle = "Subtitle Switch",
icon = CompoundIcons.Threads(),
enabled = true,
isChecked = true,
onCheckedChange = {},
)
Column {
PreferenceSwitch(
title = "Switch",
subtitle = "Subtitle Switch",
icon = CompoundIcons.Threads(),
enabled = true,
isChecked = true,
onCheckedChange = {},
)
PreferenceSwitch(
title = "Switch",
subtitle = "Subtitle Switch",
icon = CompoundIcons.Threads(),
enabled = false,
isChecked = true,
onCheckedChange = {},
)
PreferenceSwitch(
title = "Switch no subtitle",
subtitle = null,
icon = CompoundIcons.Threads(),
enabled = false,
isChecked = true,
onCheckedChange = {},
)
}
}

View file

@ -17,12 +17,9 @@
package io.element.android.libraries.designsystem.components.preferences
import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
@ -38,18 +35,17 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.components.preferenceIcon
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
/**
* Tried to use ListItem, but it cannot really match the design. Keep custom Layout for now.
*/
@Composable
fun PreferenceText(
title: String,
@ -67,76 +63,76 @@ fun PreferenceText(
tintColor: Color? = null,
onClick: () -> Unit = {},
) {
val minHeight = if (subtitle == null && subtitleAnnotated == null) preferenceMinHeightOnlyTitle else preferenceMinHeight
Row(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = minHeight)
.clickable { onClick() }
.padding(horizontal = preferencePaddingHorizontal, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
PreferenceIcon(
ListItem(
modifier = modifier,
enabled = enabled,
onClick = onClick,
leadingContent = preferenceIcon(
icon = icon,
iconResourceId = iconResourceId,
showIconBadge = showIconBadge,
enabled = enabled,
isVisible = showIconAreaIfNoIcon,
tintColor = tintColor ?: enabled.toSecondaryEnabledColor(),
)
Column(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
) {
showIconAreaIfNoIcon = showIconAreaIfNoIcon,
tintColor = tintColor,
),
headlineContent = {
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = tintColor ?: enabled.toEnabledColor(),
)
if (subtitle != null) {
},
supportingContent = if (subtitle != null) {
{
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = subtitle,
color = tintColor ?: enabled.toSecondaryEnabledColor(),
)
} else if (subtitleAnnotated != null) {
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = subtitleAnnotated,
color = tintColor ?: enabled.toSecondaryEnabledColor(),
)
}
} else {
subtitleAnnotated?.let {
{
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = it,
color = tintColor ?: enabled.toSecondaryEnabledColor(),
)
}
}
},
trailingContent = if (currentValue != null || loadingCurrentValue || showEndBadge) {
ListItemContent.Custom {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
if (currentValue != null) {
Text(
text = currentValue,
style = ElementTheme.typography.fontBodyXsMedium,
color = enabled.toSecondaryEnabledColor(),
)
} else if (loadingCurrentValue) {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.size(20.dp),
strokeWidth = 2.dp
)
}
if (showEndBadge) {
val endBadgeStartPadding = if (currentValue != null || loadingCurrentValue) 16.dp else 0.dp
RedIndicatorAtom(
modifier = Modifier
.padding(start = endBadgeStartPadding)
)
}
}
}
} else {
null
}
if (currentValue != null) {
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 16.dp, end = 8.dp),
text = currentValue,
style = ElementTheme.typography.fontBodyXsMedium,
color = enabled.toSecondaryEnabledColor(),
)
} else if (loadingCurrentValue) {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.padding(start = 16.dp, end = 8.dp)
.size(20.dp)
.align(Alignment.CenterVertically),
strokeWidth = 2.dp
)
}
if (showEndBadge) {
val endBadgeStartPadding = if (currentValue != null || loadingCurrentValue) 8.dp else 16.dp
RedIndicatorAtom(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = endBadgeStartPadding)
)
}
}
)
}
@Preview(group = PreviewGroup.Preferences)

View file

@ -19,7 +19,6 @@ package io.element.android.libraries.designsystem.components.preferences.compone
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
@ -31,13 +30,39 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
@Composable
fun PreferenceIcon(
fun preferenceIcon(
icon: ImageVector? = null,
@DrawableRes iconResourceId: Int? = null,
showIconBadge: Boolean = false,
tintColor: Color? = null,
enabled: Boolean = true,
showIconAreaIfNoIcon: Boolean = false,
): ListItemContent.Custom? {
return if (icon != null || iconResourceId != null || showIconAreaIfNoIcon) {
ListItemContent.Custom {
PreferenceIcon(
icon = icon,
iconResourceId = iconResourceId,
showIconBadge = showIconBadge,
enabled = enabled,
isVisible = showIconAreaIfNoIcon,
tintColor = tintColor,
)
}
} else {
null
}
}
@Composable
private fun PreferenceIcon(
modifier: Modifier = Modifier,
icon: ImageVector? = null,
@DrawableRes iconResourceId: Int? = null,
@ -54,19 +79,17 @@ fun PreferenceIcon(
contentDescription = null,
tint = tintColor ?: enabled.toSecondaryEnabledColor(),
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp),
)
if (showIconBadge) {
RedIndicatorAtom(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(end = 16.dp)
)
}
}
} else if (isVisible) {
Spacer(modifier = modifier.width(40.dp))
Spacer(modifier = modifier.width(24.dp))
}
}

View file

@ -34,6 +34,7 @@ class RoomMembershipContentFormatter @Inject constructor(
): CharSequence? {
val userId = membershipContent.userId
val memberIsYou = matrixClient.isMe(userId)
val userDisplayNameOrId = membershipContent.userDisplayName ?: userId.value
return when (membershipContent.change) {
MembershipChange.JOINED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_join_by_you)
@ -46,41 +47,41 @@ class RoomMembershipContentFormatter @Inject constructor(
sp.getString(R.string.state_event_room_leave, senderDisambiguatedDisplayName)
}
MembershipChange.BANNED, MembershipChange.KICKED_AND_BANNED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_ban_by_you, userId.value)
sp.getString(R.string.state_event_room_ban_by_you, userDisplayNameOrId)
} else {
sp.getString(R.string.state_event_room_ban, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_ban, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.UNBANNED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_unban_by_you, userId.value)
sp.getString(R.string.state_event_room_unban_by_you, userDisplayNameOrId)
} else {
sp.getString(R.string.state_event_room_unban, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_unban, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.KICKED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_remove_by_you, userId.value)
sp.getString(R.string.state_event_room_remove_by_you, userDisplayNameOrId)
} else {
sp.getString(R.string.state_event_room_remove, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_remove, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.INVITED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_invite_by_you, userId.value)
sp.getString(R.string.state_event_room_invite_by_you, userDisplayNameOrId)
} else if (memberIsYou) {
sp.getString(R.string.state_event_room_invite_you, senderDisambiguatedDisplayName)
} else {
sp.getString(R.string.state_event_room_invite, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_invite, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.INVITATION_ACCEPTED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_invite_accepted_by_you)
} else {
sp.getString(R.string.state_event_room_invite_accepted, userId.value)
sp.getString(R.string.state_event_room_invite_accepted, userDisplayNameOrId)
}
MembershipChange.INVITATION_REJECTED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_reject_by_you)
} else {
sp.getString(R.string.state_event_room_reject, userId.value)
sp.getString(R.string.state_event_room_reject, userDisplayNameOrId)
}
MembershipChange.INVITATION_REVOKED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userId.value)
sp.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userDisplayNameOrId)
} else {
sp.getString(R.string.state_event_room_third_party_revoked_invite, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_third_party_revoked_invite, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.KNOCKED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_knock_by_you)
@ -88,9 +89,9 @@ class RoomMembershipContentFormatter @Inject constructor(
sp.getString(R.string.state_event_room_knock, senderDisambiguatedDisplayName)
}
MembershipChange.KNOCK_ACCEPTED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_knock_accepted_by_you, userId.value)
sp.getString(R.string.state_event_room_knock_accepted_by_you, userDisplayNameOrId)
} else {
sp.getString(R.string.state_event_room_knock_accepted, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_knock_accepted, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.KNOCK_RETRACTED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_knock_retracted_by_you)
@ -98,11 +99,11 @@ class RoomMembershipContentFormatter @Inject constructor(
sp.getString(R.string.state_event_room_knock_retracted, senderDisambiguatedDisplayName)
}
MembershipChange.KNOCK_DENIED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_knock_denied_by_you, userId.value)
sp.getString(R.string.state_event_room_knock_denied_by_you, userDisplayNameOrId)
} else if (memberIsYou) {
sp.getString(R.string.state_event_room_knock_denied_you, senderDisambiguatedDisplayName)
} else {
sp.getString(R.string.state_event_room_knock_denied, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_knock_denied, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.NONE -> if (senderIsYou) {
sp.getString(R.string.state_event_room_none_by_you)

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="state_event_avatar_changed_too">"(ფოტოც შეიცვალა)"</string>
<string name="state_event_avatar_url_changed">"%1$s პროფილის ფოტო შეცვალა"</string>
<string name="state_event_avatar_url_changed_by_you">"თქვენ შეცვალეთ პროფილის ფოტო"</string>
<string name="state_event_display_name_changed_from">"%1$s თავისი ნაჩვენები სახელი შეცვალა %2$s დან %3$s ზე"</string>
<string name="state_event_display_name_changed_from_by_you">"თქვენ შეცვალეთ თქვენი ნაჩვენები სახელი %1$s -დან %2$s -ზე"</string>
<string name="state_event_display_name_removed">"%1$s წაშალა თავისი ნაჩვენები სახელი (იყო %2$s)"</string>
<string name="state_event_display_name_removed_by_you">"თქვენ წაშალეთ ნაჩვენები სახელი (იყო %1$s)"</string>
<string name="state_event_display_name_set">"%1$s თავისი ნაჩვენები სახელი შეცვალა %2$s"</string>
<string name="state_event_display_name_set_by_you">"თქვენი ახალი ნაჩვენები სახელი - %1$s"</string>
<string name="state_event_room_avatar_changed">"%1$s ოთახის ფოტო შეცვალა"</string>
<string name="state_event_room_avatar_changed_by_you">"თქვენ შეცვალეთ ოთახის ფოტო"</string>
<string name="state_event_room_avatar_removed">"%1$s წაშალა ოთახის ფოტო"</string>
<string name="state_event_room_avatar_removed_by_you">"თქვენ წაშალეთ ოთახის ფოტო"</string>
<string name="state_event_room_ban">"%1$s დაბლოკა %2$s"</string>
<string name="state_event_room_ban_by_you">"თქვენ დაბლოკეთ %1$s"</string>
<string name="state_event_room_created">"%1$s შექმნა ოთახი"</string>
<string name="state_event_room_created_by_you">"თქვენ შექმენით ოთახი"</string>
<string name="state_event_room_invite">"%1$s მოიწვია %2$s"</string>
<string name="state_event_room_invite_accepted">"%1$s მიიღო მოწვევა"</string>
<string name="state_event_room_invite_accepted_by_you">"თქვენ მიიღეთ მოწვევა"</string>
<string name="state_event_room_invite_by_you">"თქვენ მოიწვიეთ %1$s"</string>
<string name="state_event_room_invite_you">"%1$s მოგიწვიათ"</string>
<string name="state_event_room_join">"%1$s გაწევრიანდა ოთახში"</string>
<string name="state_event_room_join_by_you">"თქვენ გაწევრიანდით ოთახში"</string>
<string name="state_event_room_knock">"%1$s გაწევრიანება მოითხოვა"</string>
<string name="state_event_room_knock_accepted">"%1$s გაწევრიანების უფლება მისცა %2$s"</string>
<string name="state_event_room_knock_accepted_by_you">"თქვენ %1$s გაწევრიანების უფლება მიეცით"</string>
<string name="state_event_room_knock_by_you">"თქვენ მოითხოვეთ გაწევრიანება"</string>
<string name="state_event_room_knock_denied">"%1$s უარი თქვა %2$s-ს გაწევრიანების მოთხოვნაზე"</string>
<string name="state_event_room_knock_denied_by_you">"თქვენ უარი თქვით %1$s გაწევრიანების თხოვნაზე"</string>
<string name="state_event_room_knock_denied_you">"%1$s უარი თქვა თქვენს მოთხოვნაზე გაწევრიანების შესახებ"</string>
<string name="state_event_room_knock_retracted">"%1$s აღარ არის დაინტერესებული გაწევრიანებით"</string>
<string name="state_event_room_knock_retracted_by_you">"თქვენ გააუქმეთ გაწევრიანების მოთხოვნა"</string>
<string name="state_event_room_leave">"%1$s დატოვა ოთახი"</string>
<string name="state_event_room_leave_by_you">"თქვენ დატოვეთ ოთახი"</string>
<string name="state_event_room_name_changed">"%1$s შეცვალა ოთახის სახელი: %2$s"</string>
<string name="state_event_room_name_changed_by_you">"თქვენ შეცვალეთ ოთახის სახელი: %1$s"</string>
<string name="state_event_room_name_removed">"%1$s წაშალა ოთახის სახელი"</string>
<string name="state_event_room_name_removed_by_you">"თქვენ წაშალეთ ოთახის სახელი"</string>
<string name="state_event_room_reject">"%1$s მოწვევაზე უარი თქვა"</string>
<string name="state_event_room_reject_by_you">"თქვენ უარი თქვით მოწვევაზე"</string>
<string name="state_event_room_remove">"%1$s გააგდო %2$s"</string>
<string name="state_event_room_remove_by_you">"თქვენ გააგდეთ %1$s"</string>
<string name="state_event_room_third_party_invite">"%1$s მოიწვია %2$s ოთახში"</string>
<string name="state_event_room_third_party_invite_by_you">"თქვენ მოიწვიეთ %1$s ოთახში"</string>
<string name="state_event_room_third_party_revoked_invite">"%1$s გააუქმო %2$s-ს ოთახში მოწვევა"</string>
<string name="state_event_room_third_party_revoked_invite_by_you">"თქვენ %1$s-ს ოთახში მოწვევა გააუქმეთ"</string>
<string name="state_event_room_topic_changed">"%1$s შეცვალა თემა: %2$s"</string>
<string name="state_event_room_topic_changed_by_you">"თქვენ შეცვალეთ თემა: %1$s"</string>
<string name="state_event_room_topic_removed">"%1$s წაშალა ოთახის თემა"</string>
<string name="state_event_room_topic_removed_by_you">"თქვენ წაშალეთ ოთახის თემა"</string>
<string name="state_event_room_unban">"%1$s განბლოკა %2$s"</string>
<string name="state_event_room_unban_by_you">"თქვენ განბლოკეთ %1$s"</string>
<string name="state_event_room_unknown_membership_change">"%1$s უცნობი ცვლილება შეიტანა თავის წევრობაში"</string>
</resources>

View file

@ -254,9 +254,9 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Membership change - joined`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.JOINED)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.JOINED)
val youJoinedRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youJoinedRoom = formatter.format(youJoinedRoomEvent, false)
@ -270,9 +270,9 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Membership change - left`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.LEFT)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.LEFT)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.LEFT)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.LEFT)
val youLeftRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youLeftRoom = formatter.format(youLeftRoomEvent, false)
@ -286,67 +286,71 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Membership change - banned`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.BANNED)
val youKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KICKED_AND_BANNED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.BANNED)
val someoneKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KICKED_AND_BANNED)
val otherName = "Other"
val third = "Someone"
val youContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED)
val youKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED)
val someoneKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED)
val youBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youBanned = formatter.format(youBannedEvent, false)
assertThat(youBanned).isEqualTo("You banned ${youContent.userId}")
assertThat(youBanned).isEqualTo("You banned $third")
val youKickBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youKickedContent)
val youKickedBanned = formatter.format(youKickBannedEvent, false)
assertThat(youKickedBanned).isEqualTo("You banned ${youContent.userId}")
assertThat(youKickedBanned).isEqualTo("You banned $third")
val someoneBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneBanned = formatter.format(someoneBannedEvent, false)
assertThat(someoneBanned).isEqualTo("$otherName banned ${someoneContent.userId}")
assertThat(someoneBanned).isEqualTo("$otherName banned $third")
val someoneKickBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneKickedContent)
val someoneKickBanned = formatter.format(someoneKickBannedEvent, false)
assertThat(someoneKickBanned).isEqualTo("$otherName banned ${someoneContent.userId}")
assertThat(someoneKickBanned).isEqualTo("$otherName banned $third")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - unban`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.UNBANNED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.UNBANNED)
val otherName = "Other"
val third = "Someone"
val youContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.UNBANNED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.UNBANNED)
val youUnbannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youUnbanned = formatter.format(youUnbannedEvent, false)
assertThat(youUnbanned).isEqualTo("You unbanned ${youContent.userId}")
assertThat(youUnbanned).isEqualTo("You unbanned $third")
val someoneUnbannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneUnbanned = formatter.format(someoneUnbannedEvent, false)
assertThat(someoneUnbanned).isEqualTo("$otherName unbanned ${someoneContent.userId}")
assertThat(someoneUnbanned).isEqualTo("$otherName unbanned $third")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - kicked`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KICKED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KICKED)
val otherName = "Other"
val third = "Someone"
val youContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED)
val youKickedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youKicked = formatter.format(youKickedEvent, false)
assertThat(youKicked).isEqualTo("You removed ${youContent.userId}")
assertThat(youKicked).isEqualTo("You removed $third")
val someoneKickedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneKicked = formatter.format(someoneKickedEvent, false)
assertThat(someoneKicked).isEqualTo("$otherName removed ${someoneContent.userId}")
assertThat(someoneKicked).isEqualTo("$otherName removed $third")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - invited`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.INVITED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.INVITED)
val otherName = "Other"
val third = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.INVITED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.INVITED)
val youWereInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent)
val youWereInvited = formatter.format(youWereInvitedEvent, false)
@ -354,19 +358,19 @@ class DefaultRoomLastMessageFormatterTest {
val youInvitedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youInvited = formatter.format(youInvitedEvent, false)
assertThat(youInvited).isEqualTo("You invited ${someoneContent.userId}")
assertThat(youInvited).isEqualTo("You invited $third")
val someoneInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneInvited = formatter.format(someoneInvitedEvent, false)
assertThat(someoneInvited).isEqualTo("$otherName invited ${someoneContent.userId}")
assertThat(someoneInvited).isEqualTo("$otherName invited $third")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - invitation accepted`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.INVITATION_ACCEPTED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.INVITATION_ACCEPTED)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.INVITATION_ACCEPTED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.INVITATION_ACCEPTED)
val youAcceptedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youAcceptedInvite = formatter.format(youAcceptedInviteEvent, false)
@ -374,15 +378,15 @@ class DefaultRoomLastMessageFormatterTest {
val someoneAcceptedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneAcceptedInvite = formatter.format(someoneAcceptedInviteEvent, false)
assertThat(someoneAcceptedInvite).isEqualTo("${someoneContent.userId} accepted the invite")
assertThat(someoneAcceptedInvite).isEqualTo("$otherName accepted the invite")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - invitation rejected`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.INVITATION_REJECTED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.INVITATION_REJECTED)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.INVITATION_REJECTED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.INVITATION_REJECTED)
val youRejectedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youRejectedInvite = formatter.format(youRejectedInviteEvent, false)
@ -390,30 +394,31 @@ class DefaultRoomLastMessageFormatterTest {
val someoneRejectedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneRejectedInvite = formatter.format(someoneRejectedInviteEvent, false)
assertThat(someoneRejectedInvite).isEqualTo("${someoneContent.userId} rejected the invitation")
assertThat(someoneRejectedInvite).isEqualTo("$otherName rejected the invitation")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - invitation revoked`() {
val otherName = "Someone"
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.INVITATION_REVOKED)
val otherName = "Other"
val third = "Someone"
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.INVITATION_REVOKED)
val youRevokedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youRevokedInvite = formatter.format(youRevokedInviteEvent, false)
assertThat(youRevokedInvite).isEqualTo("You revoked the invitation for ${someoneContent.userId} to join the room")
assertThat(youRevokedInvite).isEqualTo("You revoked the invitation for $third to join the room")
val someoneRevokedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneRevokedInvite = formatter.format(someoneRevokedInviteEvent, false)
assertThat(someoneRevokedInvite).isEqualTo("$otherName revoked the invitation for ${someoneContent.userId} to join the room")
assertThat(someoneRevokedInvite).isEqualTo("$otherName revoked the invitation for $third to join the room")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - knocked`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.KNOCKED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KNOCKED)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.KNOCKED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.KNOCKED)
val youKnockedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youKnocked = formatter.format(youKnockedEvent, false)
@ -427,24 +432,25 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Membership change - knock accepted`() {
val otherName = "Someone"
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KNOCK_ACCEPTED)
val otherName = "Other"
val third = "Someone"
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KNOCK_ACCEPTED)
val youAcceptedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youAcceptedKnock = formatter.format(youAcceptedKnockEvent, false)
assertThat(youAcceptedKnock).isEqualTo("You allowed ${someoneContent.userId} to join")
assertThat(youAcceptedKnock).isEqualTo("You allowed $third to join")
val someoneAcceptedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneAcceptedKnock = formatter.format(someoneAcceptedKnockEvent, false)
assertThat(someoneAcceptedKnock).isEqualTo("$otherName allowed ${someoneContent.userId} to join")
assertThat(someoneAcceptedKnock).isEqualTo("$otherName allowed $third to join")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - knock retracted`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.KNOCK_RETRACTED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KNOCK_RETRACTED)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.KNOCK_RETRACTED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), null, MembershipChange.KNOCK_RETRACTED)
val youRetractedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youRetractedKnock = formatter.format(youRetractedKnockEvent, false)
@ -458,17 +464,18 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Membership change - knock denied`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.KNOCK_DENIED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KNOCK_DENIED)
val otherName = "Other"
val third = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, third, MembershipChange.KNOCK_DENIED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KNOCK_DENIED)
val youDeniedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youDeniedKnock = formatter.format(youDeniedKnockEvent, false)
assertThat(youDeniedKnock).isEqualTo("You rejected ${someoneContent.userId}'s request to join")
assertThat(youDeniedKnock).isEqualTo("You rejected $third's request to join")
val someoneDeniedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneDeniedKnock = formatter.format(someoneDeniedKnockEvent, false)
assertThat(someoneDeniedKnock).isEqualTo("$otherName rejected ${someoneContent.userId}'s request to join")
assertThat(someoneDeniedKnock).isEqualTo("$otherName rejected $third's request to join")
val someoneDeniedYourKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent)
val someoneDeniedYourKnock = formatter.format(someoneDeniedYourKnockEvent, false)
@ -478,9 +485,9 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Membership change - None`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.NONE)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.NONE)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.NONE)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.NONE)
val youNoneRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youNoneRoom = formatter.format(youNoneRoomEvent, false)
@ -497,7 +504,7 @@ class DefaultRoomLastMessageFormatterTest {
val otherChanges = arrayOf(MembershipChange.ERROR, MembershipChange.NOT_IMPLEMENTED, null)
val results = otherChanges.map { change ->
val content = RoomMembershipContent(A_USER_ID, change)
val content = RoomMembershipContent(A_USER_ID, null, change)
val event = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content)
val result = formatter.format(event, false)
change to result
@ -513,7 +520,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Room state change - avatar`() {
val otherName = "Someone"
val otherName = "Other"
val changedContent = StateContent("", OtherState.RoomAvatar("new_avatar"))
val removedContent = StateContent("", OtherState.RoomAvatar(null))
@ -537,7 +544,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Room state change - create`() {
val otherName = "Someone"
val otherName = "Other"
val content = StateContent("", OtherState.RoomCreate)
val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content)
@ -552,7 +559,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Room state change - encryption`() {
val otherName = "Someone"
val otherName = "Other"
val content = StateContent("", OtherState.RoomEncryption)
val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content)
@ -567,7 +574,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Room state change - room name`() {
val otherName = "Someone"
val otherName = "Other"
val newName = "New name"
val changedContent = StateContent("", OtherState.RoomName(newName))
val removedContent = StateContent("", OtherState.RoomName(null))
@ -592,7 +599,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Room state change - third party invite`() {
val otherName = "Someone"
val otherName = "Other"
val inviteeName = "Alice"
val changedContent = StateContent("", OtherState.RoomThirdPartyInvite(inviteeName))
val removedContent = StateContent("", OtherState.RoomThirdPartyInvite(null))
@ -617,7 +624,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Room state change - room topic`() {
val otherName = "Someone"
val otherName = "Other"
val roomTopic = "New topic"
val changedContent = StateContent("", OtherState.RoomTopic(roomTopic))
val removedContent = StateContent("", OtherState.RoomTopic(null))
@ -677,7 +684,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Profile change - avatar`() {
val otherName = "Someone"
val otherName = "Other"
val changedContent = aProfileChangeMessageContent(avatarUrl = "new_avatar_url", prevAvatarUrl = "old_avatar_url")
val setContent = aProfileChangeMessageContent(avatarUrl = "new_avatar_url", prevAvatarUrl = null)
val removedContent = aProfileChangeMessageContent(avatarUrl = null, prevAvatarUrl = "old_avatar_url")
@ -722,7 +729,7 @@ class DefaultRoomLastMessageFormatterTest {
fun `Profile change - display name`() {
val newDisplayName = "New"
val oldDisplayName = "Old"
val otherName = "Someone"
val otherName = "Other"
val changedContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = oldDisplayName)
val setContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = null)
val removedContent = aProfileChangeMessageContent(displayName = null, prevDisplayName = oldDisplayName)

View file

@ -38,7 +38,7 @@ class DefaultIndicatorService @Inject constructor(
) : IndicatorService {
@Composable
override fun showRoomListTopBarIndicator(): State<Boolean> {
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
val canVerifySession by sessionVerificationService.needsSessionVerification.collectAsState(initial = false)
val settingChatBackupIndicator = showSettingChatBackupIndicator()
return remember {

View file

@ -19,7 +19,6 @@ package io.element.android.libraries.matrix.api
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
@ -105,5 +104,5 @@ interface MatrixClient : Closeable {
suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit>
suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>>
suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<ResolvedRoomAlias>
suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview>
suspend fun getRoomPreviewFromRoomId(roomId: RoomId, serverNames: List<String>): Result<RoomPreview>
}

View file

@ -50,4 +50,16 @@ interface EncryptionService {
* Wait for backup upload steady state.
*/
fun waitForBackupUploadSteadyState(): Flow<BackupUploadState>
/**
* Get the public curve25519 key of our own device in base64. This is usually what is
* called the identity key of the device.
*/
suspend fun deviceCurve25519(): String?
/**
* Get the public ed25519 key of our own device. This is usually what is
* called the fingerprint of the device.
*/
suspend fun deviceEd25519(): String?
}

View file

@ -18,5 +18,5 @@ package io.element.android.libraries.matrix.api.pusher
interface PushersService {
suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result<Unit>
suspend fun unsetHttpPusher(): Result<Unit>
suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result<Unit>
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2023 New Vector Ltd
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,10 +14,9 @@
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.preferences
package io.element.android.libraries.matrix.api.pusher
import androidx.compose.ui.unit.dp
internal val preferenceMinHeightOnlyTitle = 56.dp
internal val preferenceMinHeight = 56.dp
internal val preferencePaddingHorizontal = 16.dp
data class UnsetHttpPusherData(
val pushKey: String,
val appId: String,
)

View file

@ -73,6 +73,7 @@ data class UnableToDecryptContent(
data class RoomMembershipContent(
val userId: UserId,
val userDisplayName: String?,
val change: MembershipChange?
) : EventContent

View file

@ -26,12 +26,6 @@ interface SessionVerificationService {
*/
val verificationFlowState: StateFlow<VerificationFlowState>
/**
* The internal service that checks verification can only run after the initial sync.
* This [StateFlow] will notify consumers when the service is ready to be used.
*/
val isReady: StateFlow<Boolean>
/**
* Returns whether the current verification status is either: [SessionVerifiedStatus.Unknown], [SessionVerifiedStatus.NotVerified]
* or [SessionVerifiedStatus.Verified].
@ -39,9 +33,9 @@ interface SessionVerificationService {
val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus>
/**
* Returns whether the current session needs to be verified and the SDK is ready to start the verification.
* Returns whether the current session needs to be verified.
*/
val canVerifySessionFlow: Flow<Boolean>
val needsSessionVerification: Flow<Boolean>
/**
* Request verification of the current session.

View file

@ -25,7 +25,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset
@ -487,9 +486,12 @@ class RustMatrixClient(
}
}
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview> = withContext(sessionDispatcher) {
override suspend fun getRoomPreviewFromRoomId(roomId: RoomId, serverNames: List<String>): Result<RoomPreview> = withContext(sessionDispatcher) {
runCatching {
client.getRoomPreview(roomIdOrAlias.identifier).let(RoomPreviewMapper::map)
client.getRoomPreviewFromRoomId(
roomId = roomId.value,
viaServers = serverNames,
).let(RoomPreviewMapper::map)
}
}

View file

@ -190,4 +190,12 @@ internal class RustEncryptionService(
it.mapRecoveryException()
}
}
override suspend fun deviceCurve25519(): String? {
return service.curve25519Key()
}
override suspend fun deviceEd25519(): String? {
return service.ed25519Key()
}
}

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.pushers
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.HttpPusherData
@ -54,8 +55,16 @@ class RustPushersService(
}
}
override suspend fun unsetHttpPusher(): Result<Unit> {
// TODO Missing client API. We need to set the pusher with Kind == null, but we do not have access to this field from the SDK.
return Result.success(Unit)
override suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result<Unit> {
return withContext(dispatchers.io) {
runCatching {
client.deletePusher(
identifiers = PusherIdentifiers(
pushkey = unsetHttpPusherData.pushKey,
appId = unsetHttpPusherData.appId
),
)
}
}
}
}

View file

@ -88,8 +88,9 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
}
is TimelineItemContentKind.RoomMembership -> {
RoomMembershipContent(
UserId(kind.userId),
kind.change?.map()
userId = UserId(kind.userId),
userDisplayName = kind.userDisplayName,
change = kind.change?.map()
)
}
is TimelineItemContentKind.State -> {

View file

@ -30,8 +30,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -80,10 +80,14 @@ class RustSessionVerificationService(
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
override val isReady = isSyncServiceReady.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
/**
* The internal service that checks verification can only run after the initial sync.
* This [StateFlow] will notify consumers when the service is ready to be used.
*/
private val isReady = isSyncServiceReady.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
override val canVerifySessionFlow = combine(sessionVerifiedStatus, isReady) { verificationStatus, isReady ->
isReady && verificationStatus == SessionVerifiedStatus.NotVerified
override val needsSessionVerification = sessionVerifiedStatus.map { verificationStatus ->
verificationStatus == SessionVerifiedStatus.NotVerified
}
init {

View file

@ -57,9 +57,9 @@ class DefaultJoinRoomTest {
.isNeverCalled()
joinRoomLambda
.assertions()
.isCalledExactly(1)
.withSequence(
listOf(value(A_ROOM_ID))
.isCalledOnce()
.with(
value(A_ROOM_ID)
)
assertThat(analyticsService.capturedEvents).containsExactly(
roomResult.toAnalyticsJoinedRoom(aTrigger)
@ -88,9 +88,10 @@ class DefaultJoinRoomTest {
sut.invoke(A_ROOM_ID, A_SERVER_LIST, aTrigger)
joinRoomByIdOrAliasLambda
.assertions()
.isCalledExactly(1)
.withSequence(
listOf(value(A_ROOM_ID), value(A_SERVER_LIST))
.isCalledOnce()
.with(
value(A_ROOM_ID),
value(A_SERVER_LIST)
)
joinRoomLambda
.assertions()

View file

@ -34,7 +34,7 @@ class RoomBeginningPostProcessorTest {
fun `processor removes room creation event and self-join event from DM timeline`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
@ -44,13 +44,13 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor removes room creation event and self-join event from DM timeline even if they're not the first items`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val expected = listOf(
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
)
val processor = RoomBeginningPostProcessor()
@ -62,7 +62,7 @@ class RoomBeginningPostProcessorTest {
fun `processor will add beginning of room item if it's not a DM`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
@ -85,7 +85,7 @@ class RoomBeginningPostProcessorTest {
fun `processor won't remove items if it's not at the start of the timeline`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
@ -95,7 +95,7 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor won't remove the first member join event if it can't find the room creation event`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
@ -106,7 +106,7 @@ class RoomBeginningPostProcessorTest {
fun `processor won't remove the first member join event if it's not from the room creator`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)

View file

@ -20,7 +20,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
@ -41,7 +40,7 @@ import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.pushers.FakePushersService
@ -54,7 +53,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOf
@ -68,7 +66,7 @@ class FakeMatrixClient(
private val userDisplayName: String? = A_USER_NAME,
private val userAvatarUrl: String? = AN_AVATAR_URL,
override val roomListService: RoomListService = FakeRoomListService(),
override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(),
override val mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(),
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
private val pushersService: FakePushersService = FakePushersService(),
private val notificationService: FakeNotificationService = FakeNotificationService(),
@ -78,7 +76,7 @@ class FakeMatrixClient(
private val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(),
private val accountManagementUrlString: Result<String?> = Result.success(null),
private val resolveRoomAliasResult: (RoomAlias) -> Result<ResolvedRoomAlias> = { Result.success(ResolvedRoomAlias(A_ROOM_ID, emptyList())) },
private val getRoomPreviewResult: (RoomIdOrAlias) -> Result<RoomPreview> = { Result.failure(AN_EXCEPTION) },
private val getRoomPreviewFromRoomIdResult: (RoomId, List<String>) -> Result<RoomPreview> = { _, _ -> Result.failure(AN_EXCEPTION) },
) : MatrixClient {
var setDisplayNameCalled: Boolean = false
private set
@ -96,7 +94,6 @@ class FakeMatrixClient(
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var findDmResult: RoomId? = A_ROOM_ID
private var logoutFailure: Throwable? = null
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
@ -116,6 +113,9 @@ class FakeMatrixClient(
var getRoomInfoFlowLambda = { _: RoomId ->
flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
}
var logoutLambda: (Boolean) -> String? = {
null
}
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
return getRoomResults[roomId]
@ -160,12 +160,8 @@ class FakeMatrixClient(
override suspend fun clearCache() {
}
override suspend fun logout(ignoreSdkError: Boolean): String? {
delay(100)
if (ignoreSdkError.not()) {
logoutFailure?.let { throw it }
}
return null
override suspend fun logout(ignoreSdkError: Boolean): String? = simulateLongTask {
return logoutLambda(ignoreSdkError)
}
override fun close() = Unit
@ -229,10 +225,6 @@ class FakeMatrixClient(
// Mocks
fun givenLogoutError(failure: Throwable?) {
logoutFailure = failure
}
fun givenCreateRoomResult(result: Result<RoomId>) {
createRoomResult = result
}
@ -297,8 +289,8 @@ class FakeMatrixClient(
resolveRoomAliasResult(roomAlias)
}
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview> = simulateLongTask {
getRoomPreviewResult(roomIdOrAlias)
override suspend fun getRoomPreviewFromRoomId(roomId: RoomId, serverNames: List<String>) = simulateLongTask {
getRoomPreviewFromRoomIdResult(roomId, serverNames)
}
override suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>> {

View file

@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
const val A_USER_NAME = "alice"
const val A_PASSWORD = "password"
const val A_SECRET = "secret"
val A_USER_ID = UserId("@alice:server.org")
val A_USER_ID_2 = UserId("@bob:server.org")

View file

@ -31,7 +31,9 @@ import kotlinx.coroutines.flow.flowOf
val A_OIDC_DATA = OidcDetails(url = "a-url")
class FakeAuthenticationService : MatrixAuthenticationService {
class FakeMatrixAuthenticationService(
private val matrixClientResult: ((SessionId) -> Result<MatrixClient>)? = null
) : MatrixAuthenticationService {
private val homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
private var oidcError: Throwable? = null
private var oidcCancelError: Throwable? = null
@ -39,15 +41,18 @@ class FakeAuthenticationService : MatrixAuthenticationService {
private var changeServerError: Throwable? = null
private var matrixClient: MatrixClient? = null
var getLatestSessionIdLambda: (() -> SessionId?) = { null }
override fun loggedInStateFlow(): Flow<LoggedInState> {
return flowOf(LoggedInState.NotLoggedIn)
}
override suspend fun getLatestSessionId(): SessionId? {
return null
}
override suspend fun getLatestSessionId(): SessionId? = getLatestSessionIdLambda()
override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> {
if (matrixClientResult != null) {
return matrixClientResult.invoke(sessionId)
}
return if (matrixClient != null) {
Result.success(matrixClient!!)
} else {

View file

@ -28,7 +28,7 @@ fun aBuildMeta(
applicationId: String = "",
lowPrivacyLoggingEnabled: Boolean = true,
versionName: String = "",
versionCode: Int = 0,
versionCode: Long = 0,
gitRevision: String = "",
gitBranchName: String = "",
flavorDescription: String = "",

View file

@ -39,6 +39,9 @@ class FakeEncryptionService : EncryptionService {
private var enableBackupsFailure: Exception? = null
private var curve25519: String? = null
private var ed25519: String? = null
fun givenEnableBackupsFailure(exception: Exception?) {
enableBackupsFailure = exception
}
@ -94,6 +97,15 @@ class FakeEncryptionService : EncryptionService {
return waitForBackupUploadSteadyStateFlow
}
fun givenDeviceKeys(curve25519: String?, ed25519: String?) {
this.curve25519 = curve25519
this.ed25519 = ed25519
}
override suspend fun deviceCurve25519(): String? = curve25519
override suspend fun deviceEd25519(): String? = ed25519
suspend fun emitBackupState(state: BackupState) {
backupStateStateFlow.emit(state)
}

View file

@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.tests.testutils.simulateLongTask
class FakeMediaLoader : MatrixMediaLoader {
class FakeMatrixMediaLoader : MatrixMediaLoader {
var shouldFail = false
var path: String = ""

View file

@ -20,9 +20,9 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
class FakePermalinkBuilder(
private val result: () -> Result<String> = { Result.failure(Exception("Not implemented")) }
private val result: (UserId) -> Result<String> = { Result.failure(Exception("Not implemented")) }
) : PermalinkBuilder {
override fun permalinkForUser(userId: UserId): Result<String> {
return result()
return result(userId)
}
}

View file

@ -18,9 +18,10 @@ package io.element.android.libraries.matrix.test.permalink
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.tests.testutils.lambda.lambdaError
class FakePermalinkParser(
private var result: () -> PermalinkData = { TODO("Not implemented") }
private var result: () -> PermalinkData = { lambdaError() }
) : PermalinkParser {
fun givenResult(result: PermalinkData) {
this.result = { result }

View file

@ -18,8 +18,13 @@ package io.element.android.libraries.matrix.test.pushers
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
import io.element.android.tests.testutils.lambda.lambdaError
class FakePushersService : PushersService {
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit)
override suspend fun unsetHttpPusher(): Result<Unit> = Result.success(Unit)
class FakePushersService(
private val setHttpPusherResult: (SetHttpPusherData) -> Result<Unit> = { lambdaError() },
private val unsetHttpPusherResult: (UnsetHttpPusherData) -> Result<Unit> = { lambdaError() },
) : PushersService {
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = setHttpPusherResult(setHttpPusherData)
override suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result<Unit> = unsetHttpPusherResult(unsetHttpPusherData)
}

View file

@ -55,7 +55,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
@ -125,7 +125,7 @@ class FakeMatrixRoom(
private var endPollResult = Result.success(Unit)
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
private var generateWidgetWebViewUrlResult = Result.success("https://call.element.io")
private var getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeWidgetDriver())
private var getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeMatrixWidgetDriver())
private var canUserTriggerRoomNotificationResult: Result<Boolean> = Result.success(true)
private var canUserJoinCallResult: Result<Boolean> = Result.success(true)
private var setIsFavoriteResult = Result.success(Unit)

View file

@ -25,17 +25,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class FakeSessionVerificationService : SessionVerificationService {
private val _isReady = MutableStateFlow(false)
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
private var _canVerifySessionFlow = MutableStateFlow(true)
private var _needsSessionVerification = MutableStateFlow(true)
var shouldFail = false
override val verificationFlowState: StateFlow<VerificationFlowState> = _verificationFlowState
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
override val canVerifySessionFlow: Flow<Boolean> = _canVerifySessionFlow
override val isReady: StateFlow<Boolean> = _isReady
override val needsSessionVerification: Flow<Boolean> = _needsSessionVerification
override suspend fun requestVerification() {
if (!shouldFail) {
@ -85,12 +82,8 @@ class FakeSessionVerificationService : SessionVerificationService {
_verificationFlowState.value = state
}
fun givenCanVerifySession(canVerify: Boolean) {
_canVerifySessionFlow.value = canVerify
}
fun givenIsReady(value: Boolean) {
_isReady.value = value
fun givenNeedsSessionVerification(needsVerification: Boolean) {
_needsSessionVerification.value = needsVerification
}
override suspend fun reset() {

View file

@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import kotlinx.coroutines.flow.MutableSharedFlow
import java.util.UUID
class FakeWidgetDriver(
class FakeMatrixWidgetDriver(
override val id: String = UUID.randomUUID().toString(),
) : MatrixWidgetDriver {
private val _sentMessages = mutableListOf<String>()

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_invited_you">"%1$s (%2$s) მოგიწვიათ"</string>
</resources>

View file

@ -25,7 +25,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
import io.element.android.libraries.matrix.test.media.aMediaSource
import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerEvents
@ -51,9 +51,9 @@ class MediaViewerPresenterTest {
@Test
fun `present - download media success scenario`() = runTest {
val mediaLoader = FakeMediaLoader()
val matrixMediaLoader = FakeMatrixMediaLoader()
val mediaActions = FakeLocalMediaActions()
val presenter = createMediaViewerPresenter(mediaLoader, mediaActions)
val presenter = createMediaViewerPresenter(matrixMediaLoader, mediaActions)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -71,10 +71,10 @@ class MediaViewerPresenterTest {
@Test
fun `present - check all actions `() = runTest {
val mediaLoader = FakeMediaLoader()
val matrixMediaLoader = FakeMatrixMediaLoader()
val mediaActions = FakeLocalMediaActions()
val snackbarDispatcher = SnackbarDispatcher()
val presenter = createMediaViewerPresenter(mediaLoader, mediaActions, snackbarDispatcher)
val presenter = createMediaViewerPresenter(matrixMediaLoader, mediaActions, snackbarDispatcher)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -118,13 +118,13 @@ class MediaViewerPresenterTest {
@Test
fun `present - download media failure then retry with success scenario`() = runTest {
val mediaLoader = FakeMediaLoader()
val matrixMediaLoader = FakeMatrixMediaLoader()
val mediaActions = FakeLocalMediaActions()
val presenter = createMediaViewerPresenter(mediaLoader, mediaActions)
val presenter = createMediaViewerPresenter(matrixMediaLoader, mediaActions)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
mediaLoader.shouldFail = true
matrixMediaLoader.shouldFail = true
val initialState = awaitItem()
assertThat(initialState.downloadedMedia).isEqualTo(AsyncData.Uninitialized)
assertThat(initialState.mediaInfo).isEqualTo(TESTED_MEDIA_INFO)
@ -132,7 +132,7 @@ class MediaViewerPresenterTest {
assertThat(loadingState.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java)
val failureState = awaitItem()
assertThat(failureState.downloadedMedia).isInstanceOf(AsyncData.Failure::class.java)
mediaLoader.shouldFail = false
matrixMediaLoader.shouldFail = false
failureState.eventSink(MediaViewerEvents.RetryLoading)
// There is one recomposition because of the retry mechanism
skipItems(1)
@ -146,7 +146,7 @@ class MediaViewerPresenterTest {
}
private fun createMediaViewerPresenter(
mediaLoader: FakeMediaLoader,
matrixMediaLoader: FakeMatrixMediaLoader,
localMediaActions: FakeLocalMediaActions,
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
canShare: Boolean = true,
@ -161,7 +161,7 @@ class MediaViewerPresenterTest {
canDownload = canDownload,
),
localMediaFactory = localMediaFactory,
mediaLoader = mediaLoader,
mediaLoader = matrixMediaLoader,
localMediaActions = localMediaActions,
snackbarDispatcher = snackbarDispatcher,
)

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"იმისათვის, რომ აპლიკაციამ გამოიყენოს კამერა, გთხოვთ, მიანიჭოთ ნებართვა სისტემის პარამეტრებში."</string>
<string name="dialog_permission_generic">"გთხოვთ, მიანიჭოთ ნებართვა სისტემის პარამეტრებში."</string>
<string name="dialog_permission_microphone">"იმისათვის, რომ აპლიკაციამ მიკროფონი გამოიყენოს, გთხოვთ, მიანიჭოთ ნებართვა სისტემის პარამეტრებში."</string>
<string name="dialog_permission_notification">"იმისათვის, რომ აპლიკაციამ გამოაჩინოს შეტყობინებები, გთხოვთ, მიანიჭოთ ნებართვა სისტემის პარამეტრებში."</string>
</resources>

View file

@ -19,9 +19,6 @@ package io.element.android.features.preferences.api.store
import kotlinx.coroutines.flow.Flow
interface AppPreferencesStore {
suspend fun setRichTextEditorEnabled(enabled: Boolean)
fun isRichTextEditorEnabledFlow(): Flow<Boolean>
suspend fun setDeveloperModeEnabled(enabled: Boolean)
fun isDeveloperModeEnabledFlow(): Flow<Boolean>

View file

@ -25,7 +25,6 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.core.bool.orTrue
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.di.AppScope
@ -36,7 +35,6 @@ import javax.inject.Inject
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_preferences")
private val richTextEditorKey = booleanPreferencesKey("richTextEditor")
private val developerModeKey = booleanPreferencesKey("developerMode")
private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl")
private val themeKey = stringPreferencesKey("theme")
@ -48,19 +46,6 @@ class DefaultAppPreferencesStore @Inject constructor(
) : AppPreferencesStore {
private val store = context.dataStore
override suspend fun setRichTextEditorEnabled(enabled: Boolean) {
store.edit { prefs ->
prefs[richTextEditorKey] = enabled
}
}
override fun isRichTextEditorEnabledFlow(): Flow<Boolean> {
return store.data.map { prefs ->
// enabled by default
prefs[richTextEditorKey].orTrue()
}
}
override suspend fun setDeveloperModeEnabled(enabled: Boolean) {
store.edit { prefs ->
prefs[developerModeKey] = enabled

View file

@ -24,7 +24,7 @@ import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.CoroutineScope
class FakeSessionPreferenceStoreFactory(
class FakeSessionPreferencesStoreFactory(
var getLambda: LambdaTwoParamsRecorder<SessionId, CoroutineScope, SessionPreferencesStore> = lambdaRecorder { _, _ -> throw NotImplementedError() },
var removeLambda: LambdaOneParamRecorder<SessionId, Unit> = lambdaRecorder { _ -> },
) : SessionPreferencesStoreFactory {

View file

@ -21,24 +21,14 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class InMemoryAppPreferencesStore(
isRichTextEditorEnabled: Boolean = false,
isDeveloperModeEnabled: Boolean = false,
customElementCallBaseUrl: String? = null,
theme: String? = null,
) : AppPreferencesStore {
private val isRichTextEditorEnabled = MutableStateFlow(isRichTextEditorEnabled)
private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
private val theme = MutableStateFlow(theme)
override suspend fun setRichTextEditorEnabled(enabled: Boolean) {
isRichTextEditorEnabled.value = enabled
}
override fun isRichTextEditorEnabledFlow(): Flow<Boolean> {
return isRichTextEditorEnabled
}
override suspend fun setDeveloperModeEnabled(enabled: Boolean) {
isDeveloperModeEnabled.value = enabled
}

View file

@ -21,8 +21,10 @@ import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
interface PushService {
// TODO Move away
fun notificationStyleChanged()
/**
* Return the current push provider, or null if none.
*/
suspend fun getCurrentPushProvider(): PushProvider?
/**
* Return the list of push providers, available at compile time, and
@ -35,7 +37,11 @@ interface PushService {
*
* The method has effect only if the [PushProvider] is different than the current one.
*/
suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor)
suspend fun registerWith(
matrixClient: MatrixClient,
pushProvider: PushProvider,
distributor: Distributor,
): Result<Unit>
/**
* Return false in case of early error.

View file

@ -72,6 +72,7 @@ dependencies {
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.toolbox.impl)

View file

@ -21,22 +21,23 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.api.GetCurrentPushProvider
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
import io.element.android.libraries.push.impl.test.TestPush
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import timber.log.Timber
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultPushService @Inject constructor(
private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager,
private val pushersManager: PushersManager,
private val testPush: TestPush,
private val userPushStoreFactory: UserPushStoreFactory,
private val pushProviders: Set<@JvmSuppressWildcards PushProvider>,
private val getCurrentPushProvider: GetCurrentPushProvider,
) : PushService {
override fun notificationStyleChanged() {
defaultNotificationDrawerManager.notificationStyleChanged()
override suspend fun getCurrentPushProvider(): PushProvider? {
val currentPushProvider = getCurrentPushProvider.getCurrentPushProvider()
return pushProviders.find { it.name == currentPushProvider }
}
override fun getAvailablePushProviders(): List<PushProvider> {
@ -45,26 +46,36 @@ class DefaultPushService @Inject constructor(
.sortedBy { it.index }
}
/**
* Get current push provider, compare with provided one, then unregister and register if different, and store change.
*/
override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) {
override suspend fun registerWith(
matrixClient: MatrixClient,
pushProvider: PushProvider,
distributor: Distributor,
): Result<Unit> {
Timber.d("Registering with ${pushProvider.name}/${distributor.name}}")
val userPushStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId)
val currentPushProviderName = userPushStore.getPushProviderName()
if (currentPushProviderName != pushProvider.name) {
val currentPushProvider = pushProviders.find { it.name == currentPushProviderName }
val currentDistributorValue = currentPushProvider?.getCurrentDistributor(matrixClient)?.value
if (currentPushProviderName != pushProvider.name || currentDistributorValue != distributor.value) {
// Unregister previous one if any
pushProviders.find { it.name == currentPushProviderName }?.unregister(matrixClient)
currentPushProvider
?.also { Timber.d("Unregistering previous push provider $currentPushProviderName/$currentDistributorValue") }
?.unregister(matrixClient)
?.onFailure {
Timber.w(it, "Failed to unregister previous push provider")
return Result.failure(it)
}
}
pushProvider.registerWith(matrixClient, distributor)
// Store new value
userPushStore.setPushProviderName(pushProvider.name)
// Then try to register
return pushProvider.registerWith(matrixClient, distributor)
}
override suspend fun testPush(): Boolean {
val currentPushProvider = getCurrentPushProvider.getCurrentPushProvider()
val pushProvider = pushProviders.find { it.name == currentPushProvider } ?: return false
val pushProvider = getCurrentPushProvider() ?: return false
val config = pushProvider.getCurrentUserPushConfig() ?: return false
pushersManager.testPush(config)
testPush.execute(config)
return true
}
}

View file

@ -22,12 +22,9 @@ import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient
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.pusher.SetHttpPusherData
import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
import io.element.android.libraries.pushproviders.api.PusherSubscriber
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
@ -36,48 +33,37 @@ import javax.inject.Inject
internal const val DEFAULT_PUSHER_FILE_TAG = "mobile"
private val loggerTag = LoggerTag("PushersManager", LoggerTag.PushLoggerTag)
private val loggerTag = LoggerTag("DefaultPusherSubscriber", LoggerTag.PushLoggerTag)
@ContributesBinding(AppScope::class)
class PushersManager @Inject constructor(
// private val localeProvider: LocaleProvider,
class DefaultPusherSubscriber @Inject constructor(
private val buildMeta: BuildMeta,
// private val getDeviceInfoUseCase: GetDeviceInfoUseCase,
private val pushGatewayNotifyRequest: PushGatewayNotifyRequest,
private val pushClientSecret: PushClientSecret,
private val userPushStoreFactory: UserPushStoreFactory,
) : PusherSubscriber {
suspend fun testPush(config: CurrentUserPushConfig) {
pushGatewayNotifyRequest.execute(
PushGatewayNotifyRequest.Params(
url = config.url,
appId = PushConfig.PUSHER_APP_ID,
pushKey = config.pushKey,
eventId = TEST_EVENT_ID,
roomId = TEST_ROOM_ID,
)
)
}
/**
* Register a pusher to the server if not done yet.
*/
override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) {
override suspend fun registerPusher(
matrixClient: MatrixClient,
pushKey: String,
gateway: String,
): Result<Unit> {
val userDataStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId)
if (userDataStore.getCurrentRegisteredPushKey() == pushKey) {
Timber.tag(loggerTag.value)
.d("Unnecessary to register again the same pusher, but do it in case the pusher has been removed from the server")
}
matrixClient.pushersService().setHttpPusher(
createHttpPusher(pushKey, gateway, matrixClient.sessionId)
).fold(
{
return matrixClient.pushersService()
.setHttpPusher(
createHttpPusher(pushKey, gateway, matrixClient.sessionId)
)
.onSuccess {
userDataStore.setCurrentRegisteredPushKey(pushKey)
},
{ throwable ->
}
.onFailure { throwable ->
Timber.tag(loggerTag.value).e(throwable, "Unable to register the pusher")
}
)
}
private suspend fun createHttpPusher(
@ -106,12 +92,24 @@ class PushersManager @Inject constructor(
return "{\"cs\":\"$secretForUser\"}"
}
override suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) {
matrixClient.pushersService().unsetHttpPusher()
}
companion object {
val TEST_EVENT_ID = EventId("\$THIS_IS_A_FAKE_EVENT_ID")
val TEST_ROOM_ID = RoomId("!room:domain")
override suspend fun unregisterPusher(
matrixClient: MatrixClient,
pushKey: String,
gateway: String,
): Result<Unit> {
val userDataStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId)
return matrixClient.pushersService()
.unsetHttpPusher(
unsetHttpPusherData = UnsetHttpPusherData(
pushKey = pushKey,
appId = PushConfig.PUSHER_APP_ID
)
)
.onSuccess {
userDataStore.setCurrentRegisteredPushKey(null)
}
.onFailure { throwable ->
Timber.tag(loggerTag.value).e(throwable, "Unable to unregister the pusher")
}
}
}

View file

@ -19,7 +19,9 @@ package io.element.android.libraries.push.impl.notifications
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider
@ -55,7 +57,7 @@ import io.element.android.services.toolbox.api.systemclock.SystemClock
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("NotifiableEventResolver", LoggerTag.NotificationLoggerTag)
private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.NotificationLoggerTag)
/**
* The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event.
@ -63,15 +65,20 @@ private val loggerTag = LoggerTag("NotifiableEventResolver", LoggerTag.Notificat
* The NotifiableEventResolver is the only aware of session/store, the NotificationDrawerManager has no knowledge of that,
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
*/
class NotifiableEventResolver @Inject constructor(
interface NotifiableEventResolver {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent?
}
@ContributesBinding(AppScope::class)
class DefaultNotifiableEventResolver @Inject constructor(
private val stringProvider: StringProvider,
private val clock: SystemClock,
private val matrixClientProvider: MatrixClientProvider,
private val notificationMediaRepoFactory: NotificationMediaRepo.Factory,
@ApplicationContext private val context: Context,
private val permalinkParser: PermalinkParser,
) {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
) : NotifiableEventResolver {
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
// Restore session
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
val notificationService = client.notificationService()

View file

@ -221,18 +221,6 @@ class DefaultNotificationDrawerManager @Inject constructor(
}
}
// TODO EAx Must be per account
fun notificationStyleChanged() {
updateEvents(doRender = true) {
val newSettings = true // pushDataStore.useCompleteNotificationFormat()
if (newSettings != useCompleteNotificationFormat) {
// Settings has changed, remove all current notifications
notificationRenderer.cancelAllNotifications()
useCompleteNotificationFormat = newSettings
}
}
}
private fun updateEvents(
doRender: Boolean,
action: (NotificationEventQueue) -> Unit,

View file

@ -16,27 +16,19 @@
package io.element.android.libraries.push.impl.push
import android.os.Handler
import android.os.Looper
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.push.impl.PushersManager
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
import io.element.android.libraries.push.impl.store.DefaultPushDataStore
import io.element.android.libraries.push.impl.test.DefaultTestPush
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
import io.element.android.libraries.pushproviders.api.PushData
import io.element.android.libraries.pushproviders.api.PushHandler
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -44,23 +36,15 @@ private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag)
@ContributesBinding(AppScope::class)
class DefaultPushHandler @Inject constructor(
private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager,
private val onNotifiableEventReceived: OnNotifiableEventReceived,
private val notifiableEventResolver: NotifiableEventResolver,
private val defaultPushDataStore: DefaultPushDataStore,
private val incrementPushDataStore: IncrementPushDataStore,
private val userPushStoreFactory: UserPushStoreFactory,
private val pushClientSecret: PushClientSecret,
// private val actionIds: NotificationActionIds,
private val buildMeta: BuildMeta,
private val matrixAuthenticationService: MatrixAuthenticationService,
private val diagnosticPushHandler: DiagnosticPushHandler,
) : PushHandler {
private val coroutineScope = CoroutineScope(SupervisorJob())
// UI handler
private val uiHandler by lazy {
Handler(Looper.getMainLooper())
}
/**
* Called when message is received.
*
@ -68,21 +52,15 @@ class DefaultPushHandler @Inject constructor(
*/
override suspend fun handle(pushData: PushData) {
Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}")
if (buildMeta.lowPrivacyLoggingEnabled) {
Timber.tag(loggerTag.value).d("## pushData: $pushData")
}
defaultPushDataStore.incrementPushCounter()
incrementPushDataStore.incrementPushCounter()
// Diagnostic Push
if (pushData.eventId == PushersManager.TEST_EVENT_ID) {
if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) {
diagnosticPushHandler.handlePush()
return
}
uiHandler.post {
coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) }
} else {
handleInternal(pushData)
}
}
@ -98,7 +76,6 @@ class DefaultPushHandler @Inject constructor(
} else {
Timber.tag(loggerTag.value).d("## handleInternal()")
}
val clientSecret = pushData.clientSecret
// clientSecret should not be null. If this happens, restore default session
val userId = clientSecret
@ -109,27 +86,22 @@ class DefaultPushHandler @Inject constructor(
?: run {
matrixAuthenticationService.getLatestSessionId()
}
if (userId == null) {
Timber.w("Unable to get a session")
return
}
val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
if (notifiableEvent == null) {
Timber.w("Unable to get a notification data")
return
}
val userPushStore = userPushStoreFactory.getOrCreate(userId)
if (!userPushStore.getNotificationEnabledForDevice().first()) {
if (userPushStore.getNotificationEnabledForDevice().first()) {
val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
if (notifiableEvent == null) {
Timber.w("Unable to get a notification data")
return
}
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
} else {
// TODO We need to check if this is an incoming call
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
return
}
defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
} catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.push
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.push.impl.store.DefaultPushDataStore
import javax.inject.Inject
interface IncrementPushDataStore {
suspend fun incrementPushCounter()
}
@ContributesBinding(AppScope::class)
class DefaultIncrementPushDataStore @Inject constructor(
private val defaultPushDataStore: DefaultPushDataStore
) : IncrementPushDataStore {
override suspend fun incrementPushCounter() {
defaultPushDataStore.incrementPushCounter()
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.push
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import javax.inject.Inject
interface OnNotifiableEventReceived {
fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent)
}
@ContributesBinding(AppScope::class)
class DefaultOnNotifiableEventReceived @Inject constructor(
private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager,
) : OnNotifiableEventReceived {
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
}
}

View file

@ -18,7 +18,7 @@ package io.element.android.libraries.push.impl.pushgateway
import retrofit2.http.Body
import retrofit2.http.POST
internal interface PushGatewayAPI {
interface PushGatewayAPI {
/**
* Ask the Push Gateway to send a push to the current device.
*

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.pushgateway
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.network.RetrofitFactory
import javax.inject.Inject
interface PushGatewayApiFactory {
fun create(baseUrl: String): PushGatewayAPI
}
@ContributesBinding(AppScope::class)
class DefaultPushGatewayApiFactory @Inject constructor(
private val retrofitFactory: RetrofitFactory,
) : PushGatewayApiFactory {
override fun create(baseUrl: String): PushGatewayAPI {
return retrofitFactory.create(baseUrl)
.create(PushGatewayAPI::class.java)
}
}

View file

@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class PushGatewayDevice(
data class PushGatewayDevice(
/**
* Required. The app_id given when the pusher was created.
*/

View file

@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class PushGatewayNotification(
data class PushGatewayNotification(
@SerialName("event_id")
val eventId: String,
@SerialName("room_id")

View file

@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class PushGatewayNotifyBody(
data class PushGatewayNotifyBody(
/**
* Required. Information about the push notification
*/

View file

@ -15,15 +15,14 @@
*/
package io.element.android.libraries.push.impl.pushgateway
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.network.RetrofitFactory
import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import javax.inject.Inject
class PushGatewayNotifyRequest @Inject constructor(
private val retrofitFactory: RetrofitFactory,
) {
interface PushGatewayNotifyRequest {
data class Params(
val url: String,
val appId: String,
@ -32,13 +31,18 @@ class PushGatewayNotifyRequest @Inject constructor(
val roomId: RoomId,
)
suspend fun execute(params: Params) {
val sygnalApi = retrofitFactory.create(
suspend fun execute(params: Params)
}
@ContributesBinding(AppScope::class)
class DefaultPushGatewayNotifyRequest @Inject constructor(
private val pushGatewayApiFactory: PushGatewayApiFactory,
) : PushGatewayNotifyRequest {
override suspend fun execute(params: PushGatewayNotifyRequest.Params) {
val pushGatewayApi = pushGatewayApiFactory.create(
params.url.substringBefore(PushGatewayConfig.URI_PUSH_GATEWAY_PREFIX_PATH)
)
.create(PushGatewayAPI::class.java)
val response = sygnalApi.notify(
val response = pushGatewayApi.notify(
PushGatewayNotifyBody(
PushGatewayNotification(
eventId = params.eventId.value,

View file

@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class PushGatewayNotifyResponse(
data class PushGatewayNotifyResponse(
@SerialName("rejected")
val rejectedPushKeys: List<String>
)

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.test
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.PushConfig
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
import javax.inject.Inject
interface TestPush {
suspend fun execute(config: CurrentUserPushConfig)
}
@ContributesBinding(AppScope::class)
class DefaultTestPush @Inject constructor(
private val pushGatewayNotifyRequest: PushGatewayNotifyRequest,
) : TestPush {
override suspend fun execute(config: CurrentUserPushConfig) {
pushGatewayNotifyRequest.execute(
PushGatewayNotifyRequest.Params(
url = config.url,
appId = PushConfig.PUSHER_APP_ID,
pushKey = config.pushKey,
eventId = TEST_EVENT_ID,
roomId = TEST_ROOM_ID,
)
)
}
companion object {
val TEST_EVENT_ID = EventId("\$THIS_IS_A_FAKE_EVENT_ID")
val TEST_ROOM_ID = RoomId("!room:domain")
}
}

View file

@ -52,7 +52,6 @@
<item quantity="few">"%d пакоя"</item>
<item quantity="many">"%d пакояў"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Выберыце спосаб атрымання апавяшчэнняў"</string>
<string name="push_distributor_background_sync_android">"Фонавая сінхранізацыя"</string>
<string name="push_distributor_firebase_android">"Сэрвісы Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Службы Google Play не знойдзены. Апавяшчэнні могуць не працаваць належным чынам."</string>

View file

@ -52,7 +52,6 @@
<item quantity="few">"%d místnosti"</item>
<item quantity="other">"%d místností"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Vyberte, jak chcete přijímat oznámení"</string>
<string name="push_distributor_background_sync_android">"Synchronizace na pozadí"</string>
<string name="push_distributor_firebase_android">"Služby Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Nebyly nalezeny žádné funkční služby Google Play. Oznámení nemusí fungovat správně."</string>

View file

@ -46,7 +46,6 @@
<item quantity="one">"%d Raum"</item>
<item quantity="other">"%d Räume"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Wähle aus, wie du Benachrichtigungen erhalten möchtest"</string>
<string name="push_distributor_background_sync_android">"Hintergrundsynchronisation"</string>
<string name="push_distributor_firebase_android">"Google-Dienste"</string>
<string name="push_no_valid_google_play_services_apk_android">"Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig."</string>

View file

@ -46,7 +46,6 @@
<item quantity="one">"%d sala"</item>
<item quantity="other">"%d salas"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Elige cómo recibir las notificaciones"</string>
<string name="push_distributor_background_sync_android">"Sincronización en segundo plano"</string>
<string name="push_distributor_firebase_android">"Servicios de Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"No se han encontrado Servicios de Google Play válidos. Es posible que las notificaciones no funcionen correctamente."</string>

View file

@ -46,7 +46,6 @@
<item quantity="one">"%d salon"</item>
<item quantity="other">"%d salons"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Choisissez le mode de réception des notifications"</string>
<string name="push_distributor_background_sync_android">"Synchronisation en arrière-plan"</string>
<string name="push_distributor_firebase_android">"Services Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Aucun service Google Play valide na été trouvé. Les notifications peuvent ne pas fonctionner correctement."</string>

View file

@ -46,7 +46,6 @@
<item quantity="one">"%d szoba"</item>
<item quantity="other">"%d szoba"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Válassza ki az értesítések fogadási módját"</string>
<string name="push_distributor_background_sync_android">"Háttérszinkronizálás"</string>
<string name="push_distributor_firebase_android">"Google szolgáltatások"</string>
<string name="push_no_valid_google_play_services_apk_android">"A Google Play szolgáltatások nem találhatók. Előfordulhat, hogy az értesítések nem működnek megfelelően."</string>

View file

@ -40,7 +40,6 @@
<plurals name="notification_unread_notified_messages_in_room_rooms">
<item quantity="other">"%d ruangan"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Pilih cara menerima notifikasi"</string>
<string name="push_distributor_background_sync_android">"Sinkronisasi latar belakang"</string>
<string name="push_distributor_firebase_android">"Layanan Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Tidak ditemukan Layanan Google Play yang valid. Pemberitahuan mungkin tidak berfungsi dengan baik."</string>

View file

@ -46,7 +46,6 @@
<item quantity="one">"%d stanza"</item>
<item quantity="other">"%d stanze"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Scegli come ricevere le notifiche"</string>
<string name="push_distributor_background_sync_android">"Sincronizzazione in background"</string>
<string name="push_distributor_firebase_android">"Servizi Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Google Play Services non trovato. Le notifiche non funzioneranno bene."</string>

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_call">"ზარი"</string>
<string name="notification_channel_listening_for_events">"მოვლენებისთვის მოსმენა"</string>
<string name="notification_channel_noisy">"ხმაურიანი შეტყობინებები"</string>
<string name="notification_channel_silent">"ჩუმი შეტყობინებები"</string>
<plurals name="notification_compat_summary_line_for_room">
<item quantity="one">"%1$s: %2$d შეტყობინება"</item>
<item quantity="other">"%1$s: %2$d შეტყობინება"</item>
</plurals>
<plurals name="notification_compat_summary_title">
<item quantity="one">"%d შეტყობინება"</item>
<item quantity="other">"%d შეტყობინება"</item>
</plurals>
<string name="notification_fallback_content">"შეტყობინება"</string>
<string name="notification_inline_reply_failed">"** გაგზავნა ვერ მოხერხდა - გთხოვთ, გახსნათ ოთახი"</string>
<string name="notification_invitation_action_join">"გაწევრიანება"</string>
<string name="notification_invitation_action_reject">"უარყოფა"</string>
<plurals name="notification_invitations">
<item quantity="one">"%d მოწვევა"</item>
<item quantity="other">"%d მოწვევები"</item>
</plurals>
<string name="notification_invite_body">"მოგიწვიათ ჩატში"</string>
<string name="notification_new_messages">"ახალი შეტყობინებები"</string>
<plurals name="notification_new_messages_for_room">
<item quantity="one">"%d ახალი მესიჯი"</item>
<item quantity="other">"%d ახალი მესიჯი"</item>
</plurals>
<string name="notification_reaction_body">"რეაგირება მოხდა: %1$s"</string>
<string name="notification_room_action_quick_reply">"Სწრაფი პასუხი"</string>
<string name="notification_room_invite_body">"მოგიწვიათ ოთახში"</string>
<string name="notification_sender_me">"მე"</string>
<string name="notification_test_push_notification_content">"თქვენ ხედავთ შეტყობინებას! დამაწკაპუნეთ!"</string>
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
<plurals name="notification_unread_notified_messages">
<item quantity="one">"%d წაუკითხავი შეტყობინება"</item>
<item quantity="other">"%d წაუკითხავი შეტყობინება"</item>
</plurals>
<string name="notification_unread_notified_messages_and_invitation">"%1$s და %2$s"</string>
<string name="notification_unread_notified_messages_in_room">"%1$s %2$s-ში"</string>
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s %2$s-ში და %3$s"</string>
<plurals name="notification_unread_notified_messages_in_room_rooms">
<item quantity="one">"%d ოთახი"</item>
<item quantity="other">"%d ოთახი"</item>
</plurals>
<string name="push_distributor_background_sync_android">"ფონის სინქრონიზაცია"</string>
<string name="push_distributor_firebase_android">"Google სერვისები"</string>
<string name="push_no_valid_google_play_services_apk_android">"მოქმედი Google Play სერვისები ვერ მოიძებნა. შეტყობინებები შეიძლება ვერ იმუშაოს სწორად."</string>
</resources>

View file

@ -46,7 +46,6 @@
<item quantity="one">"%d sala"</item>
<item quantity="other">"%d salas"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Escolhe como receber notificações"</string>
<string name="push_distributor_background_sync_android">"Sincronização em segundo plano"</string>
<string name="push_distributor_firebase_android">"Serviços do Google Play"</string>
<string name="push_no_valid_google_play_services_apk_android">"Nenhuns Serviços do Google Play válidos encontrados. As notificações poderão não funcionar devidamente."</string>

View file

@ -46,7 +46,6 @@
<item quantity="one">"%d cameră"</item>
<item quantity="other">"%d camere"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Alegeți modul de primire a notificărilor"</string>
<string name="push_distributor_background_sync_android">"Sincronizare în fundal"</string>
<string name="push_distributor_firebase_android">"Servicii Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Nu au fost găsite servicii Google Play valide. Este posibil ca notificările să nu funcționeze corect."</string>
@ -58,6 +57,7 @@
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Nu s-au găsit furnizori push."</string>
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
<item quantity="one">"S-a găsit %1$d furnizor push: %2$s"</item>
<item quantity="few">"S-au găsit %1$d furnizori push: %2$s"</item>
<item quantity="other">"S-au găsit %1$d furnizori push: %2$s"</item>
</plurals>
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Detectați furnizorii push"</string>

View file

@ -52,7 +52,6 @@
<item quantity="few">"%d комнаты"</item>
<item quantity="many">"%d комнат"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Выберите способ получения уведомлений"</string>
<string name="push_distributor_background_sync_android">"Фоновая синхронизация"</string>
<string name="push_distributor_firebase_android">"Сервисы Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Не найдены действующие службы Google Play. Уведомления могут работать некорректно."</string>

View file

@ -52,7 +52,6 @@
<item quantity="few">"%d miestnosti"</item>
<item quantity="other">"%d miestností"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Vyberte spôsob prijímania oznámení"</string>
<string name="push_distributor_background_sync_android">"Synchronizácia na pozadí"</string>
<string name="push_distributor_firebase_android">"Služby Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Nenašli sa žiadne platné služby Google Play. Oznámenia nemusia fungovať správne."</string>

View file

@ -45,7 +45,6 @@
<item quantity="one">"%d rum"</item>
<item quantity="other">"%d rum"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Välj hur du vill ta emot aviseringar"</string>
<string name="push_distributor_background_sync_android">"Bakgrundssynkronisering"</string>
<string name="push_distributor_firebase_android">"Google-tjänster"</string>
<string name="push_no_valid_google_play_services_apk_android">"Inga giltiga Google Play-tjänster hittades. Aviseringar kanske inte fungerar korrekt."</string>

View file

@ -52,7 +52,6 @@
<item quantity="few">"%d кімнати"</item>
<item quantity="many">"%d кімнат"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Виберіть спосіб отримання сповіщень"</string>
<string name="push_distributor_background_sync_android">"Фонова синхронізація"</string>
<string name="push_distributor_firebase_android">"Сервіси Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Не знайдено дійсних сервісів Google Play. Сповіщення можуть не працювати належним чином."</string>

View file

@ -22,6 +22,7 @@
<item quantity="other">"%d 則新訊息"</item>
</plurals>
<string name="notification_reaction_body">"回應 %1$s"</string>
<string name="notification_room_action_mark_as_read">"標為已讀"</string>
<string name="notification_room_action_quick_reply">"快速回覆"</string>
<string name="notification_room_invite_body">"邀請您加入聊天室"</string>
<string name="notification_sender_me">"我"</string>
@ -31,7 +32,6 @@
<plurals name="notification_unread_notified_messages_in_room_rooms">
<item quantity="other">"%d 個聊天室"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"選擇接收通知的機制"</string>
<string name="push_distributor_background_sync_android">"背景同步"</string>
<string name="push_distributor_firebase_android">"Google 服務"</string>
</resources>

View file

@ -40,7 +40,6 @@
<plurals name="notification_unread_notified_messages_in_room_rooms">
<item quantity="other">"%d 个房间"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"选择如何接收通知"</string>
<string name="push_distributor_background_sync_android">"后台同步"</string>
<string name="push_distributor_firebase_android">"谷歌服务"</string>
<string name="push_no_valid_google_play_services_apk_android">"找不到有效的 Google Play 服务。通知可能无法正常工作。"</string>

View file

@ -46,7 +46,6 @@
<item quantity="one">"%d room"</item>
<item quantity="other">"%d rooms"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Choose how to receive notifications"</string>
<string name="push_distributor_background_sync_android">"Background synchronization"</string>
<string name="push_distributor_firebase_android">"Google Services"</string>
<string name="push_no_valid_google_play_services_apk_android">"No valid Google Play Services found. Notifications may not work properly."</string>

View file

@ -0,0 +1,221 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.push.api.GetCurrentPushProvider
import io.element.android.libraries.push.impl.test.FakeTestPush
import io.element.android.libraries.push.impl.test.TestPush
import io.element.android.libraries.push.test.FakeGetCurrentPushProvider
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.libraries.pushproviders.test.FakePushProvider
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultPushServiceTest {
@Test
fun `test push no push provider`() = runTest {
val defaultPushService = createDefaultPushService()
assertThat(defaultPushService.testPush()).isFalse()
}
@Test
fun `test push no config`() = runTest {
val aPushProvider = FakePushProvider()
val defaultPushService = createDefaultPushService(
pushProviders = setOf(aPushProvider),
getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = aPushProvider.name),
)
assertThat(defaultPushService.testPush()).isFalse()
}
@Test
fun `test push ok`() = runTest {
val aConfig = CurrentUserPushConfig(
url = "aUrl",
pushKey = "aPushKey",
)
val testPushResult = lambdaRecorder<CurrentUserPushConfig, Unit> { }
val aPushProvider = FakePushProvider(
currentUserPushConfig = aConfig
)
val defaultPushService = createDefaultPushService(
pushProviders = setOf(aPushProvider),
getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = aPushProvider.name),
testPush = FakeTestPush(executeResult = testPushResult),
)
assertThat(defaultPushService.testPush()).isTrue()
testPushResult.assertions()
.isCalledOnce()
.with(value(aConfig))
}
@Test
fun `getCurrentPushProvider null`() = runTest {
val defaultPushService = createDefaultPushService()
val result = defaultPushService.getCurrentPushProvider()
assertThat(result).isNull()
}
@Test
fun `getCurrentPushProvider ok`() = runTest {
val aPushProvider = FakePushProvider()
val defaultPushService = createDefaultPushService(
pushProviders = setOf(aPushProvider),
getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = aPushProvider.name),
)
val result = defaultPushService.getCurrentPushProvider()
assertThat(result).isEqualTo(aPushProvider)
}
@Test
fun `getAvailablePushProviders empty`() = runTest {
val defaultPushService = createDefaultPushService()
val result = defaultPushService.getAvailablePushProviders()
assertThat(result).isEmpty()
}
@Test
fun `registerWith ok`() = runTest {
val client = FakeMatrixClient()
val aPushProvider = FakePushProvider(
registerWithResult = { _, _ -> Result.success(Unit) },
)
val aDistributor = Distributor("aValue", "aName")
val defaultPushService = createDefaultPushService()
val result = defaultPushService.registerWith(client, aPushProvider, aDistributor)
assertThat(result).isEqualTo(Result.success(Unit))
}
@Test
fun `registerWith fail to register`() = runTest {
val client = FakeMatrixClient()
val aPushProvider = FakePushProvider(
registerWithResult = { _, _ -> Result.failure(AN_EXCEPTION) },
)
val aDistributor = Distributor("aValue", "aName")
val defaultPushService = createDefaultPushService()
val result = defaultPushService.registerWith(client, aPushProvider, aDistributor)
assertThat(result.isFailure).isTrue()
}
@Test
fun `registerWith fail to unregister previous push provider`() = runTest {
val client = FakeMatrixClient()
val aCurrentPushProvider = FakePushProvider(
unregisterWithResult = { Result.failure(AN_EXCEPTION) },
name = "aCurrentPushProvider",
)
val aPushProvider = FakePushProvider(
name = "aPushProvider",
)
val userPushStore = FakeUserPushStore().apply {
setPushProviderName(aCurrentPushProvider.name)
}
val aDistributor = Distributor("aValue", "aName")
val defaultPushService = createDefaultPushService(
pushProviders = setOf(aCurrentPushProvider, aPushProvider),
getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = aCurrentPushProvider.name),
userPushStoreFactory = FakeUserPushStoreFactory(
userPushStore = { userPushStore },
),
)
val result = defaultPushService.registerWith(client, aPushProvider, aDistributor)
assertThat(result.isFailure).isTrue()
assertThat(userPushStore.getPushProviderName()).isEqualTo(aCurrentPushProvider.name)
}
@Test
fun `registerWith unregister previous push provider and register new OK`() = runTest {
val client = FakeMatrixClient()
val unregisterLambda = lambdaRecorder<MatrixClient, Result<Unit>> { Result.success(Unit) }
val registerLambda = lambdaRecorder<MatrixClient, Distributor, Result<Unit>> { _, _ -> Result.success(Unit) }
val aCurrentPushProvider = FakePushProvider(
unregisterWithResult = unregisterLambda,
name = "aCurrentPushProvider",
)
val aPushProvider = FakePushProvider(
registerWithResult = registerLambda,
name = "aPushProvider",
)
val userPushStore = FakeUserPushStore().apply {
setPushProviderName(aCurrentPushProvider.name)
}
val aDistributor = Distributor("aValue", "aName")
val defaultPushService = createDefaultPushService(
pushProviders = setOf(aCurrentPushProvider, aPushProvider),
getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = aCurrentPushProvider.name),
userPushStoreFactory = FakeUserPushStoreFactory(
userPushStore = { userPushStore },
),
)
val result = defaultPushService.registerWith(client, aPushProvider, aDistributor)
assertThat(result.isSuccess).isTrue()
assertThat(userPushStore.getPushProviderName()).isEqualTo(aPushProvider.name)
unregisterLambda.assertions()
.isCalledOnce()
.with(value(client))
registerLambda.assertions()
.isCalledOnce()
.with(value(client), value(aDistributor))
}
@Test
fun `getAvailablePushProviders sorted`() = runTest {
val aPushProvider1 = FakePushProvider(
index = 1,
name = "aPushProvider1",
)
val aPushProvider2 = FakePushProvider(
index = 2,
name = "aPushProvider2",
)
val aPushProvider3 = FakePushProvider(
index = 3,
name = "aPushProvider3",
)
val defaultPushService = createDefaultPushService(
pushProviders = setOf(aPushProvider1, aPushProvider3, aPushProvider2),
)
val result = defaultPushService.getAvailablePushProviders()
assertThat(result).containsExactly(aPushProvider1, aPushProvider2, aPushProvider3).inOrder()
}
private fun createDefaultPushService(
testPush: TestPush = FakeTestPush(),
userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(),
pushProviders: Set<@JvmSuppressWildcards PushProvider> = emptySet(),
getCurrentPushProvider: GetCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = null),
): DefaultPushService {
return DefaultPushService(
testPush = testPush,
userPushStoreFactory = userPushStoreFactory,
pushProviders = pushProviders,
getCurrentPushProvider = getCurrentPushProvider,
)
}
}

View file

@ -0,0 +1,193 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.PushConfig
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_SECRET
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.pushers.FakePushersService
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultPusherSubscriberTest {
@Test
fun `test register pusher OK`() = runTest {
testRegisterPusher(
currentPushKey = null,
registerResult = Result.success(Unit),
)
}
@Test
fun `test re-register pusher OK`() = runTest {
testRegisterPusher(
currentPushKey = "aPushKey",
registerResult = Result.success(Unit),
)
}
@Test
fun `test register pusher error`() = runTest {
testRegisterPusher(
currentPushKey = null,
registerResult = Result.failure(AN_EXCEPTION),
)
}
@Test
fun `test re-register pusher error`() = runTest {
testRegisterPusher(
currentPushKey = "aPushKey",
registerResult = Result.failure(AN_EXCEPTION),
)
}
private suspend fun testRegisterPusher(
currentPushKey: String?,
registerResult: Result<Unit>,
) {
val setHttpPusherResult = lambdaRecorder<SetHttpPusherData, Result<Unit>> { registerResult }
val userPushStore = FakeUserPushStore().apply {
setCurrentRegisteredPushKey(currentPushKey)
}
assertThat(userPushStore.getCurrentRegisteredPushKey()).isEqualTo(currentPushKey)
val matrixClient = FakeMatrixClient(
pushersService = FakePushersService(
setHttpPusherResult = setHttpPusherResult,
),
)
val defaultPusherSubscriber = createDefaultPusherSubscriber(
pushClientSecret = FakePushClientSecret(
getSecretForUserResult = { A_SECRET },
),
userPushStoreFactory = FakeUserPushStoreFactory(
userPushStore = { userPushStore },
),
)
val result = defaultPusherSubscriber.registerPusher(
matrixClient = matrixClient,
pushKey = "aPushKey",
gateway = "aGateway",
)
assertThat(result).isEqualTo(registerResult)
setHttpPusherResult.assertions()
.isCalledOnce()
.with(
value(
SetHttpPusherData(
pushKey = "aPushKey",
appId = PushConfig.PUSHER_APP_ID,
url = "aGateway",
appDisplayName = "MyApp",
deviceDisplayName = "MyDevice",
profileTag = DEFAULT_PUSHER_FILE_TAG + "_",
lang = "en",
defaultPayload = "{\"cs\":\"$A_SECRET\"}",
),
)
)
assertThat(userPushStore.getCurrentRegisteredPushKey()).isEqualTo(
if (registerResult.isSuccess) "aPushKey" else currentPushKey
)
}
@Test
fun `test unregister pusher OK`() = runTest {
testUnregisterPusher(
currentPushKey = "aPushKey",
unregisterResult = Result.success(Unit),
)
}
@Test
fun `test unregister pusher error`() = runTest {
testUnregisterPusher(
currentPushKey = "aPushKey",
unregisterResult = Result.failure(AN_EXCEPTION),
)
}
private suspend fun testUnregisterPusher(
currentPushKey: String?,
unregisterResult: Result<Unit>,
) {
val unsetHttpPusherResult = lambdaRecorder<UnsetHttpPusherData, Result<Unit>> { unregisterResult }
val userPushStore = FakeUserPushStore().apply {
setCurrentRegisteredPushKey(currentPushKey)
}
assertThat(userPushStore.getCurrentRegisteredPushKey()).isEqualTo(currentPushKey)
val matrixClient = FakeMatrixClient(
pushersService = FakePushersService(
unsetHttpPusherResult = unsetHttpPusherResult,
),
)
val defaultPusherSubscriber = createDefaultPusherSubscriber(
pushClientSecret = FakePushClientSecret(
getSecretForUserResult = { A_SECRET },
),
userPushStoreFactory = FakeUserPushStoreFactory(
userPushStore = { userPushStore },
),
)
val result = defaultPusherSubscriber.unregisterPusher(
matrixClient = matrixClient,
pushKey = "aPushKey",
gateway = "aGateway",
)
assertThat(result).isEqualTo(unregisterResult)
unsetHttpPusherResult.assertions()
.isCalledOnce()
.with(
value(
UnsetHttpPusherData(
pushKey = "aPushKey",
appId = PushConfig.PUSHER_APP_ID,
),
)
)
assertThat(userPushStore.getCurrentRegisteredPushKey()).isEqualTo(
if (unregisterResult.isSuccess) null else currentPushKey
)
}
private fun createDefaultPusherSubscriber(
buildMeta: BuildMeta = aBuildMeta(applicationName = "MyApp"),
userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(),
pushClientSecret: PushClientSecret = FakePushClientSecret(),
): DefaultPusherSubscriber {
return DefaultPusherSubscriber(
buildMeta = buildMeta,
pushClientSecret = pushClientSecret,
userPushStoreFactory = userPushStoreFactory,
)
}
}

View file

@ -59,17 +59,17 @@ import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
class NotifiableEventResolverTest {
class DefaultNotifiableEventResolverTest {
@Test
fun `resolve event no session`() = runTest {
val sut = createNotifiableEventResolver(notificationService = null)
val sut = createDefaultNotifiableEventResolver(notificationService = null)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
assertThat(result).isNull()
}
@Test
fun `resolve event failure`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.failure(AN_EXCEPTION)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
@ -78,7 +78,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve event null`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(null)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
@ -87,7 +87,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve event message text`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -105,7 +105,7 @@ class NotifiableEventResolverTest {
@Test
@Config(qualifiers = "en")
fun `resolve event message with mention`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -123,7 +123,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve HTML formatted event message text takes plain text version`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -146,7 +146,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve incorrectly formatted event message text uses fallback`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -169,7 +169,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve event message audio`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -186,7 +186,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve event message video`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -203,7 +203,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve event message voice`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -220,7 +220,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve event message image`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -237,7 +237,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve event message sticker`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -254,7 +254,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve event message file`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -271,7 +271,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve event message location`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -288,7 +288,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve event message notice`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -305,7 +305,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve event message emote`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
@ -322,7 +322,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve poll`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.Poll(
@ -339,7 +339,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve RoomMemberContent invite room`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.StateEvent.RoomMemberContent(
@ -372,7 +372,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve RoomMemberContent invite direct`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.StateEvent.RoomMemberContent(
@ -405,7 +405,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve RoomMemberContent other`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.StateEvent.RoomMemberContent(
@ -421,7 +421,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve RoomEncrypted`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomEncrypted
@ -445,7 +445,7 @@ class NotifiableEventResolverTest {
@Test
fun `resolve CallInvite`() = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.CallInvite(A_USER_ID_2)
@ -517,7 +517,7 @@ class NotifiableEventResolverTest {
}
private fun testNull(content: NotificationContent) = runTest {
val sut = createNotifiableEventResolver(
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = content
@ -528,10 +528,10 @@ class NotifiableEventResolverTest {
assertThat(result).isNull()
}
private fun createNotifiableEventResolver(
private fun createDefaultNotifiableEventResolver(
notificationService: FakeNotificationService? = FakeNotificationService(),
notificationResult: Result<NotificationData?> = Result.success(null),
): NotifiableEventResolver {
): DefaultNotifiableEventResolver {
val context = RuntimeEnvironment.getApplication() as Context
notificationService?.givenGetNotificationResult(notificationResult)
val matrixClientProvider = FakeMatrixClientProvider(getClient = {
@ -544,7 +544,7 @@ class NotifiableEventResolverTest {
val notificationMediaRepoFactory = NotificationMediaRepo.Factory {
FakeNotificationMediaRepo()
}
return NotifiableEventResolver(
return DefaultNotifiableEventResolver(
stringProvider = AndroidStringProvider(context.resources),
clock = FakeSystemClock(),
matrixClientProvider = matrixClientProvider,

View file

@ -59,7 +59,6 @@ class DefaultNotificationDrawerManagerTest {
fun `cover all APIs`() = runTest {
// For now just call all the API. Later, add more valuable tests.
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager()
defaultNotificationDrawerManager.notificationStyleChanged()
defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = true)
defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = false)
defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = true)

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications
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.push.impl.notifications.model.NotifiableEvent
import io.element.android.tests.testutils.lambda.lambdaError
class FakeNotifiableEventResolver(
private val notifiableEventResult: (SessionId, RoomId, EventId) -> NotifiableEvent? = { _, _, _ -> lambdaError() }
) : NotifiableEventResolver {
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
return notifiableEventResult(sessionId, roomId, eventId)
}
}

View file

@ -0,0 +1,268 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.libraries.push.impl.push
import app.cash.turbine.test
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
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.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SECRET
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.test.DefaultTestPush
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
import io.element.android.libraries.pushproviders.api.PushData
import io.element.android.libraries.pushstore.api.UserPushStore
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultPushHandlerTest {
@Test
fun `when classical PushData is received, the notification drawer is informed`() = runTest {
val aNotifiableMessageEvent = aNotifiableMessageEvent()
val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData(
eventId = AN_EVENT_ID,
roomId = A_ROOM_ID,
unread = 0,
clientSecret = A_SECRET,
)
val defaultPushHandler = createDefaultPushHandler(
onNotifiableEventReceived = onNotifiableEventReceived,
notifiableEventResult = notifiableEventResult,
pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID }
),
incrementPushCounterResult = incrementPushCounterResult
)
defaultPushHandler.handle(aPushData)
incrementPushCounterResult.assertions()
.isCalledOnce()
notifiableEventResult.assertions()
.isCalledOnce()
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID))
onNotifiableEventReceived.assertions()
.isCalledOnce()
.with(value(aNotifiableMessageEvent))
}
@Test
fun `when classical PushData is received, but notifications are disabled, nothing happen`() =
runTest {
val aNotifiableMessageEvent = aNotifiableMessageEvent()
val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData(
eventId = AN_EVENT_ID,
roomId = A_ROOM_ID,
unread = 0,
clientSecret = A_SECRET,
)
val defaultPushHandler = createDefaultPushHandler(
onNotifiableEventReceived = onNotifiableEventReceived,
notifiableEventResult = notifiableEventResult,
pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID }
),
userPushStore = FakeUserPushStore().apply {
setNotificationEnabledForDevice(false)
},
incrementPushCounterResult = incrementPushCounterResult
)
defaultPushHandler.handle(aPushData)
incrementPushCounterResult.assertions()
.isCalledOnce()
notifiableEventResult.assertions()
.isNeverCalled()
onNotifiableEventReceived.assertions()
.isNeverCalled()
}
@Test
fun `when PushData is received, but client secret is not known, fallback the latest session`() =
runTest {
val aNotifiableMessageEvent = aNotifiableMessageEvent()
val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData(
eventId = AN_EVENT_ID,
roomId = A_ROOM_ID,
unread = 0,
clientSecret = A_SECRET,
)
val defaultPushHandler = createDefaultPushHandler(
onNotifiableEventReceived = onNotifiableEventReceived,
notifiableEventResult = notifiableEventResult,
pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { null }
),
matrixAuthenticationService = FakeMatrixAuthenticationService().apply {
getLatestSessionIdLambda = { A_USER_ID }
},
incrementPushCounterResult = incrementPushCounterResult
)
defaultPushHandler.handle(aPushData)
incrementPushCounterResult.assertions()
.isCalledOnce()
notifiableEventResult.assertions()
.isCalledOnce()
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID))
onNotifiableEventReceived.assertions()
.isCalledOnce()
.with(value(aNotifiableMessageEvent))
}
@Test
fun `when PushData is received, but client secret is not known, and there is no latest session, nothing happen`() =
runTest {
val aNotifiableMessageEvent = aNotifiableMessageEvent()
val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData(
eventId = AN_EVENT_ID,
roomId = A_ROOM_ID,
unread = 0,
clientSecret = A_SECRET,
)
val defaultPushHandler = createDefaultPushHandler(
onNotifiableEventReceived = onNotifiableEventReceived,
notifiableEventResult = notifiableEventResult,
pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { null }
),
matrixAuthenticationService = FakeMatrixAuthenticationService().apply {
getLatestSessionIdLambda = { null }
},
incrementPushCounterResult = incrementPushCounterResult
)
defaultPushHandler.handle(aPushData)
incrementPushCounterResult.assertions()
.isCalledOnce()
notifiableEventResult.assertions()
.isNeverCalled()
onNotifiableEventReceived.assertions()
.isNeverCalled()
}
@Test
fun `when classical PushData is received, but not able to resolve the event, nothing happen`() =
runTest {
val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent?> { _, _, _ -> null }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData(
eventId = AN_EVENT_ID,
roomId = A_ROOM_ID,
unread = 0,
clientSecret = A_SECRET,
)
val defaultPushHandler = createDefaultPushHandler(
onNotifiableEventReceived = onNotifiableEventReceived,
notifiableEventResult = notifiableEventResult,
buildMeta = aBuildMeta(
// Also test `lowPrivacyLoggingEnabled = false` here
lowPrivacyLoggingEnabled = false
),
pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID }
),
incrementPushCounterResult = incrementPushCounterResult
)
defaultPushHandler.handle(aPushData)
incrementPushCounterResult.assertions()
.isCalledOnce()
notifiableEventResult.assertions()
.isCalledOnce()
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID))
onNotifiableEventReceived.assertions()
.isNeverCalled()
}
@Test
fun `when diagnostic PushData is received, the diagnostic push handler is informed `() =
runTest {
val aPushData = PushData(
eventId = DefaultTestPush.TEST_EVENT_ID,
roomId = A_ROOM_ID,
unread = 0,
clientSecret = A_SECRET,
)
val diagnosticPushHandler = DiagnosticPushHandler()
val defaultPushHandler = createDefaultPushHandler(
diagnosticPushHandler = diagnosticPushHandler,
incrementPushCounterResult = { }
)
diagnosticPushHandler.state.test {
defaultPushHandler.handle(aPushData)
awaitItem()
}
}
private fun createDefaultPushHandler(
onNotifiableEventReceived: (NotifiableEvent) -> Unit = { lambdaError() },
notifiableEventResult: (SessionId, RoomId, EventId) -> NotifiableEvent? = { _, _, _ -> lambdaError() },
incrementPushCounterResult: () -> Unit = { lambdaError() },
userPushStore: UserPushStore = FakeUserPushStore(),
pushClientSecret: PushClientSecret = FakePushClientSecret(),
buildMeta: BuildMeta = aBuildMeta(),
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
diagnosticPushHandler: DiagnosticPushHandler = DiagnosticPushHandler(),
): DefaultPushHandler {
return DefaultPushHandler(
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived),
notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventResult),
incrementPushDataStore = object : IncrementPushDataStore {
override suspend fun incrementPushCounter() {
incrementPushCounterResult()
}
},
userPushStoreFactory = FakeUserPushStoreFactory { userPushStore },
pushClientSecret = pushClientSecret,
buildMeta = buildMeta,
matrixAuthenticationService = matrixAuthenticationService,
diagnosticPushHandler = diagnosticPushHandler,
)
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.push
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
class FakeOnNotifiableEventReceived(
private val onNotifiableEventReceivedResult: (NotifiableEvent) -> Unit,
) : OnNotifiableEventReceived {
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
onNotifiableEventReceivedResult(notifiableEvent)
}
}

View file

@ -0,0 +1,102 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.pushgateway
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import io.element.android.libraries.push.impl.test.DefaultTestPush
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows
import org.junit.Test
class DefaultPushGatewayNotifyRequestTest {
@Test
fun `notify success`() = runTest {
val factory = FakePushGatewayApiFactory(
notifyResponse = {
PushGatewayNotifyResponse(
rejectedPushKeys = emptyList()
)
}
)
val pushGatewayNotifyRequest = DefaultPushGatewayNotifyRequest(
pushGatewayApiFactory = factory,
)
pushGatewayNotifyRequest.execute(
PushGatewayNotifyRequest.Params(
url = "aUrl",
appId = "anAppId",
pushKey = "aPushKey",
eventId = DefaultTestPush.TEST_EVENT_ID,
roomId = DefaultTestPush.TEST_ROOM_ID,
)
)
assertThat(factory.baseUrlParameter).isEqualTo("aUrl")
}
@Test
fun `notify success, url is stripped`() = runTest {
val factory = FakePushGatewayApiFactory(
notifyResponse = {
PushGatewayNotifyResponse(
rejectedPushKeys = emptyList()
)
}
)
val pushGatewayNotifyRequest = DefaultPushGatewayNotifyRequest(
pushGatewayApiFactory = factory,
)
pushGatewayNotifyRequest.execute(
PushGatewayNotifyRequest.Params(
url = "aUrl" + PushGatewayConfig.URI_PUSH_GATEWAY_PREFIX_PATH,
appId = "anAppId",
pushKey = "aPushKey",
eventId = DefaultTestPush.TEST_EVENT_ID,
roomId = DefaultTestPush.TEST_ROOM_ID,
)
)
assertThat(factory.baseUrlParameter).isEqualTo("aUrl")
}
@Test
fun `notify with rejected push key should throw expected Exception`() {
val factory = FakePushGatewayApiFactory(
notifyResponse = {
PushGatewayNotifyResponse(
rejectedPushKeys = listOf("aPushKey")
)
}
)
val pushGatewayNotifyRequest = DefaultPushGatewayNotifyRequest(
pushGatewayApiFactory = factory,
)
assertThrows(PushGatewayFailure.PusherRejected::class.java) {
runTest {
pushGatewayNotifyRequest.execute(
PushGatewayNotifyRequest.Params(
url = "aUrl",
appId = "anAppId",
pushKey = "aPushKey",
eventId = DefaultTestPush.TEST_EVENT_ID,
roomId = DefaultTestPush.TEST_ROOM_ID,
)
)
}
}
assertThat(factory.baseUrlParameter).isEqualTo("aUrl")
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.pushgateway
class FakePushGatewayApiFactory(
private val notifyResponse: () -> PushGatewayNotifyResponse
) : PushGatewayApiFactory {
var baseUrlParameter: String? = null
private set
override fun create(baseUrl: String): PushGatewayAPI {
baseUrlParameter = baseUrl
return FakePushGatewayAPI(notifyResponse)
}
}
class FakePushGatewayAPI(
private val notifyResponse: () -> PushGatewayNotifyResponse
) : PushGatewayAPI {
override suspend fun notify(body: PushGatewayNotifyBody): PushGatewayNotifyResponse {
return notifyResponse()
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.test
import io.element.android.appconfig.PushConfig
import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultTestPushTest {
@Test
fun `test DefaultTestPush`() = runTest {
val executeResult = lambdaRecorder<PushGatewayNotifyRequest.Params, Unit> { }
val defaultTestPush = DefaultTestPush(
pushGatewayNotifyRequest = FakePushGatewayNotifyRequest(
executeResult = executeResult,
)
)
val aConfig = CurrentUserPushConfig(
url = "aUrl",
pushKey = "aPushKey",
)
defaultTestPush.execute(aConfig)
executeResult.assertions()
.isCalledOnce()
.with(
value(
PushGatewayNotifyRequest.Params(
url = aConfig.url,
appId = PushConfig.PUSHER_APP_ID,
pushKey = aConfig.pushKey,
eventId = DefaultTestPush.TEST_EVENT_ID,
roomId = DefaultTestPush.TEST_ROOM_ID,
)
)
)
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.test
import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest
import io.element.android.tests.testutils.lambda.lambdaError
class FakePushGatewayNotifyRequest(
private val executeResult: (PushGatewayNotifyRequest.Params) -> Unit = { lambdaError() }
) : PushGatewayNotifyRequest {
override suspend fun execute(params: PushGatewayNotifyRequest.Params) {
executeResult(params)
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.test
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
import io.element.android.tests.testutils.lambda.lambdaError
class FakeTestPush(
private val executeResult: (CurrentUserPushConfig) -> Unit = { lambdaError() }
) : TestPush {
override suspend fun execute(config: CurrentUserPushConfig) {
executeResult(config)
}
}

Some files were not shown because too many files have changed in this diff Show more