Merge branch 'develop' into feature/fga/pdf_renderer
This commit is contained in:
commit
26adc55ea9
435 changed files with 6832 additions and 1041 deletions
|
|
@ -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">"Nebyla nalezena žádná kompatibilní aplikace, která by tuto akci zpracovala."</string>
|
||||
</resources>
|
||||
|
|
@ -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">"Keine kompatible App für diese Aktion gefunden."</string>
|
||||
</resources>
|
||||
|
|
@ -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">"Aucune application compatible n\'a été trouvée pour gérer cette action."</string>
|
||||
</resources>
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.core.extensions
|
||||
|
||||
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
||||
fun Boolean.to01() = if (this) "1" else "0"
|
||||
|
||||
inline fun <T> T.ooi(block: (T) -> Unit): T = also(block)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ dependencies {
|
|||
implementation(libs.dagger)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
|
|
|
|||
|
|
@ -79,3 +79,6 @@ val Compound_Gray_300_Light = Color(0xFFF0F2F5)
|
|||
val Compound_Gray_300_Dark = Color(0xFF1D1F24)
|
||||
val Compound_Gray_400_Light = Color(0xFFE1E6EC)
|
||||
val Compound_Gray_400_Dark = Color(0xFF26282D)
|
||||
|
||||
val Gray_1400_Light = Color(0xFF1B1D22)
|
||||
val Gray_1400_Dark = Color(0xFFEBEEF2)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.designsystem.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||
|
||||
@Composable
|
||||
fun LabelledTextField(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier,
|
||||
placeholder: String? = null,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
singleLine: Boolean = false,
|
||||
onValueChange: (String) -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
text = label
|
||||
)
|
||||
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = value,
|
||||
placeholder = placeholder?.let { { Text(placeholder) } },
|
||||
onValueChange = onValueChange,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LabelledTextFieldLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LabelledTextFieldDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Column {
|
||||
LabelledTextField(
|
||||
label = "Room name",
|
||||
value = "",
|
||||
placeholder = "e.g. Product Sprint",
|
||||
)
|
||||
LabelledTextField(
|
||||
label = "Room name",
|
||||
value = "a room name",
|
||||
placeholder = "e.g. Product Sprint",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.designsystem.components.async
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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 androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
fun AsyncFailure(
|
||||
throwable: Throwable,
|
||||
onRetry: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(text = throwable.message ?: "An error occurred")
|
||||
if (onRetry != null) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Button(onClick = onRetry) {
|
||||
Text(text = "Retry")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun AsyncFailurePreviewLight() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun AsyncFailurePreviewDark() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
AsyncFailure(
|
||||
throwable = IllegalStateException("An error occurred"),
|
||||
onRetry = {}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.designsystem.components.async
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
|
||||
@Composable
|
||||
fun AsyncLoading(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun AsyncLoadingPreviewLight() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun AsyncLoadingPreviewDark() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
AsyncLoading()
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import io.element.android.libraries.designsystem.Black_800
|
|||
import io.element.android.libraries.designsystem.Black_950
|
||||
import io.element.android.libraries.designsystem.Compound_Gray_300_Dark
|
||||
import io.element.android.libraries.designsystem.DarkGrey
|
||||
import io.element.android.libraries.designsystem.Gray_1400_Dark
|
||||
import io.element.android.libraries.designsystem.Gray_300
|
||||
import io.element.android.libraries.designsystem.Gray_400
|
||||
import io.element.android.libraries.designsystem.Compound_Gray_400_Dark
|
||||
|
|
@ -42,6 +43,7 @@ fun elementColorsDark() = ElementColors(
|
|||
quinary = Gray_450,
|
||||
gray300 = Compound_Gray_300_Dark,
|
||||
gray400 = Compound_Gray_400_Dark,
|
||||
gray1400 = Gray_1400_Dark,
|
||||
textActionCritical = TextColorCriticalDark,
|
||||
isLight = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,12 +22,13 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.libraries.designsystem.Azure
|
||||
import io.element.android.libraries.designsystem.Black_900
|
||||
import io.element.android.libraries.designsystem.Compound_Gray_300_Light
|
||||
import io.element.android.libraries.designsystem.Compound_Gray_400_Light
|
||||
import io.element.android.libraries.designsystem.Gray_100
|
||||
import io.element.android.libraries.designsystem.Gray_1400_Light
|
||||
import io.element.android.libraries.designsystem.Gray_150
|
||||
import io.element.android.libraries.designsystem.Gray_200
|
||||
import io.element.android.libraries.designsystem.Gray_25
|
||||
import io.element.android.libraries.designsystem.Compound_Gray_300_Light
|
||||
import io.element.android.libraries.designsystem.Compound_Gray_400_Light
|
||||
import io.element.android.libraries.designsystem.Gray_50
|
||||
import io.element.android.libraries.designsystem.SystemGrey5Light
|
||||
import io.element.android.libraries.designsystem.SystemGrey6Light
|
||||
|
|
@ -42,6 +43,7 @@ fun elementColorsLight() = ElementColors(
|
|||
quinary = Gray_50,
|
||||
gray300 = Compound_Gray_300_Light,
|
||||
gray400 = Compound_Gray_400_Light,
|
||||
gray1400 = Gray_1400_Light,
|
||||
textActionCritical = TextColorCriticalLight,
|
||||
isLight = true,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class ElementColors(
|
|||
quinary: Color,
|
||||
gray300: Color,
|
||||
gray400: Color,
|
||||
gray1400: Color,
|
||||
textActionCritical: Color,
|
||||
isLight: Boolean
|
||||
) {
|
||||
|
|
@ -53,6 +54,9 @@ class ElementColors(
|
|||
var gray400 by mutableStateOf(gray400)
|
||||
private set
|
||||
|
||||
var gray1400 by mutableStateOf(gray1400)
|
||||
private set
|
||||
|
||||
var textActionCritical by mutableStateOf(textActionCritical)
|
||||
private set
|
||||
|
||||
|
|
@ -67,6 +71,7 @@ class ElementColors(
|
|||
quinary: Color = this.quinary,
|
||||
gray300: Color = this.gray300,
|
||||
gray400: Color = this.gray400,
|
||||
gray1400: Color = this.gray1400,
|
||||
textActionCritical: Color = this.textActionCritical,
|
||||
isLight: Boolean = this.isLight,
|
||||
) = ElementColors(
|
||||
|
|
@ -77,6 +82,7 @@ class ElementColors(
|
|||
quinary = quinary,
|
||||
gray300 = gray300,
|
||||
gray400 = gray400,
|
||||
gray1400 = gray1400,
|
||||
textActionCritical = textActionCritical,
|
||||
isLight = isLight,
|
||||
)
|
||||
|
|
@ -89,6 +95,7 @@ class ElementColors(
|
|||
quinary = other.quinary
|
||||
gray300 = other.gray300
|
||||
gray400 = other.gray400
|
||||
gray1400 = other.gray1400
|
||||
textActionCritical = other.textActionCritical
|
||||
isLight = other.isLight
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
<?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">"(Avatar wurde ebenfalls geändert)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s hat seinen Avatar geändert"</string>
|
||||
<string name="state_event_avatar_changed_too">"(Profilbild wurde auch geändert)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s hat sein Profilbild geändert"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Du hast deinen Avatar geändert"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s hat den Anzeigenamen von %2$s in %3$s geändert"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s hat seinen Anzeigenamen von %2$s in %3$s geändert"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Du hast deinen Anzeigenamen von %1$s in %2$s geändert"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s hat den Anzeigenamen entfernt (war %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Du hast deinen Anzeigenamen entfernt (war %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s hat den Anzeigenamen auf %2$s gesetzt"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Du hast deinen Anzeigenamen auf %1$s gesetzt"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s hat seinen Anzeigenamen entfernt (es war %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Du hast deinen Anzeigenamen entfernt (es war %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s hat seinen Anzeigenamen zu %2$s geändert"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Du hast deinen Anzeigenamen auf %1$s geändert"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s hat den Raum-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Du hast den Raum-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s hat den Raum-Avatar entfernt"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s hat das Raumbild entfernt"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"Du hast das Raumbild entfernt"</string>
|
||||
<string name="state_event_room_ban">"%1$s hat %2$s gebannt"</string>
|
||||
<string name="state_event_room_ban_by_you">"Du hast %1$s gebannt"</string>
|
||||
<string name="state_event_room_created">"%1$s hat den Raum erstellt"</string>
|
||||
<string name="state_event_room_created_by_you">"Du hast den Raum erstellt"</string>
|
||||
<string name="state_event_room_invite">"%1$s hat %2$s eingeladen"</string>
|
||||
|
|
@ -21,19 +24,34 @@
|
|||
<string name="state_event_room_invite_you">"%1$s hat dich eingeladen"</string>
|
||||
<string name="state_event_room_join">"%1$s ist dem Raum beigetreten"</string>
|
||||
<string name="state_event_room_join_by_you">"Du bist dem Raum beigetreten"</string>
|
||||
<string name="state_event_room_knock">"%1$s hat um Beitritt gebeten"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s hat %2$s erlaubt, beizutreten"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"%1$s hat dir erlaubt beizutreten"</string>
|
||||
<string name="state_event_room_knock_by_you">"Du hast um Beitritt gebeten"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s hat die Beitrittsanfrage von %2$s abgelehnt"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"Du hast die Beitrittsanfrage von %1$s abgelehnt"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s hat deine Beitrittsanfrage abgelehnt"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s ist nicht mehr daran interessiert, beizutreten"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"Du hast deine Beitrittsanfrage zurückgezogen"</string>
|
||||
<string name="state_event_room_leave">"%1$s hat den Raum verlassen"</string>
|
||||
<string name="state_event_room_leave_by_you">"Du hast den Raum verlassen"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s hat den Raumnamen geändert in: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Sie haben den Raumnamen geändert in: %1$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Du hast den Raumnamen geändert in: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s hat den Raumnamen entfernt"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Du hast den Raumnamen entfernt"</string>
|
||||
<string name="state_event_room_reject">"%1$s hat die Einladung abgelehnt"</string>
|
||||
<string name="state_event_room_reject_by_you">"Du hast die Einladung abgelehnt"</string>
|
||||
<string name="state_event_room_remove">"%1$s hat %2$s entfernt"</string>
|
||||
<string name="state_event_room_remove_by_you">"Du hast %1$s entfernt"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s hat eine Einladung an %2$s gesendet, um dem Raum beizutreten"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Du hast eine Einladung an %1$s gesendet, um dem Raum beizutreten"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s hat die Einladung für %2$s widerrufen, dem Raum beizutreten"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Du hast die Einladung für %1$s widerrufen, dem Raum beizutreten"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s hat das Thema geändert zu: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Sie haben das Thema geändert zu: %1$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Du hast das Thema geändert zu: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s hat das Raumthema entfernt"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Du hast das Raumthema entfernt"</string>
|
||||
<string name="state_event_room_unban">"%1$s hat %2$s entbannt"</string>
|
||||
<string name="state_event_room_unban_by_you">"Du hast %1$s entbannt"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s hat eine unbekannte Änderung an seiner Mitgliedschaft vorgenommen"</string>
|
||||
</resources>
|
||||
|
|
@ -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">"(l\'avatar a aussi été modifié)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s a changé son avatar"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Vous avez changé d\'avatar"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s a changé son nom d\'affichage de %2$s à %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Vous avez changé votre nom d\'affichage de %1$s à %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s a supprimé son nom d\'affichage (il s\'agissait de %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Vous avez supprimé votre nom d\'affichage (il s\'agissait de %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s a défini son nom d\'affichage en tant que %2$s"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Vous avez défini votre nom d\'affichage en tant que %1$s"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s a changé l\'avatar de la salle"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Vous avez changé l\'avatar de la salle"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s a supprimé l\'avatar de la salle"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"Vous avez supprimé l\'avatar de la salle"</string>
|
||||
<string name="state_event_room_ban">"%1$s a banni %2$s"</string>
|
||||
<string name="state_event_room_ban_by_you">"Vous avez banni %1$s"</string>
|
||||
<string name="state_event_room_created">"%1$s a créé la salle"</string>
|
||||
<string name="state_event_room_created_by_you">"Vous avez créé la salle"</string>
|
||||
<string name="state_event_room_invite">"%1$s a invité %2$s"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s a accepté l\'invitation"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Vous avez accepté l\'invitation"</string>
|
||||
<string name="state_event_room_invite_by_you">"Vous avez invité %1$s"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s vous a invité."</string>
|
||||
<string name="state_event_room_join">"%1$s a rejoint la salle"</string>
|
||||
<string name="state_event_room_join_by_you">"Vous avez rejoint la salle"</string>
|
||||
<string name="state_event_room_knock">"%1$s a demandé à rejoindre"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s a autorisé %2$s à rejoindre"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"%1$s vous a autorisé à rejoindre"</string>
|
||||
<string name="state_event_room_knock_by_you">"Vous avez demandé à rejoindre"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s a rejeté la demande d\'adhésion de %2$s"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"Vous avez rejeté la demande d\'adhésion de %1$s"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s a rejeté votre demande d\'adhésion"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s n’est plus intéressé à rejoindre"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"Vous avez annulé votre demande d\'adhésion"</string>
|
||||
<string name="state_event_room_leave">"%1$s a quitté la salle"</string>
|
||||
<string name="state_event_room_leave_by_you">"Vous avez quitté la salle"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s a changé le nom de la salle en : %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Vous avez changé le nom de la salle en : %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s a supprimé le nom de la salle"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Vous avez supprimé le nom de la salle"</string>
|
||||
<string name="state_event_room_reject">"%1$s a rejeté l\'invitation"</string>
|
||||
<string name="state_event_room_reject_by_you">"Vous avez refusé l\'invitation"</string>
|
||||
<string name="state_event_room_remove">"%1$s a supprimé %2$s"</string>
|
||||
<string name="state_event_room_remove_by_you">"Vous avez supprimé %1$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s a envoyé une invitation à %2$s à rejoindre la salle"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Vous avez envoyé une invitation à %1$s pour rejoindre la salle"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s a révoqué l\'invitation de %2$s à rejoindre la salle"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Vous avez révoqué l\'invitation de %1$s à rejoindre la salle"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s a changé le sujet en : %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Vous avez changé le sujet en : %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s a supprimé le sujet de la salle"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Vous avez supprimé le sujet de la salle"</string>
|
||||
<string name="state_event_room_unban">"%1$s a débanni %2$s"</string>
|
||||
<string name="state_event_room_unban_by_you">"Vous avez débanni %1$s"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s a apporté une modification inconnue à son adhésion"</string>
|
||||
</resources>
|
||||
|
|
@ -22,4 +22,5 @@ sealed class AuthenticationException(message: String) : Exception(message) {
|
|||
class SlidingSyncNotAvailable(message: String) : AuthenticationException(message)
|
||||
class SessionMissing(message: String) : AuthenticationException(message)
|
||||
class Generic(message: String) : AuthenticationException(message)
|
||||
class OidcError(type: String, message: String) : AuthenticationException(message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,4 +28,23 @@ interface MatrixAuthenticationService {
|
|||
fun getHomeserverDetails(): StateFlow<MatrixHomeServerDetails?>
|
||||
suspend fun setHomeserver(homeserver: String): Result<Unit>
|
||||
suspend fun login(username: String, password: String): Result<SessionId>
|
||||
|
||||
/*
|
||||
* OIDC part.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the Oidc url to display to the user.
|
||||
*/
|
||||
suspend fun getOidcUrl(): Result<OidcDetails>
|
||||
|
||||
/**
|
||||
* Cancel Oidc login sequence.
|
||||
*/
|
||||
suspend fun cancelOidcLogin(): Result<Unit>
|
||||
|
||||
/**
|
||||
* Attempt to login using the [callbackUrl] provided by the Oidc page.
|
||||
*/
|
||||
suspend fun loginWithOidc(callbackUrl: String): Result<SessionId>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ import kotlinx.parcelize.Parcelize
|
|||
data class MatrixHomeServerDetails(
|
||||
val url: String,
|
||||
val supportsPasswordLogin: Boolean,
|
||||
val authenticationIssuer: String?
|
||||
val supportsOidcLogin: Boolean,
|
||||
): Parcelable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.matrix.api.auth
|
||||
|
||||
object OidcConfig {
|
||||
const val redirectUri = "io.element:/callback"
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.matrix.api.auth
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class OidcDetails(
|
||||
val url: String,
|
||||
) : Parcelable
|
||||
|
|
@ -34,7 +34,7 @@ object MatrixPatterns {
|
|||
val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
// regex pattern to find room ids in a string.
|
||||
private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX"
|
||||
private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9-]+$DOMAIN_REGEX"
|
||||
private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = MATRIX_ROOM_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
// regex pattern to find room aliases in a string.
|
||||
|
|
|
|||
|
|
@ -20,15 +20,23 @@ 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.UserId
|
||||
|
||||
//TODO add content
|
||||
data class NotificationData(
|
||||
val senderId: UserId,
|
||||
val eventId: EventId,
|
||||
val roomId: RoomId,
|
||||
val senderAvatarUrl: String? = null,
|
||||
val senderDisplayName: String? = null,
|
||||
val roomAvatarUrl: String? = null,
|
||||
val senderAvatarUrl: String?,
|
||||
val senderDisplayName: String?,
|
||||
val roomAvatarUrl: String?,
|
||||
val roomDisplayName: String?,
|
||||
val isDirect: Boolean,
|
||||
val isEncrypted: Boolean,
|
||||
val isNoisy: Boolean,
|
||||
val event: NotificationEvent,
|
||||
)
|
||||
|
||||
data class NotificationEvent(
|
||||
val timestamp: Long,
|
||||
val content: String,
|
||||
// For images for instance
|
||||
val contentUrl: String?
|
||||
)
|
||||
|
|
|
|||
|
|
@ -89,4 +89,14 @@ interface MatrixRoom : Closeable {
|
|||
suspend fun inviteUserById(id: UserId): Result<Unit>
|
||||
|
||||
suspend fun canInvite(): Result<Boolean>
|
||||
|
||||
suspend fun canSendStateEvent(type: StateEventType): Result<Boolean>
|
||||
|
||||
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>
|
||||
|
||||
suspend fun removeAvatar(): Result<Unit>
|
||||
|
||||
suspend fun setName(name: String): Result<Unit>
|
||||
|
||||
suspend fun setTopic(topic: String): Result<Unit>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.matrix.api.room
|
||||
|
||||
enum class StateEventType {
|
||||
POLICY_RULE_ROOM,
|
||||
POLICY_RULE_SERVER,
|
||||
POLICY_RULE_USER,
|
||||
ROOM_ALIASES,
|
||||
ROOM_AVATAR,
|
||||
ROOM_CANONICAL_ALIAS,
|
||||
ROOM_CREATE,
|
||||
ROOM_ENCRYPTION,
|
||||
ROOM_GUEST_ACCESS,
|
||||
ROOM_HISTORY_VISIBILITY,
|
||||
ROOM_JOIN_RULES,
|
||||
ROOM_MEMBER_EVENT,
|
||||
ROOM_NAME,
|
||||
ROOM_PINNED_EVENTS,
|
||||
ROOM_POWER_LEVELS,
|
||||
ROOM_SERVER_ACL,
|
||||
ROOM_THIRD_PARTY_INVITE,
|
||||
ROOM_TOMBSTONE,
|
||||
ROOM_TOPIC,
|
||||
SPACE_CHILD,
|
||||
SPACE_PARENT;
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.matrix.api.timeline.item.event
|
||||
|
||||
/**
|
||||
* Constants defining known event types from Matrix specifications.
|
||||
*/
|
||||
object EventType {
|
||||
const val PRESENCE = "m.presence"
|
||||
const val MESSAGE = "m.room.message"
|
||||
const val STICKER = "m.sticker"
|
||||
const val ENCRYPTED = "m.room.encrypted"
|
||||
const val FEEDBACK = "m.room.message.feedback"
|
||||
const val TYPING = "m.typing"
|
||||
const val REDACTION = "m.room.redaction"
|
||||
const val RECEIPT = "m.receipt"
|
||||
const val ROOM_KEY = "m.room_key"
|
||||
const val PLUMBING = "m.room.plumbing"
|
||||
const val BOT_OPTIONS = "m.room.bot.options"
|
||||
const val PREVIEW_URLS = "org.matrix.room.preview_urls"
|
||||
|
||||
// State Events
|
||||
|
||||
const val STATE_ROOM_WIDGET_LEGACY = "im.vector.modular.widgets"
|
||||
const val STATE_ROOM_WIDGET = "m.widget"
|
||||
const val STATE_ROOM_NAME = "m.room.name"
|
||||
const val STATE_ROOM_TOPIC = "m.room.topic"
|
||||
const val STATE_ROOM_AVATAR = "m.room.avatar"
|
||||
const val STATE_ROOM_MEMBER = "m.room.member"
|
||||
const val STATE_ROOM_THIRD_PARTY_INVITE = "m.room.third_party_invite"
|
||||
const val STATE_ROOM_CREATE = "m.room.create"
|
||||
const val STATE_ROOM_JOIN_RULES = "m.room.join_rules"
|
||||
const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"
|
||||
const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
|
||||
|
||||
const val STATE_SPACE_CHILD = "m.space.child"
|
||||
const val STATE_SPACE_PARENT = "m.space.parent"
|
||||
|
||||
/**
|
||||
* Note that this Event has been deprecated, see
|
||||
* - https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
|
||||
* - https://github.com/matrix-org/matrix-doc/pull/2432
|
||||
*/
|
||||
const val STATE_ROOM_ALIASES = "m.room.aliases"
|
||||
const val STATE_ROOM_TOMBSTONE = "m.room.tombstone"
|
||||
const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias"
|
||||
const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility"
|
||||
const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups"
|
||||
const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events"
|
||||
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
|
||||
const val STATE_ROOM_SERVER_ACL = "m.room.server_acl"
|
||||
|
||||
// Call Events
|
||||
const val CALL_INVITE = "m.call.invite"
|
||||
const val CALL_CANDIDATES = "m.call.candidates"
|
||||
const val CALL_ANSWER = "m.call.answer"
|
||||
const val CALL_SELECT_ANSWER = "m.call.select_answer"
|
||||
const val CALL_NEGOTIATE = "m.call.negotiate"
|
||||
const val CALL_REJECT = "m.call.reject"
|
||||
const val CALL_HANGUP = "m.call.hangup"
|
||||
|
||||
// This type is not processed by the client, just sent to the server
|
||||
const val CALL_REPLACES = "m.call.replaces"
|
||||
|
||||
// Key share events
|
||||
const val ROOM_KEY_REQUEST = "m.room_key_request"
|
||||
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
|
||||
|
||||
const val REQUEST_SECRET = "m.secret.request"
|
||||
const val SEND_SECRET = "m.secret.send"
|
||||
|
||||
// Relation Events
|
||||
const val REACTION = "m.reaction"
|
||||
|
||||
fun isCallEvent(type: String): Boolean {
|
||||
return type == CALL_INVITE ||
|
||||
type == CALL_CANDIDATES ||
|
||||
type == CALL_ANSWER ||
|
||||
type == CALL_HANGUP ||
|
||||
type == CALL_SELECT_ANSWER ||
|
||||
type == CALL_NEGOTIATE ||
|
||||
type == CALL_REJECT ||
|
||||
type == CALL_REPLACES
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ dependencies {
|
|||
// api(projects.libraries.rustsdk)
|
||||
implementation(libs.matrix.sdk)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.services.toolbox.api)
|
||||
api(projects.libraries.matrix.api)
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.core)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
|
|||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
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
|
||||
|
|
@ -44,6 +45,7 @@ import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
|
|||
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
|
||||
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -77,6 +79,7 @@ class RustMatrixClient constructor(
|
|||
private val coroutineScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val baseDirectory: File,
|
||||
private val clock: SystemClock,
|
||||
) : MatrixClient {
|
||||
|
||||
override val sessionId: UserId = UserId(client.userId())
|
||||
|
|
@ -114,9 +117,9 @@ class RustMatrixClient constructor(
|
|||
.timelineLimit(limit = 1u)
|
||||
.requiredState(
|
||||
requiredState = listOf(
|
||||
RequiredState(key = "m.room.avatar", value = ""),
|
||||
RequiredState(key = "m.room.encryption", value = ""),
|
||||
RequiredState(key = "m.room.join_rules", value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_AVATAR, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_ENCRYPTION, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
|
||||
)
|
||||
)
|
||||
.filters(visibleRoomsSlidingSyncFilters)
|
||||
|
|
@ -136,9 +139,9 @@ class RustMatrixClient constructor(
|
|||
.timelineLimit(limit = 1u)
|
||||
.requiredState(
|
||||
requiredState = listOf(
|
||||
RequiredState(key = "m.room.avatar", value = ""),
|
||||
RequiredState(key = "m.room.encryption", value = ""),
|
||||
RequiredState(key = "m.room.canonical_alias", value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_AVATAR, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_ENCRYPTION, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
|
||||
)
|
||||
)
|
||||
.filters(invitesSlidingSyncFilters)
|
||||
|
|
@ -153,7 +156,7 @@ class RustMatrixClient constructor(
|
|||
|
||||
private val slidingSync = client
|
||||
.slidingSync()
|
||||
.homeserver("https://slidingsync.lab.matrix.org")
|
||||
// .homeserver("https://slidingsync.lab.matrix.org")
|
||||
.withCommonExtensions()
|
||||
.storageKey("ElementX")
|
||||
.addList(visibleRoomsSlidingSyncListBuilder)
|
||||
|
|
@ -215,6 +218,7 @@ class RustMatrixClient constructor(
|
|||
innerRoom = fullRoom,
|
||||
coroutineScope = coroutineScope,
|
||||
coroutineDispatchers = dispatchers,
|
||||
clock = clock,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,15 @@ fun Throwable.mapAuthenticationException(): Throwable {
|
|||
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!)
|
||||
is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!)
|
||||
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!)
|
||||
|
||||
/* TODO Oidc
|
||||
is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!)
|
||||
is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!)
|
||||
is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!)
|
||||
is RustAuthenticationException.OidcNotStarted -> AuthenticationException.OidcError("OidcNotStarted", message!!)
|
||||
is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!)
|
||||
*/
|
||||
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ fun HomeserverLoginDetails.map(): MatrixHomeServerDetails = use {
|
|||
MatrixHomeServerDetails(
|
||||
url = url(),
|
||||
supportsPasswordLogin = supportsPasswordLogin(),
|
||||
authenticationIssuer = authenticationIssuer()
|
||||
supportsOidcLogin = false // TODO Oidc supportsOidcLogin(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.matrix.impl.auth
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.OidcConfig
|
||||
// TODO Oidc
|
||||
// import org.matrix.rustcomponents.sdk.OidcClientMetadata
|
||||
|
||||
/*
|
||||
val oidcClientMetadata: OidcClientMetadata = OidcClientMetadata(
|
||||
clientName = "Element",
|
||||
redirectUri = OidcConfig.redirectUri,
|
||||
clientUri = "https://element.io",
|
||||
tosUri = "https://element.io/user-terms-of-service",
|
||||
policyUri = "https://element.io/privacy"
|
||||
)
|
||||
*/
|
||||
|
||||
|
|
@ -24,11 +24,12 @@ import io.element.android.libraries.di.SingleIn
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.impl.RustMatrixClient
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -36,6 +37,8 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
// TODO Oidc
|
||||
// import org.matrix.rustcomponents.sdk.OidcAuthenticationUrl
|
||||
import org.matrix.rustcomponents.sdk.Session
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import java.io.File
|
||||
|
|
@ -49,9 +52,16 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
private val coroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val sessionStore: SessionStore,
|
||||
private val clock: SystemClock,
|
||||
) : MatrixAuthenticationService {
|
||||
|
||||
private val authService: RustAuthenticationService = RustAuthenticationService(baseDirectory.absolutePath, null, null)
|
||||
private val authService: RustAuthenticationService = RustAuthenticationService(
|
||||
basePath = baseDirectory.absolutePath,
|
||||
passphrase = null,
|
||||
// TODO Oidc
|
||||
// oidcClientMetadata = oidcClientMetadata,
|
||||
customSlidingSyncProxy = null
|
||||
)
|
||||
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
||||
|
||||
override fun isLoggedIn(): Flow<Boolean> {
|
||||
|
|
@ -91,9 +101,9 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
if (homeServerDetails != null) {
|
||||
currentHomeserver.value = homeServerDetails.copy(url = homeserver)
|
||||
}
|
||||
}.mapFailure { failure ->
|
||||
failure.mapAuthenticationException()
|
||||
}
|
||||
}.mapFailure { failure ->
|
||||
failure.mapAuthenticationException()
|
||||
}
|
||||
|
||||
override suspend fun login(username: String, password: String): Result<SessionId> =
|
||||
|
|
@ -103,11 +113,65 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
val sessionData = client.use { it.session().toSessionData() }
|
||||
sessionStore.storeData(sessionData)
|
||||
SessionId(sessionData.userId)
|
||||
}.mapFailure { failure ->
|
||||
failure.mapAuthenticationException()
|
||||
}
|
||||
}.mapFailure { failure ->
|
||||
failure.mapAuthenticationException()
|
||||
}
|
||||
|
||||
// TODO Oidc
|
||||
// private var pendingUrlForOidcLogin: OidcAuthenticationUrl? = null
|
||||
|
||||
override suspend fun getOidcUrl(): Result<OidcDetails> {
|
||||
TODO("Oidc")
|
||||
/*
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val urlForOidcLogin = authService.urlForOidcLogin()
|
||||
val url = urlForOidcLogin.loginUrl()
|
||||
pendingUrlForOidcLogin = urlForOidcLogin
|
||||
OidcDetails(url)
|
||||
}.mapFailure { failure ->
|
||||
failure.mapAuthenticationException()
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
override suspend fun cancelOidcLogin(): Result<Unit> {
|
||||
TODO("Oidc")
|
||||
/*
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
pendingUrlForOidcLogin?.close()
|
||||
pendingUrlForOidcLogin = null
|
||||
}.mapFailure { failure ->
|
||||
failure.mapAuthenticationException()
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters).
|
||||
*/
|
||||
override suspend fun loginWithOidc(callbackUrl: String): Result<SessionId> {
|
||||
TODO("Oidc")
|
||||
/*
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val urlForOidcLogin = pendingUrlForOidcLogin ?: error("You need to call `getOidcUrl()` first")
|
||||
val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
|
||||
val sessionData = client.use { it.session().toSessionData() }
|
||||
pendingUrlForOidcLogin = null
|
||||
sessionStore.storeData(sessionData)
|
||||
SessionId(sessionData.userId)
|
||||
}.mapFailure { failure ->
|
||||
failure.mapAuthenticationException()
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private fun createMatrixClient(client: Client): MatrixClient {
|
||||
return RustMatrixClient(
|
||||
client = client,
|
||||
|
|
@ -115,6 +179,7 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
coroutineScope = coroutineScope,
|
||||
dispatchers = coroutineDispatchers,
|
||||
baseDirectory = baseDirectory,
|
||||
clock = clock,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import org.matrix.rustcomponents.sdk.NotificationItem
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationMapper @Inject constructor() {
|
||||
class NotificationMapper {
|
||||
private val timelineEventMapper = TimelineEventMapper()
|
||||
|
||||
fun map(notificationItem: NotificationItem): NotificationData {
|
||||
return notificationItem.use {
|
||||
|
|
@ -36,9 +36,11 @@ class NotificationMapper @Inject constructor() {
|
|||
senderAvatarUrl = it.senderAvatarUrl,
|
||||
senderDisplayName = it.senderDisplayName,
|
||||
roomAvatarUrl = it.roomAvatarUrl,
|
||||
roomDisplayName = it.roomDisplayName,
|
||||
isDirect = it.isDirect,
|
||||
isEncrypted = it.isEncrypted.orFalse(),
|
||||
isNoisy = it.isNoisy
|
||||
isNoisy = it.isNoisy,
|
||||
event = it.event.use { event -> timelineEventMapper.map(event) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,18 +16,13 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.notification
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
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.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import java.io.File
|
||||
|
||||
class RustNotificationService(
|
||||
private val client: Client,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.matrix.impl.notification
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationEvent
|
||||
import org.matrix.rustcomponents.sdk.MessageLikeEventContent
|
||||
import org.matrix.rustcomponents.sdk.MessageType
|
||||
import org.matrix.rustcomponents.sdk.StateEventContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineEvent
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventType
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineEventMapper @Inject constructor() {
|
||||
|
||||
fun map(timelineEvent: TimelineEvent): NotificationEvent {
|
||||
return timelineEvent.use {
|
||||
NotificationEvent(
|
||||
timestamp = it.timestamp().toLong(),
|
||||
content = it.eventType().toContent(),
|
||||
contentUrl = null // TODO it.eventType().toContentUrl(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TimelineEventType.toContent(): String {
|
||||
return when (this) {
|
||||
is TimelineEventType.MessageLike -> content.toContent()
|
||||
is TimelineEventType.State -> content.toContent()
|
||||
}
|
||||
}
|
||||
|
||||
private fun StateEventContent.toContent(): String {
|
||||
return when (this) {
|
||||
StateEventContent.PolicyRuleRoom -> "PolicyRuleRoom"
|
||||
StateEventContent.PolicyRuleServer -> "PolicyRuleServer"
|
||||
StateEventContent.PolicyRuleUser -> "PolicyRuleUser"
|
||||
StateEventContent.RoomAliases -> "RoomAliases"
|
||||
StateEventContent.RoomAvatar -> "RoomAvatar"
|
||||
StateEventContent.RoomCanonicalAlias -> "RoomCanonicalAlias"
|
||||
StateEventContent.RoomCreate -> "RoomCreate"
|
||||
StateEventContent.RoomEncryption -> "RoomEncryption"
|
||||
StateEventContent.RoomGuestAccess -> "RoomGuestAccess"
|
||||
StateEventContent.RoomHistoryVisibility -> "RoomHistoryVisibility"
|
||||
StateEventContent.RoomJoinRules -> "RoomJoinRules"
|
||||
is StateEventContent.RoomMemberContent -> "$userId is now $membershipState"
|
||||
StateEventContent.RoomName -> "RoomName"
|
||||
StateEventContent.RoomPinnedEvents -> "RoomPinnedEvents"
|
||||
StateEventContent.RoomPowerLevels -> "RoomPowerLevels"
|
||||
StateEventContent.RoomServerAcl -> "RoomServerAcl"
|
||||
StateEventContent.RoomThirdPartyInvite -> "RoomThirdPartyInvite"
|
||||
StateEventContent.RoomTombstone -> "RoomTombstone"
|
||||
StateEventContent.RoomTopic -> "RoomTopic"
|
||||
StateEventContent.SpaceChild -> "SpaceChild"
|
||||
StateEventContent.SpaceParent -> "SpaceParent"
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessageLikeEventContent.toContent(): String {
|
||||
return use {
|
||||
when (it) {
|
||||
MessageLikeEventContent.CallAnswer -> "CallAnswer"
|
||||
MessageLikeEventContent.CallCandidates -> "CallCandidates"
|
||||
MessageLikeEventContent.CallHangup -> "CallHangup"
|
||||
MessageLikeEventContent.CallInvite -> "CallInvite"
|
||||
MessageLikeEventContent.KeyVerificationAccept -> "KeyVerificationAccept"
|
||||
MessageLikeEventContent.KeyVerificationCancel -> "KeyVerificationCancel"
|
||||
MessageLikeEventContent.KeyVerificationDone -> "KeyVerificationDone"
|
||||
MessageLikeEventContent.KeyVerificationKey -> "KeyVerificationKey"
|
||||
MessageLikeEventContent.KeyVerificationMac -> "KeyVerificationMac"
|
||||
MessageLikeEventContent.KeyVerificationReady -> "KeyVerificationReady"
|
||||
MessageLikeEventContent.KeyVerificationStart -> "KeyVerificationStart"
|
||||
is MessageLikeEventContent.ReactionContent -> "Reacted to ${it.relatedEventId.take(8)}…"
|
||||
MessageLikeEventContent.RoomEncrypted -> "RoomEncrypted"
|
||||
is MessageLikeEventContent.RoomMessage -> it.messageType.toContent()
|
||||
MessageLikeEventContent.RoomRedaction -> "RoomRedaction"
|
||||
MessageLikeEventContent.Sticker -> "Sticker"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessageType.toContent(): String {
|
||||
return when (this) {
|
||||
is MessageType.Audio -> content.use { it.body }
|
||||
is MessageType.Emote -> content.body
|
||||
is MessageType.File -> content.use { it.body }
|
||||
is MessageType.Image -> content.use { it.body }
|
||||
is MessageType.Notice -> content.body
|
||||
is MessageType.Text -> content.body
|
||||
is MessageType.Video -> content.use { it.body }
|
||||
}
|
||||
}
|
||||
|
|
@ -27,11 +27,14 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
|
|||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -54,6 +57,7 @@ class RustMatrixRoom(
|
|||
private val innerRoom: Room,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val clock: SystemClock,
|
||||
) : MatrixRoom {
|
||||
|
||||
override val membersStateFlow: StateFlow<MatrixRoomMembersState>
|
||||
|
|
@ -77,9 +81,9 @@ class RustMatrixRoom(
|
|||
it.rooms.contains(roomId.value)
|
||||
}
|
||||
.map {
|
||||
System.currentTimeMillis()
|
||||
clock.epochMillis()
|
||||
}
|
||||
.onStart { emit(System.currentTimeMillis()) }
|
||||
.onStart { emit(clock.epochMillis()) }
|
||||
}
|
||||
|
||||
override fun timeline(): MatrixTimeline {
|
||||
|
|
@ -222,6 +226,12 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun canSendStateEvent(type: StateEventType): Result<Boolean> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
innerRoom.member(sessionId.value).use { it.canSendState(type.map()) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map())
|
||||
|
|
@ -245,4 +255,33 @@ class RustMatrixRoom(
|
|||
innerRoom.sendFile(file.path, fileInfo.map())
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
innerRoom.uploadAvatar(mimeType, data.toUByteArray().toList())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeAvatar(): Result<Unit> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
innerRoom.removeAvatar()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setName(name: String): Result<Unit> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
innerRoom.setName(name)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setTopic(topic: String): Result<Unit> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
innerRoom.setTopic(topic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.matrix.impl.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import org.matrix.rustcomponents.sdk.StateEventType as RustStateEventType
|
||||
|
||||
fun StateEventType.map(): RustStateEventType = when (this) {
|
||||
StateEventType.POLICY_RULE_ROOM -> RustStateEventType.POLICY_RULE_ROOM
|
||||
StateEventType.POLICY_RULE_SERVER -> RustStateEventType.POLICY_RULE_SERVER
|
||||
StateEventType.POLICY_RULE_USER -> RustStateEventType.POLICY_RULE_USER
|
||||
StateEventType.ROOM_ALIASES -> RustStateEventType.ROOM_ALIASES
|
||||
StateEventType.ROOM_AVATAR -> RustStateEventType.ROOM_AVATAR
|
||||
StateEventType.ROOM_CANONICAL_ALIAS -> RustStateEventType.ROOM_CANONICAL_ALIAS
|
||||
StateEventType.ROOM_CREATE -> RustStateEventType.ROOM_CREATE
|
||||
StateEventType.ROOM_ENCRYPTION -> RustStateEventType.ROOM_ENCRYPTION
|
||||
StateEventType.ROOM_GUEST_ACCESS -> RustStateEventType.ROOM_GUEST_ACCESS
|
||||
StateEventType.ROOM_HISTORY_VISIBILITY -> RustStateEventType.ROOM_HISTORY_VISIBILITY
|
||||
StateEventType.ROOM_JOIN_RULES -> RustStateEventType.ROOM_JOIN_RULES
|
||||
StateEventType.ROOM_MEMBER_EVENT -> RustStateEventType.ROOM_MEMBER_EVENT
|
||||
StateEventType.ROOM_NAME -> RustStateEventType.ROOM_NAME
|
||||
StateEventType.ROOM_PINNED_EVENTS -> RustStateEventType.ROOM_PINNED_EVENTS
|
||||
StateEventType.ROOM_POWER_LEVELS -> RustStateEventType.ROOM_POWER_LEVELS
|
||||
StateEventType.ROOM_SERVER_ACL -> RustStateEventType.ROOM_SERVER_ACL
|
||||
StateEventType.ROOM_THIRD_PARTY_INVITE -> RustStateEventType.ROOM_THIRD_PARTY_INVITE
|
||||
StateEventType.ROOM_TOMBSTONE -> RustStateEventType.ROOM_TOMBSTONE
|
||||
StateEventType.ROOM_TOPIC -> RustStateEventType.ROOM_TOPIC
|
||||
StateEventType.SPACE_CHILD -> RustStateEventType.SPACE_CHILD
|
||||
StateEventType.SPACE_PARENT -> RustStateEventType.SPACE_PARENT
|
||||
}
|
||||
|
||||
fun RustStateEventType.map(): StateEventType = when (this) {
|
||||
RustStateEventType.POLICY_RULE_ROOM -> StateEventType.POLICY_RULE_ROOM
|
||||
RustStateEventType.POLICY_RULE_SERVER -> StateEventType.POLICY_RULE_SERVER
|
||||
RustStateEventType.POLICY_RULE_USER -> StateEventType.POLICY_RULE_USER
|
||||
RustStateEventType.ROOM_ALIASES -> StateEventType.ROOM_ALIASES
|
||||
RustStateEventType.ROOM_AVATAR -> StateEventType.ROOM_AVATAR
|
||||
RustStateEventType.ROOM_CANONICAL_ALIAS -> StateEventType.ROOM_CANONICAL_ALIAS
|
||||
RustStateEventType.ROOM_CREATE -> StateEventType.ROOM_CREATE
|
||||
RustStateEventType.ROOM_ENCRYPTION -> StateEventType.ROOM_ENCRYPTION
|
||||
RustStateEventType.ROOM_GUEST_ACCESS -> StateEventType.ROOM_GUEST_ACCESS
|
||||
RustStateEventType.ROOM_HISTORY_VISIBILITY -> StateEventType.ROOM_HISTORY_VISIBILITY
|
||||
RustStateEventType.ROOM_JOIN_RULES -> StateEventType.ROOM_JOIN_RULES
|
||||
RustStateEventType.ROOM_MEMBER_EVENT -> StateEventType.ROOM_MEMBER_EVENT
|
||||
RustStateEventType.ROOM_NAME -> StateEventType.ROOM_NAME
|
||||
RustStateEventType.ROOM_PINNED_EVENTS -> StateEventType.ROOM_PINNED_EVENTS
|
||||
RustStateEventType.ROOM_POWER_LEVELS -> StateEventType.ROOM_POWER_LEVELS
|
||||
RustStateEventType.ROOM_SERVER_ACL -> StateEventType.ROOM_SERVER_ACL
|
||||
RustStateEventType.ROOM_THIRD_PARTY_INVITE -> StateEventType.ROOM_THIRD_PARTY_INVITE
|
||||
RustStateEventType.ROOM_TOMBSTONE -> StateEventType.ROOM_TOMBSTONE
|
||||
RustStateEventType.ROOM_TOPIC -> StateEventType.ROOM_TOPIC
|
||||
RustStateEventType.SPACE_CHILD -> StateEventType.SPACE_CHILD
|
||||
RustStateEventType.SPACE_PARENT -> StateEventType.SPACE_PARENT
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
|
||||
|
|
@ -149,10 +150,10 @@ class RustMatrixTimeline(
|
|||
runCatching {
|
||||
val settings = RoomSubscription(
|
||||
requiredState = listOf(
|
||||
RequiredState(key = "m.room.canonical_alias", value = ""),
|
||||
RequiredState(key = "m.room.topic", value = ""),
|
||||
RequiredState(key = "m.room.join_rules", value = ""),
|
||||
RequiredState(key = "m.room.power_levels", value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_TOPIC, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
|
||||
),
|
||||
timelineLimit = null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ const val ANOTHER_MESSAGE = "Hello universe!"
|
|||
const val A_HOMESERVER_URL = "matrix.org"
|
||||
const val A_HOMESERVER_URL_2 = "matrix-client.org"
|
||||
|
||||
val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, true, null)
|
||||
val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false)
|
||||
val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
|
||||
|
||||
const val AN_AVATAR_URL = "mxc://data"
|
||||
|
||||
|
|
|
|||
|
|
@ -19,16 +19,22 @@ package io.element.android.libraries.matrix.test.auth
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
val A_OIDC_DATA = OidcDetails(url = "a-url")
|
||||
|
||||
class FakeAuthenticationService : MatrixAuthenticationService {
|
||||
private var homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
||||
private var oidcError: Throwable? = null
|
||||
private var oidcCancelError: Throwable? = null
|
||||
private var loginError: Throwable? = null
|
||||
private var changeServerError: Throwable? = null
|
||||
|
||||
|
|
@ -53,15 +59,36 @@ class FakeAuthenticationService : MatrixAuthenticationService {
|
|||
}
|
||||
|
||||
override suspend fun setHomeserver(homeserver: String): Result<Unit> {
|
||||
delay(100)
|
||||
delay(FAKE_DELAY_IN_MS)
|
||||
return changeServerError?.let { Result.failure(it) } ?: Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun login(username: String, password: String): Result<SessionId> {
|
||||
delay(100)
|
||||
delay(FAKE_DELAY_IN_MS)
|
||||
return loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID)
|
||||
}
|
||||
|
||||
override suspend fun getOidcUrl(): Result<OidcDetails> {
|
||||
return oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA)
|
||||
}
|
||||
|
||||
override suspend fun cancelOidcLogin(): Result<Unit> {
|
||||
return oidcCancelError?.let { Result.failure(it) } ?: Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun loginWithOidc(callbackUrl: String): Result<SessionId> {
|
||||
delay(FAKE_DELAY_IN_MS)
|
||||
return loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID)
|
||||
}
|
||||
|
||||
fun givenOidcError(throwable: Throwable?) {
|
||||
oidcError = throwable
|
||||
}
|
||||
|
||||
fun givenOidcCancelError(throwable: Throwable?) {
|
||||
oidcCancelError = throwable
|
||||
}
|
||||
|
||||
fun givenLoginError(throwable: Throwable?) {
|
||||
loginError = throwable
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
|
|||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
|
|
@ -62,7 +63,13 @@ class FakeMatrixRoom(
|
|||
private var rejectInviteResult = Result.success(Unit)
|
||||
private var inviteUserResult = Result.success(Unit)
|
||||
private var canInviteResult = Result.success(true)
|
||||
private val canSendStateResults = mutableMapOf<StateEventType, Result<Boolean>>()
|
||||
private var sendMediaResult = Result.success(Unit)
|
||||
private var setNameResult = Result.success(Unit)
|
||||
private var setTopicResult = Result.success(Unit)
|
||||
private var updateAvatarResult = Result.success(Unit)
|
||||
private var removeAvatarResult = Result.success(Unit)
|
||||
|
||||
var sendMediaCount = 0
|
||||
private set
|
||||
|
||||
|
|
@ -75,6 +82,18 @@ class FakeMatrixRoom(
|
|||
var invitedUserId: UserId? = null
|
||||
private set
|
||||
|
||||
var newTopic: String? = null
|
||||
private set
|
||||
|
||||
var newName: String? = null
|
||||
private set
|
||||
|
||||
var newAvatarData: ByteArray? = null
|
||||
private set
|
||||
|
||||
var removedAvatar: Boolean = false
|
||||
private set
|
||||
|
||||
private var leaveRoomError: Throwable? = null
|
||||
|
||||
override val membersStateFlow: MutableStateFlow<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown)
|
||||
|
|
@ -151,6 +170,10 @@ class FakeMatrixRoom(
|
|||
return canInviteResult
|
||||
}
|
||||
|
||||
override suspend fun canSendStateEvent(type: StateEventType): Result<Boolean> {
|
||||
return canSendStateResults[type] ?: Result.failure(IllegalStateException("No fake answer"))
|
||||
}
|
||||
|
||||
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result<Unit> = fakeSendMedia()
|
||||
|
||||
override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result<Unit> = fakeSendMedia()
|
||||
|
|
@ -166,6 +189,26 @@ class FakeMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> {
|
||||
newAvatarData = data
|
||||
return updateAvatarResult
|
||||
}
|
||||
|
||||
override suspend fun removeAvatar(): Result<Unit> {
|
||||
removedAvatar = true
|
||||
return removeAvatarResult
|
||||
}
|
||||
|
||||
override suspend fun setName(name: String): Result<Unit> {
|
||||
newName = name
|
||||
return setNameResult
|
||||
}
|
||||
|
||||
override suspend fun setTopic(topic: String): Result<Unit> {
|
||||
newTopic = topic
|
||||
return setTopicResult
|
||||
}
|
||||
|
||||
override fun close() = Unit
|
||||
|
||||
fun givenLeaveRoomError(throwable: Throwable?) {
|
||||
|
|
@ -204,6 +247,10 @@ class FakeMatrixRoom(
|
|||
canInviteResult = result
|
||||
}
|
||||
|
||||
fun givenCanSendStateResult(type: StateEventType, result: Result<Boolean>) {
|
||||
canSendStateResults[type] = result
|
||||
}
|
||||
|
||||
fun givenIgnoreResult(result: Result<Unit>) {
|
||||
ignoreResult = result
|
||||
}
|
||||
|
|
@ -215,4 +262,20 @@ class FakeMatrixRoom(
|
|||
fun givenSendMediaResult(result: Result<Unit>) {
|
||||
sendMediaResult = result
|
||||
}
|
||||
|
||||
fun givenUpdateAvatarResult(result: Result<Unit>) {
|
||||
updateAvatarResult = result
|
||||
}
|
||||
|
||||
fun givenRemoveAvatarResult(result: Result<Unit>) {
|
||||
removeAvatarResult = result
|
||||
}
|
||||
|
||||
fun givenSetNameResult(result: Result<Unit>) {
|
||||
setNameResult = result
|
||||
}
|
||||
|
||||
fun givenSetTopicResult(result: Result<Unit>) {
|
||||
setTopicResult = result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ dependencies {
|
|||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.gif)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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(ExperimentalMaterialApi::class)
|
||||
|
||||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetState
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun AvatarActionBottomSheet(
|
||||
actions: ImmutableList<AvatarAction>,
|
||||
modalBottomSheetState: ModalBottomSheetState,
|
||||
modifier: Modifier = Modifier,
|
||||
onActionSelected: (action: AvatarAction) -> Unit = {},
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
fun onItemActionClicked(itemAction: AvatarAction) {
|
||||
onActionSelected(itemAction)
|
||||
coroutineScope.launch {
|
||||
modalBottomSheetState.hide()
|
||||
}
|
||||
}
|
||||
|
||||
ModalBottomSheetLayout(
|
||||
modifier = modifier,
|
||||
sheetState = modalBottomSheetState,
|
||||
sheetContent = {
|
||||
AvatarActionBottomSheetContent(
|
||||
actions = actions,
|
||||
onActionClicked = ::onItemActionClicked,
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AvatarActionBottomSheetContent(
|
||||
actions: ImmutableList<AvatarAction>,
|
||||
modifier: Modifier = Modifier,
|
||||
onActionClicked: (AvatarAction) -> Unit = { },
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
items(
|
||||
items = actions,
|
||||
) { action ->
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { onActionClicked(action) },
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(action.titleResId),
|
||||
color = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = action.icon,
|
||||
contentDescription = stringResource(action.titleResId),
|
||||
tint = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AvatarActionBottomSheetLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AvatarActionBottomSheetDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
AvatarActionBottomSheet(
|
||||
actions = persistentListOf(AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, AvatarAction.Remove),
|
||||
modalBottomSheetState = ModalBottomSheetState(
|
||||
initialValue = ModalBottomSheetValue.Expanded
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
|
||||
@Composable
|
||||
fun UnresolvedUserRow(
|
||||
avatarData: AvatarData,
|
||||
id: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(avatarData)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp),
|
||||
) {
|
||||
// ID
|
||||
Text(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
text = id,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
|
||||
// Warning
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Error,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.align(Alignment.Top)
|
||||
.padding(2.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.common_invite_unknown_profile),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckableUnresolvedUserRow(
|
||||
checked: Boolean,
|
||||
avatarData: AvatarData,
|
||||
id: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onCheckedChange: (Boolean) -> Unit = {},
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(role = Role.Checkbox, enabled = enabled) {
|
||||
onCheckedChange(!checked)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
UnresolvedUserRow(
|
||||
modifier = Modifier.weight(1f),
|
||||
avatarData = avatarData,
|
||||
id = id,
|
||||
)
|
||||
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun UnresolvedUserRowPreview() =
|
||||
ElementThemedPreview {
|
||||
val matrixUser = aMatrixUser()
|
||||
UnresolvedUserRow(matrixUser.getAvatarData(), matrixUser.userId.value)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun CheckableUnresolvedUserRowPreview() =
|
||||
ElementThemedPreview {
|
||||
val matrixUser = aMatrixUser()
|
||||
Column {
|
||||
CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(), matrixUser.userId.value)
|
||||
CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(), matrixUser.userId.value)
|
||||
CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(), matrixUser.userId.value, enabled = false)
|
||||
CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(), matrixUser.userId.value, enabled = false)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.matrix.ui.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AddAPhoto
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
|
||||
import io.element.android.libraries.designsystem.theme.LocalColors
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
|
||||
/**
|
||||
* An avatar that the user has selected, but which has not yet been uploaded to Matrix.
|
||||
*
|
||||
* The image is loaded from a local resource instead of from a MXC URI.
|
||||
*/
|
||||
@Composable
|
||||
fun UnsavedAvatar(
|
||||
avatarUri: Uri?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val commonModifier = modifier
|
||||
.size(70.dp)
|
||||
.clip(CircleShape)
|
||||
|
||||
if (avatarUri != null) {
|
||||
val context = LocalContext.current
|
||||
val model = ImageRequest.Builder(context)
|
||||
.data(avatarUri)
|
||||
.build()
|
||||
AsyncImage(
|
||||
modifier = commonModifier,
|
||||
model = model,
|
||||
placeholder = debugPlaceholderBackground(ColorPainter(MaterialTheme.colorScheme.surfaceVariant)),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null,
|
||||
)
|
||||
} else {
|
||||
Box(modifier = commonModifier.background(LocalColors.current.quinary)) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.AddAPhoto,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(40.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun UnsavedAvatarLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun UnsavedAvatarDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Row {
|
||||
UnsavedAvatar(null)
|
||||
UnsavedAvatar(Uri.EMPTY)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.matrix.ui.media
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.PhotoCamera
|
||||
import androidx.compose.material.icons.outlined.PhotoLibrary
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
|
||||
@Immutable
|
||||
sealed class AvatarAction(
|
||||
@StringRes val titleResId: Int,
|
||||
val icon: ImageVector,
|
||||
val destructive: Boolean = false,
|
||||
) {
|
||||
object TakePhoto : AvatarAction(titleResId = R.string.action_take_photo, icon = Icons.Outlined.PhotoCamera)
|
||||
object ChoosePhoto : AvatarAction(titleResId = R.string.action_choose_photo, icon = Icons.Outlined.PhotoLibrary)
|
||||
object Remove : AvatarAction(titleResId = R.string.action_remove, icon = Icons.Outlined.Delete, destructive = true)
|
||||
}
|
||||
|
|
@ -17,8 +17,11 @@
|
|||
package io.element.android.libraries.matrix.ui.media
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import okhttp3.OkHttpClient
|
||||
|
|
@ -34,6 +37,12 @@ class LoggedInImageLoaderFactory @Inject constructor(
|
|||
.Builder(context)
|
||||
.okHttpClient(okHttpClient)
|
||||
.components {
|
||||
// Add gif support
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
add(AvatarDataKeyer())
|
||||
add(MediaRequestDataKeyer())
|
||||
add(CoilMediaFetcher.AvatarFactory(matrixClient))
|
||||
|
|
|
|||
|
|
@ -29,13 +29,13 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
|||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
|
||||
@Composable
|
||||
fun MatrixRoom.getRoomMember(userId: UserId): State<RoomMember?> {
|
||||
fun MatrixRoom.getRoomMemberAsState(userId: UserId): State<RoomMember?> {
|
||||
val roomMembersState by membersStateFlow.collectAsState()
|
||||
return getRoomMember(roomMembersState = roomMembersState, userId = userId)
|
||||
return getRoomMemberAsState(roomMembersState = roomMembersState, userId = userId)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getRoomMember(roomMembersState: MatrixRoomMembersState, userId: UserId): State<RoomMember?> {
|
||||
fun getRoomMemberAsState(roomMembersState: MatrixRoomMembersState, userId: UserId): State<RoomMember?> {
|
||||
val roomMembers = roomMembersState.roomMembers()
|
||||
return remember(roomMembers) {
|
||||
derivedStateOf {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ dependencies {
|
|||
implementation(libs.androidx.security.crypto)
|
||||
implementation(libs.network.retrofit)
|
||||
implementation(libs.serialization.json)
|
||||
implementation(libs.coil)
|
||||
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
|
|
@ -42,6 +43,7 @@ dependencies {
|
|||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.network)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
api(projects.libraries.pushproviders.api)
|
||||
api(projects.libraries.pushstore.api)
|
||||
api(projects.libraries.push.api)
|
||||
|
|
|
|||
|
|
@ -115,8 +115,7 @@ class PushersManager @Inject constructor(
|
|||
appDisplayName = appName,
|
||||
deviceDisplayName = currentSession.sessionParams.deviceId ?: "MOBILE"
|
||||
)
|
||||
|
||||
*/
|
||||
*/
|
||||
}
|
||||
|
||||
fun getPusherForCurrentSession() {}/*: Pusher? {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
|
|
@ -49,7 +50,7 @@ class NotifiableEventProcessor @Inject constructor(
|
|||
else -> ProcessedEvent.Type.KEEP
|
||||
}
|
||||
is SimpleNotifiableEvent -> when (it.type) {
|
||||
/*EventType.REDACTION*/ "m.room.redaction" -> ProcessedEvent.Type.REMOVE
|
||||
EventType.REDACTION -> ProcessedEvent.Type.REMOVE
|
||||
else -> ProcessedEvent.Type.KEEP
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationEvent
|
||||
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
|
|
@ -44,9 +45,9 @@ class NotifiableEventResolver @Inject constructor(
|
|||
private val stringProvider: StringProvider,
|
||||
// private val noticeEventFormatter: NoticeEventFormatter,
|
||||
// private val displayableEventFormatter: DisplayableEventFormatter,
|
||||
private val clock: SystemClock,
|
||||
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val clock: SystemClock,
|
||||
) {
|
||||
|
||||
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
|
||||
|
|
@ -71,44 +72,53 @@ class NotifiableEventResolver @Inject constructor(
|
|||
|
||||
return notificationData.asNotifiableEvent(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent {
|
||||
return NotifiableMessageEvent(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
noisy = isNoisy,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
senderName = senderDisplayName,
|
||||
senderId = senderId.value,
|
||||
body = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…",
|
||||
imageUriString = null,
|
||||
threadId = null,
|
||||
roomName = null,
|
||||
roomIsDirect = false,
|
||||
roomAvatarPath = roomAvatarUrl,
|
||||
senderAvatarPath = senderAvatarUrl,
|
||||
soundName = null,
|
||||
outGoingMessage = false,
|
||||
outGoingMessageFailed = false,
|
||||
isRedacted = false,
|
||||
isUpdated = false
|
||||
)
|
||||
}
|
||||
private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent {
|
||||
return NotifiableMessageEvent(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
noisy = isNoisy,
|
||||
timestamp = event.timestamp,
|
||||
senderName = senderDisplayName,
|
||||
senderId = senderId.value,
|
||||
body = event.content,
|
||||
imageUriString = event.contentUrl,
|
||||
threadId = null,
|
||||
roomName = roomDisplayName,
|
||||
roomIsDirect = isDirect,
|
||||
roomAvatarPath = roomAvatarUrl,
|
||||
senderAvatarPath = senderAvatarUrl,
|
||||
soundName = null,
|
||||
outGoingMessage = false,
|
||||
outGoingMessageFailed = false,
|
||||
isRedacted = false,
|
||||
isUpdated = false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO This is a temporary method for EAx.
|
||||
*/
|
||||
private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
|
||||
return this ?: NotificationData(
|
||||
eventId = eventId,
|
||||
senderId = UserId("@user:domain"),
|
||||
roomId = roomId,
|
||||
isNoisy = false,
|
||||
isEncrypted = false,
|
||||
isDirect = false
|
||||
)
|
||||
/**
|
||||
* TODO This is a temporary method for EAx.
|
||||
*/
|
||||
private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
|
||||
return this ?: NotificationData(
|
||||
eventId = eventId,
|
||||
senderId = UserId("@user:domain"),
|
||||
roomId = roomId,
|
||||
senderAvatarUrl = null,
|
||||
senderDisplayName = null,
|
||||
roomAvatarUrl = null,
|
||||
roomDisplayName = null,
|
||||
isNoisy = false,
|
||||
isEncrypted = false,
|
||||
isDirect = false,
|
||||
event = NotificationEvent(
|
||||
timestamp = clock.epochMillis(),
|
||||
content = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…",
|
||||
contentUrl = null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,14 @@ package io.element.android.libraries.push.impl.notifications
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -31,30 +36,24 @@ class NotificationBitmapLoader @Inject constructor(
|
|||
|
||||
/**
|
||||
* Get icon of a room.
|
||||
* @param path mxc url
|
||||
*/
|
||||
@WorkerThread
|
||||
fun getRoomBitmap(path: String?): Bitmap? {
|
||||
suspend fun getRoomBitmap(path: String?): Bitmap? {
|
||||
if (path == null) {
|
||||
return null
|
||||
}
|
||||
return loadRoomBitmap(path)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun loadRoomBitmap(path: String): Bitmap? {
|
||||
private suspend fun loadRoomBitmap(path: String): Bitmap? {
|
||||
return try {
|
||||
null
|
||||
/* TODO Notification
|
||||
Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(path)
|
||||
.format(DecodeFormat.PREFER_ARGB_8888)
|
||||
.signature(ObjectKey("room-icon-notification"))
|
||||
.submit()
|
||||
.get()
|
||||
*/
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "decodeFile failed")
|
||||
val imageRequest = ImageRequest.Builder(context)
|
||||
.data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
|
||||
.build()
|
||||
val result = context.imageLoader.execute(imageRequest)
|
||||
result.drawable?.toBitmap()
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to load room bitmap")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -62,9 +61,9 @@ class NotificationBitmapLoader @Inject constructor(
|
|||
/**
|
||||
* Get icon of a user.
|
||||
* Before Android P, this does nothing because the icon won't be used
|
||||
* @param path mxc url
|
||||
*/
|
||||
@WorkerThread
|
||||
fun getUserIcon(path: String?): IconCompat? {
|
||||
suspend fun getUserIcon(path: String?): IconCompat? {
|
||||
if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -72,23 +71,17 @@ class NotificationBitmapLoader @Inject constructor(
|
|||
return loadUserIcon(path)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun loadUserIcon(path: String): IconCompat? {
|
||||
private suspend fun loadUserIcon(path: String): IconCompat? {
|
||||
return try {
|
||||
null
|
||||
/* TODO Notification
|
||||
val bitmap = Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(path)
|
||||
.transform(CircleCrop())
|
||||
.format(DecodeFormat.PREFER_ARGB_8888)
|
||||
.signature(ObjectKey("user-icon-notification"))
|
||||
.submit()
|
||||
.get()
|
||||
IconCompat.createWithBitmap(bitmap)
|
||||
*/
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "decodeFile failed")
|
||||
val imageRequest = ImageRequest.Builder(context)
|
||||
.data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
|
||||
.transformations(CircleCropTransformation())
|
||||
.build()
|
||||
val result = context.imageLoader.execute(imageRequest)
|
||||
val bitmap = result.drawable?.toBitmap()
|
||||
return bitmap?.let { IconCompat.createWithBitmap(it) }
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to load user bitmap")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,28 +16,28 @@
|
|||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import io.element.android.libraries.androidutils.throttler.FirstThrottler
|
||||
import io.element.android.libraries.core.cache.CircularCache
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.api.store.PushDataStore
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom
|
||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -48,7 +48,6 @@ import javax.inject.Inject
|
|||
*/
|
||||
@SingleIn(AppScope::class)
|
||||
class NotificationDrawerManager @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
private val pushDataStore: PushDataStore,
|
||||
private val notifiableEventProcessor: NotifiableEventProcessor,
|
||||
private val notificationRenderer: NotificationRenderer,
|
||||
|
|
@ -56,17 +55,14 @@ class NotificationDrawerManager @Inject constructor(
|
|||
private val filteredEventDetector: FilteredEventDetector,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||
) {
|
||||
|
||||
private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY)
|
||||
private var backgroundHandler: Handler
|
||||
|
||||
/**
|
||||
* Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events.
|
||||
*/
|
||||
private val notificationState by lazy { createInitialNotificationState() }
|
||||
private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
|
||||
private var currentAppNavigationState: AppNavigationState? = null
|
||||
private val firstThrottler = FirstThrottler(200)
|
||||
|
||||
|
|
@ -74,8 +70,6 @@ class NotificationDrawerManager @Inject constructor(
|
|||
private var useCompleteNotificationFormat = true
|
||||
|
||||
init {
|
||||
handlerThread.start()
|
||||
backgroundHandler = Handler(handlerThread.looper)
|
||||
// Observe application state
|
||||
coroutineScope.launch {
|
||||
appNavigationStateService.appNavigationStateFlow
|
||||
|
|
@ -193,30 +187,25 @@ class NotificationDrawerManager @Inject constructor(
|
|||
notificationState.updateQueuedEvents(this) { queuedEvents, _ ->
|
||||
action(queuedEvents)
|
||||
}
|
||||
refreshNotificationDrawer()
|
||||
coroutineScope.refreshNotificationDrawer()
|
||||
}
|
||||
|
||||
private fun refreshNotificationDrawer() {
|
||||
private fun CoroutineScope.refreshNotificationDrawer() = launch {
|
||||
// Implement last throttler
|
||||
val canHandle = firstThrottler.canHandle()
|
||||
Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms")
|
||||
backgroundHandler.removeCallbacksAndMessages(null)
|
||||
|
||||
backgroundHandler.postDelayed(
|
||||
{
|
||||
try {
|
||||
refreshNotificationDrawerBg()
|
||||
} catch (throwable: Throwable) {
|
||||
// It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer
|
||||
Timber.w(throwable, "refreshNotificationDrawerBg failure")
|
||||
}
|
||||
},
|
||||
canHandle.waitMillis()
|
||||
)
|
||||
withContext(dispatchers.io) {
|
||||
delay(canHandle.waitMillis())
|
||||
try {
|
||||
refreshNotificationDrawerBg()
|
||||
} catch (throwable: Throwable) {
|
||||
// It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer
|
||||
Timber.w(throwable, "refreshNotificationDrawerBg failure")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun refreshNotificationDrawerBg() {
|
||||
private suspend fun refreshNotificationDrawerBg() {
|
||||
Timber.v("refreshNotificationDrawerBg()")
|
||||
val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents ->
|
||||
notifiableEventProcessor.process(queuedEvents.rawEvents(), currentAppNavigationState, renderedEvents).also {
|
||||
|
|
@ -239,24 +228,34 @@ class NotificationDrawerManager @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun renderEvents(eventsToRender: List<ProcessedEvent<NotifiableEvent>>) {
|
||||
private suspend fun renderEvents(eventsToRender: List<ProcessedEvent<NotifiableEvent>>) {
|
||||
// Group by sessionId
|
||||
val eventsForSessions = eventsToRender.groupBy {
|
||||
it.event.sessionId
|
||||
}
|
||||
|
||||
eventsForSessions.forEach { (sessionId, notifiableEvents) ->
|
||||
// TODO EAx val user = session.getUserOrDefault(session.myUserId)
|
||||
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
|
||||
val myUserDisplayName = "Todo display name" // user.toMatrixItem().getBestName()
|
||||
// TODO EAx avatar URL
|
||||
val myUserAvatarUrl = null // session.contentUrlResolver().resolveThumbnail(
|
||||
// contentUrl = user.avatarUrl,
|
||||
// width = avatarSize,
|
||||
// height = avatarSize,
|
||||
// method = ContentUrlResolver.ThumbnailMethod.SCALE
|
||||
//)
|
||||
notificationRenderer.render(sessionId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, notifiableEvents)
|
||||
val currentUser = tryOrNull(
|
||||
onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") },
|
||||
operation = {
|
||||
val client = matrixAuthenticationService.restoreSession(sessionId).getOrNull()
|
||||
|
||||
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
|
||||
val myUserDisplayName = client?.loadUserDisplayName()?.getOrNull() ?: sessionId.value
|
||||
val userAvatarUrl = client?.loadUserAvatarURLString()?.getOrNull()
|
||||
MatrixUser(
|
||||
userId = sessionId,
|
||||
displayName = myUserDisplayName,
|
||||
avatarUrl = userAvatarUrl
|
||||
)
|
||||
}
|
||||
) ?: MatrixUser(
|
||||
userId = sessionId,
|
||||
displayName = sessionId.value,
|
||||
avatarUrl = null
|
||||
)
|
||||
|
||||
notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package io.element.android.libraries.push.impl.notifications
|
|||
|
||||
import android.app.Notification
|
||||
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.user.MatrixUser
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
|
|
@ -34,10 +34,8 @@ class NotificationFactory @Inject constructor(
|
|||
private val summaryGroupMessageCreator: SummaryGroupMessageCreator
|
||||
) {
|
||||
|
||||
fun Map<RoomId, ProcessedMessageEvents>.toNotifications(
|
||||
sessionId: SessionId,
|
||||
myUserDisplayName: String,
|
||||
myUserAvatarUrl: String?
|
||||
suspend fun Map<RoomId, ProcessedMessageEvents>.toNotifications(
|
||||
currentUser: MatrixUser,
|
||||
): List<RoomNotification> {
|
||||
return map { (roomId, events) ->
|
||||
when {
|
||||
|
|
@ -45,11 +43,9 @@ class NotificationFactory @Inject constructor(
|
|||
else -> {
|
||||
val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted }
|
||||
roomGroupMessageCreator.createRoomMessage(
|
||||
sessionId = sessionId,
|
||||
currentUser = currentUser,
|
||||
events = messageEvents,
|
||||
roomId = roomId,
|
||||
userDisplayName = myUserDisplayName,
|
||||
userAvatarUrl = myUserAvatarUrl
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -99,7 +95,7 @@ class NotificationFactory @Inject constructor(
|
|||
}
|
||||
|
||||
fun createSummaryNotification(
|
||||
sessionId: SessionId,
|
||||
currentUser: MatrixUser,
|
||||
roomNotifications: List<RoomNotification>,
|
||||
invitationNotifications: List<OneShotNotification>,
|
||||
simpleNotifications: List<OneShotNotification>,
|
||||
|
|
@ -112,7 +108,7 @@ class NotificationFactory @Inject constructor(
|
|||
roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed
|
||||
else -> SummaryNotification.Update(
|
||||
summaryGroupMessageCreator.createSummaryNotification(
|
||||
sessionId = sessionId,
|
||||
currentUser = currentUser,
|
||||
roomNotifications = roomMeta,
|
||||
invitationNotifications = invitationMeta,
|
||||
simpleNotifications = simpleMeta,
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@
|
|||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
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.user.MatrixUser
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
|
|
@ -32,21 +31,18 @@ class NotificationRenderer @Inject constructor(
|
|||
private val notificationFactory: NotificationFactory,
|
||||
) {
|
||||
|
||||
@WorkerThread
|
||||
fun render(
|
||||
sessionId: SessionId,
|
||||
myUserDisplayName: String,
|
||||
myUserAvatarUrl: String?,
|
||||
suspend fun render(
|
||||
currentUser: MatrixUser,
|
||||
useCompleteNotificationFormat: Boolean,
|
||||
eventsToProcess: List<ProcessedEvent<NotifiableEvent>>
|
||||
) {
|
||||
val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType()
|
||||
with(notificationFactory) {
|
||||
val roomNotifications = roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl)
|
||||
val roomNotifications = roomEvents.toNotifications(currentUser)
|
||||
val invitationNotifications = invitationEvents.toNotifications()
|
||||
val simpleNotifications = simpleEvents.toNotifications()
|
||||
val summaryNotification = createSummaryNotification(
|
||||
sessionId = sessionId,
|
||||
currentUser = currentUser,
|
||||
roomNotifications = roomNotifications,
|
||||
invitationNotifications = invitationNotifications,
|
||||
simpleNotifications = simpleNotifications,
|
||||
|
|
@ -56,21 +52,27 @@ class NotificationRenderer @Inject constructor(
|
|||
// Remove summary first to avoid briefly displaying it after dismissing the last notification
|
||||
if (summaryNotification == SummaryNotification.Removed) {
|
||||
Timber.d("Removing summary notification")
|
||||
notificationDisplayer.cancelNotificationMessage(null, notificationIdProvider.getSummaryNotificationId(sessionId))
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = null,
|
||||
id = notificationIdProvider.getSummaryNotificationId(currentUser.userId)
|
||||
)
|
||||
}
|
||||
|
||||
roomNotifications.forEach { wrapper ->
|
||||
when (wrapper) {
|
||||
is RoomNotification.Removed -> {
|
||||
Timber.d("Removing room messages notification ${wrapper.roomId}")
|
||||
notificationDisplayer.cancelNotificationMessage(wrapper.roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = wrapper.roomId.value,
|
||||
id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId)
|
||||
)
|
||||
}
|
||||
is RoomNotification.Message -> if (useCompleteNotificationFormat) {
|
||||
Timber.d("Updating room messages notification ${wrapper.meta.roomId}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
wrapper.meta.roomId.value,
|
||||
notificationIdProvider.getRoomMessagesNotificationId(sessionId),
|
||||
wrapper.notification
|
||||
tag = wrapper.meta.roomId.value,
|
||||
id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
|
||||
notification = wrapper.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -80,14 +82,17 @@ class NotificationRenderer @Inject constructor(
|
|||
when (wrapper) {
|
||||
is OneShotNotification.Removed -> {
|
||||
Timber.d("Removing invitation notification ${wrapper.key}")
|
||||
notificationDisplayer.cancelNotificationMessage(wrapper.key, notificationIdProvider.getRoomInvitationNotificationId(sessionId))
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = wrapper.key,
|
||||
id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId)
|
||||
)
|
||||
}
|
||||
is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
|
||||
Timber.d("Updating invitation notification ${wrapper.meta.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
wrapper.meta.key,
|
||||
notificationIdProvider.getRoomInvitationNotificationId(sessionId),
|
||||
wrapper.notification
|
||||
tag = wrapper.meta.key,
|
||||
id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
|
||||
notification = wrapper.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -97,14 +102,17 @@ class NotificationRenderer @Inject constructor(
|
|||
when (wrapper) {
|
||||
is OneShotNotification.Removed -> {
|
||||
Timber.d("Removing simple notification ${wrapper.key}")
|
||||
notificationDisplayer.cancelNotificationMessage(wrapper.key, notificationIdProvider.getRoomEventNotificationId(sessionId))
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = wrapper.key,
|
||||
id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId)
|
||||
)
|
||||
}
|
||||
is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
|
||||
Timber.d("Updating simple notification ${wrapper.meta.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
wrapper.meta.key,
|
||||
notificationIdProvider.getRoomEventNotificationId(sessionId),
|
||||
wrapper.notification
|
||||
tag = wrapper.meta.key,
|
||||
id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId),
|
||||
notification = wrapper.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -114,9 +122,9 @@ class NotificationRenderer @Inject constructor(
|
|||
if (summaryNotification is SummaryNotification.Update) {
|
||||
Timber.d("Updating summary notification")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
null,
|
||||
notificationIdProvider.getSummaryNotificationId(sessionId),
|
||||
summaryNotification.notification
|
||||
tag = null,
|
||||
id = notificationIdProvider.getSummaryNotificationId(currentUser.userId),
|
||||
notification = summaryNotification.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ import android.graphics.Bitmap
|
|||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
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.user.MatrixUser
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
|
|
@ -36,24 +37,22 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
private val notificationFactory: NotificationFactory
|
||||
) {
|
||||
|
||||
fun createRoomMessage(
|
||||
sessionId: SessionId,
|
||||
suspend fun createRoomMessage(
|
||||
currentUser: MatrixUser,
|
||||
events: List<NotifiableMessageEvent>,
|
||||
roomId: RoomId,
|
||||
userDisplayName: String,
|
||||
userAvatarUrl: String?
|
||||
): RoomNotification.Message {
|
||||
val lastKnownRoomEvent = events.last()
|
||||
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "Room name (${roomId.value.take(8)}…)"
|
||||
val roomIsGroup = !lastKnownRoomEvent.roomIsDirect
|
||||
val style = NotificationCompat.MessagingStyle(
|
||||
Person.Builder()
|
||||
.setName(userDisplayName)
|
||||
.setIcon(bitmapLoader.getUserIcon(userAvatarUrl))
|
||||
.setName(currentUser.displayName?.annotateForDebug(50))
|
||||
.setIcon(bitmapLoader.getUserIcon(currentUser.avatarUrl))
|
||||
.setKey(lastKnownRoomEvent.sessionId.value)
|
||||
.build()
|
||||
).also {
|
||||
it.conversationTitle = roomName.takeIf { roomIsGroup }
|
||||
it.conversationTitle = roomName.takeIf { roomIsGroup }?.annotateForDebug(51)
|
||||
it.isGroupConversation = roomIsGroup
|
||||
it.addMessagesFromEvents(events)
|
||||
}
|
||||
|
|
@ -80,7 +79,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
notificationFactory.createMessagesListNotification(
|
||||
style,
|
||||
RoomEventGroupInfo(
|
||||
sessionId = sessionId,
|
||||
sessionId = currentUser.userId,
|
||||
roomId = roomId,
|
||||
roomDisplayName = roomName,
|
||||
isDirect = !roomIsGroup,
|
||||
|
|
@ -99,13 +98,13 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List<NotifiableMessageEvent>) {
|
||||
private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List<NotifiableMessageEvent>) {
|
||||
events.forEach { event ->
|
||||
val senderPerson = if (event.outGoingMessage) {
|
||||
null
|
||||
} else {
|
||||
Person.Builder()
|
||||
.setName(event.senderName)
|
||||
.setName(event.senderName?.annotateForDebug(70))
|
||||
.setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath))
|
||||
.setKey(event.senderId)
|
||||
.build()
|
||||
|
|
@ -117,7 +116,11 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
senderPerson
|
||||
)
|
||||
else -> {
|
||||
val message = NotificationCompat.MessagingStyle.Message(event.body, event.timestamp, senderPerson).also { message ->
|
||||
val message = NotificationCompat.MessagingStyle.Message(
|
||||
event.body?.annotateForDebug(71),
|
||||
event.timestamp,
|
||||
senderPerson
|
||||
).also { message ->
|
||||
event.imageUri?.let {
|
||||
message.setData("image/", it)
|
||||
}
|
||||
|
|
@ -168,7 +171,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getRoomBitmap(events: List<NotifiableMessageEvent>): Bitmap? {
|
||||
private suspend fun getRoomBitmap(events: List<NotifiableMessageEvent>): Bitmap? {
|
||||
// Use the last event (most recent?)
|
||||
return events.lastOrNull()
|
||||
?.roomAvatarPath
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ package io.element.android.libraries.push.impl.notifications
|
|||
|
||||
import android.app.Notification
|
||||
import androidx.core.app.NotificationCompat
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
|
@ -40,20 +41,20 @@ import javax.inject.Inject
|
|||
*/
|
||||
class SummaryGroupMessageCreator @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val notificationFactory: NotificationFactory
|
||||
private val notificationFactory: NotificationFactory,
|
||||
) {
|
||||
|
||||
fun createSummaryNotification(
|
||||
sessionId: SessionId,
|
||||
currentUser: MatrixUser,
|
||||
roomNotifications: List<RoomNotification.Message.Meta>,
|
||||
invitationNotifications: List<OneShotNotification.Append.Meta>,
|
||||
simpleNotifications: List<OneShotNotification.Append.Meta>,
|
||||
useCompleteNotificationFormat: Boolean
|
||||
): Notification {
|
||||
val summaryInboxStyle = NotificationCompat.InboxStyle().also { style ->
|
||||
roomNotifications.forEach { style.addLine(it.summaryLine) }
|
||||
invitationNotifications.forEach { style.addLine(it.summaryLine) }
|
||||
simpleNotifications.forEach { style.addLine(it.summaryLine) }
|
||||
roomNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(40)) }
|
||||
invitationNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(41)) }
|
||||
simpleNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(42)) }
|
||||
}
|
||||
|
||||
val summaryIsNoisy = roomNotifications.any { it.shouldBing } ||
|
||||
|
|
@ -69,12 +70,13 @@ class SummaryGroupMessageCreator @Inject constructor(
|
|||
// FIXME roomIdToEventMap.size is not correct, this is the number of rooms
|
||||
val nbEvents = roomNotifications.size + simpleNotifications.size
|
||||
val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
|
||||
summaryInboxStyle.setBigContentTitle(sumTitle)
|
||||
// TODO get latest event?
|
||||
.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
|
||||
summaryInboxStyle.setBigContentTitle(sumTitle.annotateForDebug(43))
|
||||
//.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents).annotateForDebug(44))
|
||||
// Use account name now, for multi-session
|
||||
.setSummaryText(currentUser.userId.value.annotateForDebug(44))
|
||||
return if (useCompleteNotificationFormat) {
|
||||
notificationFactory.createSummaryListNotification(
|
||||
sessionId,
|
||||
currentUser,
|
||||
summaryInboxStyle,
|
||||
sumTitle,
|
||||
noisy = summaryIsNoisy,
|
||||
|
|
@ -82,7 +84,7 @@ class SummaryGroupMessageCreator @Inject constructor(
|
|||
)
|
||||
} else {
|
||||
processSimpleGroupSummary(
|
||||
sessionId,
|
||||
currentUser,
|
||||
summaryIsNoisy,
|
||||
messageCount,
|
||||
simpleNotifications.size,
|
||||
|
|
@ -94,7 +96,7 @@ class SummaryGroupMessageCreator @Inject constructor(
|
|||
}
|
||||
|
||||
private fun processSimpleGroupSummary(
|
||||
sessionId: SessionId,
|
||||
currentUser: MatrixUser,
|
||||
summaryIsNoisy: Boolean,
|
||||
messageEventsCount: Int,
|
||||
simpleEventsCount: Int,
|
||||
|
|
@ -167,7 +169,7 @@ class SummaryGroupMessageCreator @Inject constructor(
|
|||
}
|
||||
}
|
||||
return notificationFactory.createSummaryListNotification(
|
||||
sessionId = sessionId,
|
||||
currentUser = currentUser,
|
||||
style = null,
|
||||
compatSummary = privacyTitle,
|
||||
noisy = summaryIsNoisy,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.debug
|
||||
|
||||
fun CharSequence.annotateForDebug(@Suppress("UNUSED_PARAMETER") prefix: Int): CharSequence {
|
||||
return this // "$prefix-$this"
|
||||
}
|
||||
|
|
@ -26,11 +26,12 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.content.res.ResourcesCompat
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory
|
||||
|
|
@ -84,16 +85,16 @@ class NotificationFactory @Inject constructor(
|
|||
// ID of the corresponding shortcut, for conversation features under API 30+
|
||||
.setShortcutId(roomInfo.roomId.value)
|
||||
// Title for API < 16 devices.
|
||||
.setContentTitle(roomInfo.roomDisplayName)
|
||||
.setContentTitle(roomInfo.roomDisplayName.annotateForDebug(1))
|
||||
// Content for API < 16 devices.
|
||||
.setContentText(stringProvider.getString(R.string.notification_new_messages))
|
||||
.setContentText(stringProvider.getString(R.string.notification_new_messages).annotateForDebug(2))
|
||||
// Number of new notifications for API <24 (M and below) devices.
|
||||
.setSubText(
|
||||
stringProvider.getQuantityString(
|
||||
R.plurals.notification_new_messages_for_room,
|
||||
messageStyle.messages.size,
|
||||
messageStyle.messages.size
|
||||
)
|
||||
).annotateForDebug(3)
|
||||
)
|
||||
// Auto-bundling is enabled for 4 or more notifications on API 24+ (N+)
|
||||
// devices and all Wear devices. But we want a custom grouping, so we specify the groupID
|
||||
|
|
@ -135,7 +136,7 @@ class NotificationFactory @Inject constructor(
|
|||
}
|
||||
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
|
||||
}
|
||||
.setTicker(tickerText)
|
||||
.setTicker(tickerText.annotateForDebug(4))
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
@ -147,8 +148,8 @@ class NotificationFactory @Inject constructor(
|
|||
val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName)
|
||||
.setContentText(inviteNotifiableEvent.description)
|
||||
.setContentTitle((inviteNotifiableEvent.roomName ?: buildMeta.applicationName).annotateForDebug(5))
|
||||
.setContentText(inviteNotifiableEvent.description.annotateForDebug(6))
|
||||
.setGroup(inviteNotifiableEvent.sessionId.value)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||
.setSmallIcon(smallIcon)
|
||||
|
|
@ -196,8 +197,8 @@ class NotificationFactory @Inject constructor(
|
|||
val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentTitle(buildMeta.applicationName)
|
||||
.setContentText(simpleNotifiableEvent.description)
|
||||
.setContentTitle(buildMeta.applicationName.annotateForDebug(7))
|
||||
.setContentText(simpleNotifiableEvent.description.annotateForDebug(8))
|
||||
.setGroup(simpleNotifiableEvent.sessionId.value)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||
.setSmallIcon(smallIcon)
|
||||
|
|
@ -226,7 +227,7 @@ class NotificationFactory @Inject constructor(
|
|||
* Create the summary notification.
|
||||
*/
|
||||
fun createSummaryListNotification(
|
||||
sessionId: SessionId,
|
||||
currentUser: MatrixUser,
|
||||
style: NotificationCompat.InboxStyle?,
|
||||
compatSummary: String,
|
||||
noisy: Boolean,
|
||||
|
|
@ -240,12 +241,12 @@ class NotificationFactory @Inject constructor(
|
|||
// used in compat < N, after summary is built based on child notifications
|
||||
.setWhen(lastMessageTimestamp)
|
||||
.setStyle(style)
|
||||
.setContentTitle(sessionId.value)
|
||||
.setContentTitle(currentUser.userId.value.annotateForDebug(9))
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setSmallIcon(smallIcon)
|
||||
// set content text to support devices running API level < 24
|
||||
.setContentText(compatSummary)
|
||||
.setGroup(sessionId.value)
|
||||
.setContentText(compatSummary.annotateForDebug(10))
|
||||
.setGroup(currentUser.userId.value)
|
||||
// set this notification as the summary for the group
|
||||
.setGroupSummary(true)
|
||||
.setColor(accentColor)
|
||||
|
|
@ -264,8 +265,8 @@ class NotificationFactory @Inject constructor(
|
|||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
}
|
||||
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(sessionId))
|
||||
.setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(sessionId))
|
||||
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(currentUser.userId))
|
||||
.setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(currentUser.userId))
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||
import io.element.android.services.appnavstate.api.currentRoomId
|
||||
import io.element.android.services.appnavstate.api.currentSessionId
|
||||
|
|
@ -52,10 +53,13 @@ data class NotifiableMessageEvent(
|
|||
override val isUpdated: Boolean = false
|
||||
) : NotifiableEvent {
|
||||
|
||||
val type: String = /* EventType.MESSAGE */ "m.room.message"
|
||||
val type: String = EventType.MESSAGE
|
||||
val description: String = body ?: ""
|
||||
val title: String = senderName ?: ""
|
||||
|
||||
// TODO EAx The image has to be downloaded and expose using the file provider.
|
||||
// Example of value from Element Android:
|
||||
// content://im.vector.app.debug.mx-sdk.fileprovider/downloads/downloads/816abf76d806c768760568952b1862c8/F/72c33edd23dee3b95f4d5a18aa25fa54/image.png
|
||||
val imageUri: Uri?
|
||||
get() = imageUriString?.let { Uri.parse(it) }
|
||||
}
|
||||
|
|
|
|||
28
libraries/push/impl/src/main/res/values-cs/translations.xml
Normal file
28
libraries/push/impl/src/main/res/values-cs/translations.xml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="notification_channel_call">"Hovor"</string>
|
||||
<string name="notification_channel_listening_for_events">"Naslouchání událostem"</string>
|
||||
<string name="notification_channel_noisy">"Hlasitá oznámení"</string>
|
||||
<string name="notification_channel_silent">"Tichá oznámení"</string>
|
||||
<string name="notification_invitation_action_join">"Vstoupit"</string>
|
||||
<string name="notification_invitation_action_reject">"Odmítnout"</string>
|
||||
<string name="notification_new_messages">"Nové zprávy"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Označit jako přečtené"</string>
|
||||
<string name="notification_sender_me">"Já"</string>
|
||||
<string name="notification_test_push_notification_content">"Prohlížíte si oznámení! Klikněte na mě!"</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>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s a %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room">"%1$s in %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s v %2$s a %3$s"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d zpráva"</item>
|
||||
<item quantity="few">"%1$s: %2$d zprávy"</item>
|
||||
<item quantity="other">"%1$s: %2$d zpráv"</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>
|
||||
<string name="notification_room_action_quick_reply">"Rychlá odpověď"</string>
|
||||
</resources>
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="notification_channel_call">"Anruf"</string>
|
||||
<string name="notification_channel_listening_for_events">"Warte auf Ereignisse"</string>
|
||||
<string name="notification_channel_noisy">"Laute Benachrichtigungen"</string>
|
||||
<string name="notification_channel_silent">"Stumme Benachrichtigungen"</string>
|
||||
<string name="notification_inline_reply_failed">"** Senden fehlgeschlagen - bitte Raum öffnen"</string>
|
||||
<string name="notification_invitation_action_join">"Beitreten"</string>
|
||||
<string name="notification_invitation_action_reject">"Ablehnen"</string>
|
||||
<string name="notification_new_messages">"Neue Nachrichten"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Als gelesen markieren"</string>
|
||||
<string name="notification_sender_me">"Ich"</string>
|
||||
<string name="notification_test_push_notification_content">"Du siehst die Benachrichtigung an! Klick mich an!"</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>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s und %2$s"</string>
|
||||
|
|
@ -26,10 +32,16 @@
|
|||
<item quantity="one">"%d neue Nachricht"</item>
|
||||
<item quantity="other">"%d neue Nachrichten"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
<item quantity="one">"%d ungelesene benachrichtigte Nachricht"</item>
|
||||
<item quantity="other">"%d ungelesene benachrichtigte Nachrichten"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="one">"%d Raum"</item>
|
||||
<item quantity="other">"%d Räume"</item>
|
||||
</plurals>
|
||||
<string name="push_choose_distributor_dialog_title_android">"Auswählen, wie Benachrichtigungen empfangen werden sollen"</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>
|
||||
<string name="notification_room_action_quick_reply">"Schnellantwort"</string>
|
||||
|
|
|
|||
48
libraries/push/impl/src/main/res/values-fr/translations.xml
Normal file
48
libraries/push/impl/src/main/res/values-fr/translations.xml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="notification_channel_call">"Appel"</string>
|
||||
<string name="notification_channel_listening_for_events">"À l\'écoute d\'événements"</string>
|
||||
<string name="notification_channel_noisy">"Notifications bruyantes"</string>
|
||||
<string name="notification_channel_silent">"Notifications silencieuses"</string>
|
||||
<string name="notification_inline_reply_failed">"** Échec de l\'envoi - veuillez ouvrir la salle"</string>
|
||||
<string name="notification_invitation_action_join">"Rejoindre"</string>
|
||||
<string name="notification_invitation_action_reject">"Refuser"</string>
|
||||
<string name="notification_new_messages">"Nouveaux messages"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Marquer comme lu"</string>
|
||||
<string name="notification_sender_me">"Moi"</string>
|
||||
<string name="notification_test_push_notification_content">"Vous êtes en train de consulter la notification ! Cliquez-moi !"</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>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s et %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room">"%1$s dans %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s dans %2$s et %3$s"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d message"</item>
|
||||
<item quantity="other">"%1$s: %2$d messages"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_compat_summary_title">
|
||||
<item quantity="one">"%d notification"</item>
|
||||
<item quantity="other">"%d notifications"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="one">"%d invitation"</item>
|
||||
<item quantity="other">"%d invitations"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
<item quantity="one">"%d nouveau message"</item>
|
||||
<item quantity="other">"%d nouveaux messages"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
<item quantity="one">"%d message notifié non lu"</item>
|
||||
<item quantity="other">"%d messages notifiés non lus"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="one">"%d conversation"</item>
|
||||
<item quantity="other">"%d conversations"</item>
|
||||
</plurals>
|
||||
<string name="push_choose_distributor_dialog_title_android">"Choisissez comment recevoir les 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 n\'a été trouvé. Les notifications peuvent ne pas fonctionner correctement."</string>
|
||||
<string name="notification_room_action_quick_reply">"Réponse rapide"</string>
|
||||
</resources>
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
|
|
@ -60,7 +61,7 @@ class NotifiableEventProcessorTest {
|
|||
|
||||
@Test
|
||||
fun `given redacted simple event when processing then remove redaction event`() {
|
||||
val events = listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID, type = "m.room.redaction"))
|
||||
val events = listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID, type = EventType.REDACTION))
|
||||
|
||||
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.notifications
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
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_SESSION_ID
|
||||
|
|
@ -27,6 +28,7 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGrou
|
|||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
private val MY_AVATAR_URL: String? = null
|
||||
|
|
@ -124,11 +126,13 @@ class NotificationFactoryTest {
|
|||
fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationFactory) {
|
||||
val events = listOf(A_MESSAGE_EVENT)
|
||||
val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(
|
||||
A_SESSION_ID, events, A_ROOM_ID, A_SESSION_ID.value, MY_AVATAR_URL
|
||||
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), events, A_ROOM_ID
|
||||
)
|
||||
val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT)))
|
||||
|
||||
val result = roomWithMessage.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
|
||||
val result = roomWithMessage.toNotifications(
|
||||
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(listOf(expectedNotification))
|
||||
}
|
||||
|
|
@ -138,7 +142,9 @@ class NotificationFactoryTest {
|
|||
val events = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_MESSAGE_EVENT))
|
||||
val emptyRoom = mapOf(A_ROOM_ID to events)
|
||||
|
||||
val result = emptyRoom.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
|
||||
val result = emptyRoom.toNotifications(
|
||||
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
|
|
@ -153,7 +159,9 @@ class NotificationFactoryTest {
|
|||
fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) {
|
||||
val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true))))
|
||||
|
||||
val result = redactedRoom.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
|
||||
val result = redactedRoom.toNotifications(
|
||||
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
|
|
@ -176,19 +184,21 @@ class NotificationFactoryTest {
|
|||
)
|
||||
val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted")))
|
||||
val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(
|
||||
A_SESSION_ID,
|
||||
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
|
||||
withRedactedRemoved,
|
||||
A_ROOM_ID,
|
||||
A_SESSION_ID.value,
|
||||
MY_AVATAR_URL
|
||||
)
|
||||
|
||||
val result = roomWithRedactedMessage.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
|
||||
val result = roomWithRedactedMessage.toNotifications(
|
||||
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(listOf(expectedNotification))
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> testWith(receiver: T, block: T.() -> Unit) {
|
||||
receiver.block()
|
||||
fun <T> testWith(receiver: T, block: suspend T.() -> Unit) {
|
||||
runTest {
|
||||
receiver.block()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import android.app.Notification
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
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_SESSION_ID
|
||||
|
|
@ -24,6 +25,7 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeNotificatio
|
|||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationFactory
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
private const val MY_USER_DISPLAY_NAME = "display-name"
|
||||
|
|
@ -53,7 +55,7 @@ class NotificationRendererTest {
|
|||
)
|
||||
|
||||
@Test
|
||||
fun `given no notifications when rendering then cancels summary notification`() {
|
||||
fun `given no notifications when rendering then cancels summary notification`() = runTest {
|
||||
givenNoNotifications()
|
||||
|
||||
renderEventsAsNotifications()
|
||||
|
|
@ -63,7 +65,7 @@ class NotificationRendererTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() {
|
||||
fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() = runTest {
|
||||
givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
|
||||
|
||||
renderEventsAsNotifications()
|
||||
|
|
@ -75,7 +77,7 @@ class NotificationRendererTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() {
|
||||
fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() = runTest {
|
||||
givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)))
|
||||
|
||||
renderEventsAsNotifications()
|
||||
|
|
@ -87,7 +89,7 @@ class NotificationRendererTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given a room message group notification is added when rendering then show the message notification and update summary`() {
|
||||
fun `given a room message group notification is added when rendering then show the message notification and update summary`() = runTest {
|
||||
givenNotifications(
|
||||
roomNotifications = listOf(
|
||||
RoomNotification.Message(
|
||||
|
|
@ -106,7 +108,7 @@ class NotificationRendererTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() {
|
||||
fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() = runTest {
|
||||
givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
|
||||
|
||||
renderEventsAsNotifications()
|
||||
|
|
@ -118,7 +120,7 @@ class NotificationRendererTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() {
|
||||
fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() = runTest {
|
||||
givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)))
|
||||
|
||||
renderEventsAsNotifications()
|
||||
|
|
@ -130,7 +132,7 @@ class NotificationRendererTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given a simple notification is added when rendering then show the simple notification and update summary`() {
|
||||
fun `given a simple notification is added when rendering then show the simple notification and update summary`() = runTest {
|
||||
givenNotifications(
|
||||
simpleNotifications = listOf(
|
||||
OneShotNotification.Append(
|
||||
|
|
@ -149,7 +151,7 @@ class NotificationRendererTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() {
|
||||
fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() = runTest {
|
||||
givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
|
||||
|
||||
renderEventsAsNotifications()
|
||||
|
|
@ -161,7 +163,7 @@ class NotificationRendererTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() {
|
||||
fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() = runTest {
|
||||
givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)))
|
||||
|
||||
renderEventsAsNotifications()
|
||||
|
|
@ -173,7 +175,7 @@ class NotificationRendererTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() {
|
||||
fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() = runTest {
|
||||
givenNotifications(
|
||||
simpleNotifications = listOf(
|
||||
OneShotNotification.Append(
|
||||
|
|
@ -191,11 +193,9 @@ class NotificationRendererTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun renderEventsAsNotifications() {
|
||||
private suspend fun renderEventsAsNotifications() {
|
||||
notificationRenderer.render(
|
||||
sessionId = A_SESSION_ID,
|
||||
myUserDisplayName = MY_USER_DISPLAY_NAME,
|
||||
myUserAvatarUrl = MY_USER_AVATAR_URL,
|
||||
MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
|
||||
useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT,
|
||||
eventsToProcess = AN_EVENT_LIST
|
||||
)
|
||||
|
|
@ -214,9 +214,7 @@ class NotificationRendererTest {
|
|||
) {
|
||||
notificationFactory.givenNotificationsFor(
|
||||
groupedEvents = A_PROCESSED_EVENTS,
|
||||
sessionId = A_SESSION_ID,
|
||||
myUserDisplayName = MY_USER_DISPLAY_NAME,
|
||||
myUserAvatarUrl = MY_USER_AVATAR_URL,
|
||||
matrixUser = MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
|
||||
useCompleteNotificationFormat = useCompleteNotificationFormat,
|
||||
roomNotifications = roomNotifications,
|
||||
invitationNotifications = invitationNotifications,
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@
|
|||
|
||||
package io.element.android.libraries.push.impl.notifications.fake
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.impl.notifications.GroupedNotificationEvents
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationFactory
|
||||
import io.element.android.libraries.push.impl.notifications.OneShotNotification
|
||||
import io.element.android.libraries.push.impl.notifications.RoomNotification
|
||||
import io.element.android.libraries.push.impl.notifications.SummaryNotification
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
||||
|
|
@ -30,9 +31,7 @@ class FakeNotificationFactory {
|
|||
|
||||
fun givenNotificationsFor(
|
||||
groupedEvents: GroupedNotificationEvents,
|
||||
sessionId: SessionId,
|
||||
myUserDisplayName: String,
|
||||
myUserAvatarUrl: String?,
|
||||
matrixUser: MatrixUser,
|
||||
useCompleteNotificationFormat: Boolean,
|
||||
roomNotifications: List<RoomNotification>,
|
||||
invitationNotifications: List<OneShotNotification>,
|
||||
|
|
@ -40,13 +39,13 @@ class FakeNotificationFactory {
|
|||
summaryNotification: SummaryNotification
|
||||
) {
|
||||
with(instance) {
|
||||
every { groupedEvents.roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl) } returns roomNotifications
|
||||
coEvery { groupedEvents.roomEvents.toNotifications(matrixUser) } returns roomNotifications
|
||||
every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications
|
||||
every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications
|
||||
|
||||
every {
|
||||
createSummaryNotification(
|
||||
sessionId,
|
||||
matrixUser,
|
||||
roomNotifications,
|
||||
invitationNotifications,
|
||||
simpleNotifications,
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
package io.element.android.libraries.push.impl.notifications.fake
|
||||
|
||||
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.user.MatrixUser
|
||||
import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator
|
||||
import io.element.android.libraries.push.impl.notifications.RoomNotification
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.mockk.every
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
|
||||
class FakeRoomGroupMessageCreator {
|
||||
|
|
@ -29,14 +29,18 @@ class FakeRoomGroupMessageCreator {
|
|||
val instance = mockk<RoomGroupMessageCreator>()
|
||||
|
||||
fun givenCreatesRoomMessageFor(
|
||||
sessionId: SessionId,
|
||||
matrixUser: MatrixUser,
|
||||
events: List<NotifiableMessageEvent>,
|
||||
roomId: RoomId,
|
||||
userDisplayName: String,
|
||||
userAvatarUrl: String?
|
||||
): RoomNotification.Message {
|
||||
val mockMessage = mockk<RoomNotification.Message>()
|
||||
every { instance.createRoomMessage(sessionId, events, roomId, userDisplayName, userAvatarUrl) } returns mockMessage
|
||||
coEvery {
|
||||
instance.createRoomMessage(
|
||||
currentUser = matrixUser,
|
||||
events = events,
|
||||
roomId = roomId,
|
||||
)
|
||||
} returns mockMessage
|
||||
return mockMessage
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ object SessionStorageModule {
|
|||
@SingleIn(AppScope::class)
|
||||
fun provideMatrixDatabase(@ApplicationContext context: Context): SessionDatabase {
|
||||
val name = "session_database"
|
||||
val secretFile = context.getDatabasePath("$name.key")
|
||||
val secretFile = context.getDatabasePath("${name}.key")
|
||||
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile)
|
||||
val driver = SqlCipherDriverFactory(passphraseProvider)
|
||||
.create(SessionDatabase.Schema, "$name.db", context)
|
||||
.create(SessionDatabase.Schema, "${name}.db", context)
|
||||
return SessionDatabase(driver)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="rich_text_editor_composer_placeholder">"Zpráva…"</string>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="rich_text_editor_bullet_list">"Aufzählungsliste ein-/ausschalten"</string>
|
||||
<string name="rich_text_editor_code_block">"Codeblock umschalten"</string>
|
||||
<string name="rich_text_editor_composer_placeholder">"Nachricht…"</string>
|
||||
<string name="rich_text_editor_format_bold">"Fettformatierung anwenden"</string>
|
||||
<string name="rich_text_editor_format_italic">"Kursivformat anwenden"</string>
|
||||
<string name="rich_text_editor_format_strikethrough">"Durchgestrichenes Format anwenden"</string>
|
||||
<string name="rich_text_editor_format_underline">"Unterstreichungsformat anwenden"</string>
|
||||
<string name="rich_text_editor_full_screen_toggle">"Vollbildmodus umschalten"</string>
|
||||
<string name="rich_text_editor_indent">"Einrücken"</string>
|
||||
<string name="rich_text_editor_inline_code">"Inline-Codeformat anwenden"</string>
|
||||
<string name="rich_text_editor_link">"Link setzen"</string>
|
||||
<string name="rich_text_editor_numbered_list">"Nummerierte Liste ein-/ausschalten"</string>
|
||||
<string name="rich_text_editor_quote">"Zitat umschalten"</string>
|
||||
<string name="rich_text_editor_unindent">"Einrücken aufheben"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="rich_text_editor_bullet_list">"Afficher une liste à puces"</string>
|
||||
<string name="rich_text_editor_code_block">"Afficher le bloc de code"</string>
|
||||
<string name="rich_text_editor_composer_placeholder">"Envoyer un message…"</string>
|
||||
<string name="rich_text_editor_format_bold">"Appliquer le format gras"</string>
|
||||
<string name="rich_text_editor_format_italic">"Appliquer le format italique"</string>
|
||||
<string name="rich_text_editor_format_strikethrough">"Appliquer le format barré"</string>
|
||||
<string name="rich_text_editor_format_underline">"Appliquer le format souligné"</string>
|
||||
<string name="rich_text_editor_full_screen_toggle">"Afficher en mode plein écran"</string>
|
||||
<string name="rich_text_editor_indent">"Décaler vers la droite"</string>
|
||||
<string name="rich_text_editor_inline_code">"Appliquer le formatage de code en ligne"</string>
|
||||
<string name="rich_text_editor_link">"Définir un lien"</string>
|
||||
<string name="rich_text_editor_numbered_list">"Afficher une liste numérotée"</string>
|
||||
<string name="rich_text_editor_quote">"Afficher une citation"</string>
|
||||
<string name="rich_text_editor_unindent">"Décaler vers la gauche"</string>
|
||||
</resources>
|
||||
143
libraries/ui-strings/src/main/res/values-cs/translations.xml
Normal file
143
libraries/ui-strings/src/main/res/values-cs/translations.xml
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_hide_password">"Skrýt heslo"</string>
|
||||
<string name="a11y_send_files">"Odeslat soubory"</string>
|
||||
<string name="a11y_show_password">"Zobrazit heslo"</string>
|
||||
<string name="a11y_user_menu">"Uživatelské menu"</string>
|
||||
<string name="action_accept">"Přijmout"</string>
|
||||
<string name="action_back">"Zpět"</string>
|
||||
<string name="action_cancel">"Zrušit"</string>
|
||||
<string name="action_choose_photo">"Vybrat fotku"</string>
|
||||
<string name="action_clear">"Vymazat"</string>
|
||||
<string name="action_close">"Zavřít"</string>
|
||||
<string name="action_complete_verification">"Dokončit ověření"</string>
|
||||
<string name="action_confirm">"Potvrdit"</string>
|
||||
<string name="action_continue">"Pokračovat"</string>
|
||||
<string name="action_copy">"Kopírovat"</string>
|
||||
<string name="action_copy_link">"Kopírovat odkaz"</string>
|
||||
<string name="action_create">"Vytvořit"</string>
|
||||
<string name="action_create_a_room">"Vytvořit místnost"</string>
|
||||
<string name="action_decline">"Odmítnout"</string>
|
||||
<string name="action_disable">"Zakázat"</string>
|
||||
<string name="action_done">"Hotovo"</string>
|
||||
<string name="action_edit">"Upravit"</string>
|
||||
<string name="action_enable">"Povolit"</string>
|
||||
<string name="action_invite">"Pozvat"</string>
|
||||
<string name="action_invite_friends_to_app">"Pozvat přátele do %1$s"</string>
|
||||
<string name="action_invites_list">"Pozvánky"</string>
|
||||
<string name="action_learn_more">"Zjistit více"</string>
|
||||
<string name="action_leave">"Odejít"</string>
|
||||
<string name="action_leave_room">"Opustit místnost"</string>
|
||||
<string name="action_next">"Další"</string>
|
||||
<string name="action_no">"Ne"</string>
|
||||
<string name="action_not_now">"Teď ne"</string>
|
||||
<string name="action_ok">"OK"</string>
|
||||
<string name="action_quick_reply">"Rychlá odpověď"</string>
|
||||
<string name="action_quote">"Citovat"</string>
|
||||
<string name="action_remove">"Odstranit"</string>
|
||||
<string name="action_reply">"Odpovědět"</string>
|
||||
<string name="action_report_bug">"Nahlásit chybu"</string>
|
||||
<string name="action_report_content">"Nahlásit obsah"</string>
|
||||
<string name="action_retry">"Zkusit znovu"</string>
|
||||
<string name="action_retry_decryption">"Opakovat dešifrování"</string>
|
||||
<string name="action_save">"Uložit"</string>
|
||||
<string name="action_search">"Hledat"</string>
|
||||
<string name="action_send">"Odeslat"</string>
|
||||
<string name="action_send_message">"Odeslat zprávu"</string>
|
||||
<string name="action_share">"Sdílet"</string>
|
||||
<string name="action_share_link">"Sdílet odkaz"</string>
|
||||
<string name="action_skip">"Přeskočit"</string>
|
||||
<string name="action_start">"Začít"</string>
|
||||
<string name="action_start_chat">"Zahájit chat"</string>
|
||||
<string name="action_start_verification">"Zahájit ověření"</string>
|
||||
<string name="action_take_photo">"Vyfotit"</string>
|
||||
<string name="action_view_source">"Zobrazit zdroj"</string>
|
||||
<string name="action_yes">"Ano"</string>
|
||||
<string name="common_about">"O aplikaci"</string>
|
||||
<string name="common_analytics">"Analytika"</string>
|
||||
<string name="common_audio">"Zvuk"</string>
|
||||
<string name="common_bubbles">"Bubliny"</string>
|
||||
<string name="common_creating_room">"Vytváření místnosti…"</string>
|
||||
<string name="common_current_user_left_room">"Opustit místnost"</string>
|
||||
<string name="common_decryption_error">"Chyba dešifrování"</string>
|
||||
<string name="common_developer_options">"Možnosti pro vývojáře"</string>
|
||||
<string name="common_edited_suffix">"(upraveno)"</string>
|
||||
<string name="common_editing">"Úpravy"</string>
|
||||
<string name="common_emote">"* %1$s %2$s"</string>
|
||||
<string name="common_encryption_enabled">"Šifrování povoleno"</string>
|
||||
<string name="common_error">"Chyba"</string>
|
||||
<string name="common_file">"Soubor"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Obrázek"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Odkaz zkopírován do schránky"</string>
|
||||
<string name="common_loading">"Načítání…"</string>
|
||||
<string name="common_message">"Zpráva"</string>
|
||||
<string name="common_message_layout">"Rozložení zprávy"</string>
|
||||
<string name="common_message_removed">"Zpráva byla odstraněna"</string>
|
||||
<string name="common_modern">"Moderní"</string>
|
||||
<string name="common_no_results">"Žádné výsledky"</string>
|
||||
<string name="common_offline">"Offline"</string>
|
||||
<string name="common_password">"Heslo"</string>
|
||||
<string name="common_people">"Lidé"</string>
|
||||
<string name="common_permalink">"Trvalý odkaz"</string>
|
||||
<string name="common_reactions">"Reakce"</string>
|
||||
<string name="common_replying_to">"Odpověď na %1$s"</string>
|
||||
<string name="common_report_a_bug">"Nahlásit chybu"</string>
|
||||
<string name="common_report_submitted">"Zpráva odeslána"</string>
|
||||
<string name="common_room_name">"Název místnosti"</string>
|
||||
<string name="common_search_for_someone">"Hledat někoho"</string>
|
||||
<string name="common_search_results">"Výsledky hledání"</string>
|
||||
<string name="common_security">"Zabezpečení"</string>
|
||||
<string name="common_select_your_server">"Vyberte svůj server"</string>
|
||||
<string name="common_sending">"Odesílání…"</string>
|
||||
<string name="common_server_not_supported">"Server není podporován"</string>
|
||||
<string name="common_server_url">"URL serveru"</string>
|
||||
<string name="common_settings">"Nastavení"</string>
|
||||
<string name="common_sticker">"Nálepka"</string>
|
||||
<string name="common_success">"Úspěch"</string>
|
||||
<string name="common_suggestions">"Návrhy"</string>
|
||||
<string name="common_topic">"Téma"</string>
|
||||
<string name="common_unable_to_decrypt">"Nelze dešifrovat"</string>
|
||||
<string name="common_unsupported_event">"Nepodporovaná událost"</string>
|
||||
<string name="common_username">"Uživatelské jméno"</string>
|
||||
<string name="common_verification_cancelled">"Ověření zrušeno"</string>
|
||||
<string name="common_verification_complete">"Ověření dokončeno"</string>
|
||||
<string name="common_video">"Video"</string>
|
||||
<string name="common_waiting">"Čekání…"</string>
|
||||
<string name="dialog_title_confirmation">"Potvrzení"</string>
|
||||
<string name="dialog_title_warning">"Upozornění"</string>
|
||||
<string name="emoji_picker_category_activity">"Aktivity"</string>
|
||||
<string name="emoji_picker_category_flags">"Vlajky"</string>
|
||||
<string name="emoji_picker_category_foods">"Jídlo a nápoje"</string>
|
||||
<string name="emoji_picker_category_nature">"Zvířata a příroda"</string>
|
||||
<string name="emoji_picker_category_objects">"Předměty"</string>
|
||||
<string name="emoji_picker_category_people">"Smajlíci a lidé"</string>
|
||||
<string name="emoji_picker_category_places">"Cestování a místa"</string>
|
||||
<string name="emoji_picker_category_symbols">"Symboly"</string>
|
||||
<string name="error_failed_creating_the_permalink">"Vytvoření trvalého odkazu se nezdařilo"</string>
|
||||
<string name="error_failed_loading_messages">"Načítání zpráv se nezdařilo"</string>
|
||||
<string name="error_some_messages_have_not_been_sent">"Některé zprávy nebyly odeslány"</string>
|
||||
<string name="error_unknown">"Omlouváme se, došlo k chybě"</string>
|
||||
<string name="invite_friends_text">"Ahoj, ozvi se mi na %1$s: %2$s"</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Opravdu chcete opustit tuto místnost? Jste tu jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Opravdu chcete opustit tuto místnost? Tato místnost není veřejná a bez pozvánky se nebudete moci znovu připojit."</string>
|
||||
<string name="leave_room_alert_subtitle">"Opravdu chcete opustit místnost?"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<plurals name="common_member_count">
|
||||
<item quantity="one">"%1$d člen"</item>
|
||||
<item quantity="few">"%1$d členové"</item>
|
||||
<item quantity="other">"%1$d členů"</item>
|
||||
</plurals>
|
||||
<string name="report_content_explanation">"Tato zpráva bude nahlášena správci vašeho domovského serveru. Nebude si moci přečíst žádné šifrované zprávy."</string>
|
||||
<string name="report_content_hint">"Důvod nahlášení tohoto obsahu"</string>
|
||||
<string name="room_timeline_beginning_of_room">"Toto je začátek %1$s."</string>
|
||||
<string name="room_timeline_beginning_of_room_no_name">"Toto je začátek této konverzace."</string>
|
||||
<string name="room_timeline_read_marker_title">"Nové"</string>
|
||||
<string name="settings_rageshake">"Rageshake"</string>
|
||||
<string name="settings_title_general">"Obecné"</string>
|
||||
<string name="settings_version_number">"Verze: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"en"</string>
|
||||
<string name="dialog_title_error">"Chyba"</string>
|
||||
<string name="dialog_title_success">"Úspěch"</string>
|
||||
<string name="screen_report_content_block_user">"Zablokovat uživatele"</string>
|
||||
</resources>
|
||||
|
|
@ -4,15 +4,19 @@
|
|||
<string name="a11y_send_files">"Dateien senden"</string>
|
||||
<string name="a11y_show_password">"Passwort anzeigen"</string>
|
||||
<string name="a11y_user_menu">"Benutzermenü"</string>
|
||||
<string name="action_accept">"Zustimmen"</string>
|
||||
<string name="action_back">"Zurück"</string>
|
||||
<string name="action_cancel">"Abbrechen"</string>
|
||||
<string name="action_choose_photo">"Foto auswählen"</string>
|
||||
<string name="action_clear">"Löschen"</string>
|
||||
<string name="action_close">"Schließen"</string>
|
||||
<string name="action_complete_verification">"Verifizierung abschließen"</string>
|
||||
<string name="action_confirm">"Bestätigen"</string>
|
||||
<string name="action_continue">"Weiter"</string>
|
||||
<string name="action_copy">"Kopieren"</string>
|
||||
<string name="action_copy_link">"Link kopieren"</string>
|
||||
<string name="action_create">"Erstellen"</string>
|
||||
<string name="action_create_a_room">"Raum erstellen"</string>
|
||||
<string name="action_decline">"Ablehnen"</string>
|
||||
<string name="action_disable">"Deaktivieren"</string>
|
||||
<string name="action_done">"Fertig"</string>
|
||||
|
|
@ -31,6 +35,7 @@
|
|||
<string name="action_quick_reply">"Schnellantwort"</string>
|
||||
<string name="action_quote">"Zitieren"</string>
|
||||
<string name="action_remove">"Entfernen"</string>
|
||||
<string name="action_reply">"Antworten"</string>
|
||||
<string name="action_report_bug">"Fehler melden"</string>
|
||||
<string name="action_report_content">"Inhalt melden"</string>
|
||||
<string name="action_retry">"Erneut versuchen"</string>
|
||||
|
|
@ -42,31 +47,50 @@
|
|||
<string name="action_share">"Teilen"</string>
|
||||
<string name="action_share_link">"Link teilen"</string>
|
||||
<string name="action_skip">"Überspringen"</string>
|
||||
<string name="action_start">"Starten"</string>
|
||||
<string name="action_start_chat">"Chat starten"</string>
|
||||
<string name="action_start_verification">"Verifizierung starten"</string>
|
||||
<string name="action_take_photo">"Foto aufnehmen"</string>
|
||||
<string name="action_view_source">"Quelltext anzeigen"</string>
|
||||
<string name="action_yes">"Ja"</string>
|
||||
<string name="common_about">"Über"</string>
|
||||
<string name="common_analytics">"Analyse"</string>
|
||||
<string name="common_audio">"Audio"</string>
|
||||
<string name="common_bubbles">"Blasen"</string>
|
||||
<string name="common_creating_room">"Erstelle Raum…"</string>
|
||||
<string name="common_current_user_left_room">"Raum verlassen"</string>
|
||||
<string name="common_decryption_error">"Entschlüsselungsfehler"</string>
|
||||
<string name="common_developer_options">"Entwickleroptionen"</string>
|
||||
<string name="common_edited_suffix">"(bearbeitet)"</string>
|
||||
<string name="common_editing">"Bearbeiten"</string>
|
||||
<string name="common_encryption_enabled">"Verschlüsselung aktiviert"</string>
|
||||
<string name="common_error">"Fehler"</string>
|
||||
<string name="common_file">"Datei"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Bild"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Link in Zwischenablage kopiert"</string>
|
||||
<string name="common_loading">"Wird geladen…"</string>
|
||||
<string name="common_message">"Nachricht"</string>
|
||||
<string name="common_message_layout">"Nachrichtenlayout"</string>
|
||||
<string name="common_message_removed">"Nachricht wurde entfernt"</string>
|
||||
<string name="common_modern">"Modern"</string>
|
||||
<string name="common_no_results">"Keine Ergebnisse"</string>
|
||||
<string name="common_offline">"Offline"</string>
|
||||
<string name="common_password">"Passwort"</string>
|
||||
<string name="common_people">"Personen"</string>
|
||||
<string name="common_permalink">"Permalink"</string>
|
||||
<string name="common_reactions">"Reaktionen"</string>
|
||||
<string name="common_report_a_bug">"Fehler melden"</string>
|
||||
<string name="common_replying_to">"Auf %1$s antworten"</string>
|
||||
<string name="common_report_a_bug">"Melde einen Fehler"</string>
|
||||
<string name="common_report_submitted">"Bericht gesendet"</string>
|
||||
<string name="common_room_name">"Raumname"</string>
|
||||
<string name="common_search_for_someone">"Suche nach jemandem"</string>
|
||||
<string name="common_search_results">"Suchergebnisse"</string>
|
||||
<string name="common_security">"Sicherheit"</string>
|
||||
<string name="common_select_your_server">"Wählen deinen Server"</string>
|
||||
<string name="common_sending">"Senden…"</string>
|
||||
<string name="common_server_not_supported">"Server wird nicht unterstützt"</string>
|
||||
<string name="common_server_url">"Server-URL"</string>
|
||||
<string name="common_settings">"Einstellungen"</string>
|
||||
<string name="common_starting_chat">"Chat wird gestartet…"</string>
|
||||
<string name="common_sticker">"Sticker"</string>
|
||||
|
|
@ -80,6 +104,7 @@
|
|||
<string name="common_verification_complete">"Verifizierung abgeschlossen"</string>
|
||||
<string name="common_video">"Video"</string>
|
||||
<string name="common_waiting">"Warten…"</string>
|
||||
<string name="dialog_title_confirmation">"Bestätigung"</string>
|
||||
<string name="dialog_title_warning">"Warnung"</string>
|
||||
<string name="emoji_picker_category_activity">"Aktivitäten"</string>
|
||||
<string name="emoji_picker_category_flags">"Flaggen"</string>
|
||||
|
|
@ -89,26 +114,45 @@
|
|||
<string name="emoji_picker_category_people">"Smileys & Personen"</string>
|
||||
<string name="emoji_picker_category_places">"Reisen & Orte"</string>
|
||||
<string name="emoji_picker_category_symbols">"Symbole"</string>
|
||||
<string name="error_failed_creating_the_permalink">"Fehler beim Erstellen des Permalinks"</string>
|
||||
<string name="error_failed_loading_messages">"Fehler beim Laden der Nachrichten"</string>
|
||||
<string name="error_some_messages_have_not_been_sent">"Einige Nachrichten wurden nicht gesendet"</string>
|
||||
<string name="error_unknown">"Entschuldigung, ein Fehler ist aufgetreten."</string>
|
||||
<string name="invite_friends_text">"Hey, sprich mit mir auf %1$s: %2$s"</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Bist du sicher, dass du diesen Raum verlassen willst? Du bist die einzige Person hier. Wenn du gehst, kann in Zukunft niemand mehr beitreten, auch du nicht."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Bist du dir sicher, dass du den Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne eine Einladung nicht mehr beitreten."</string>
|
||||
<string name="leave_room_alert_subtitle">"Bist du dir sicher, dass du den Raum verlassen möchtest?"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<plurals name="common_member_count">
|
||||
<item quantity="one">"%1$d Mitglied"</item>
|
||||
<item quantity="other">"%1$d Mitglieder"</item>
|
||||
</plurals>
|
||||
<string name="preference_rageshake">"Rageshake zum Melden von Fehlern"</string>
|
||||
<string name="rageshake_dialog_content">"Du scheinst frustriert das Telefon zu schütteln. Möchtest du den Fehlerberichtsbildschirm öffnen?"</string>
|
||||
<string name="report_content_explanation">"Diese Nachricht wird an deinen Heimserver-Admin gemeldet werden. Er wird nicht in der Lage sein, verschlüsselte Nachrichten zu lesen."</string>
|
||||
<string name="report_content_hint">"Grund für die Meldung dieses Inhalts"</string>
|
||||
<string name="room_timeline_beginning_of_room">"Dies ist der Anfang von %1$s."</string>
|
||||
<string name="room_timeline_beginning_of_room_no_name">"Dies ist der Beginn dieser Konversation."</string>
|
||||
<string name="room_timeline_read_marker_title">"Neu"</string>
|
||||
<string name="screen_analytics_prompt_data_usage">"Wir erfassen und analysieren "<b>"keine"</b>" Account-Daten"</string>
|
||||
<string name="screen_analytics_prompt_help_us_improve">"Helfen Sie uns, Probleme zu identifizieren und %1$s zu verbessern, indem Sie anonyme Nutzungsdaten weitergeben."</string>
|
||||
<string name="screen_analytics_prompt_read_terms">"Sie können alle unsere Nutzerbedingungen %1$s lesen."</string>
|
||||
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Sie können die Analyse jederzeit in den Einstellungen deaktivieren"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben "<b>"keine"</b>" Informationen an Dritte weiter"</string>
|
||||
<string name="screen_analytics_prompt_title">"Helfen Sie %1$s zu verbessern"</string>
|
||||
<string name="screen_analytics_settings_share_data">"Teile Analyse-Daten"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Medienauswahl fehlgeschlagen, bitte versuche es erneut."</string>
|
||||
<string name="screen_report_content_block_user_hint">"Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest"</string>
|
||||
<string name="settings_rageshake">"Rageshake"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Erkennungsschwelle"</string>
|
||||
<string name="settings_title_general">"Allgemein"</string>
|
||||
<string name="settings_version_number">"Version: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"de"</string>
|
||||
<string name="dialog_title_error">"Fehler"</string>
|
||||
<string name="dialog_title_success">"Erfolg"</string>
|
||||
<string name="screen_analytics_settings_help_us_improve">"Helfen Sie uns, Probleme zu identifizieren und %1$s zu verbessern, indem Sie anonyme Nutzungsdaten weitergeben."</string>
|
||||
<string name="screen_analytics_settings_read_terms">"Sie können alle unsere Nutzerbedingungen %1$s lesen."</string>
|
||||
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
|
||||
<string name="screen_report_content_block_user">"Nutzer blockieren"</string>
|
||||
</resources>
|
||||
|
|
@ -4,13 +4,140 @@
|
|||
<string name="a11y_send_files">"Envoyer des fichiers"</string>
|
||||
<string name="a11y_show_password">"Afficher le mot de passe"</string>
|
||||
<string name="a11y_user_menu">"Menu utilisateur"</string>
|
||||
<string name="action_accept">"Accepter"</string>
|
||||
<string name="action_back">"Retour"</string>
|
||||
<string name="action_cancel">"Annuler"</string>
|
||||
<string name="action_choose_photo">"Choisir une photo"</string>
|
||||
<string name="action_clear">"Effacer"</string>
|
||||
<string name="action_close">"Fermer"</string>
|
||||
<string name="action_complete_verification">"Compléter la vérification"</string>
|
||||
<string name="action_confirm">"Confirmer"</string>
|
||||
<string name="action_continue">"Continuer"</string>
|
||||
<string name="action_copy">"Copier"</string>
|
||||
<string name="action_copy_link">"Copier le lien"</string>
|
||||
<string name="action_create">"Créer"</string>
|
||||
<string name="action_create_a_room">"Créer une salle"</string>
|
||||
<string name="action_decline">"Refuser"</string>
|
||||
<string name="action_disable">"Désactiver"</string>
|
||||
<string name="action_done">"Terminé"</string>
|
||||
<string name="action_edit">"Éditer"</string>
|
||||
<string name="action_enable">"Activer"</string>
|
||||
<string name="action_invite">"Inviter"</string>
|
||||
<string name="action_invite_friends_to_app">"Inviter des amis à %1$s"</string>
|
||||
<string name="action_invites_list">"Invitations"</string>
|
||||
<string name="action_learn_more">"En savoir plus"</string>
|
||||
<string name="action_leave">"Quitter"</string>
|
||||
<string name="action_leave_room">"Quitter la salle"</string>
|
||||
<string name="action_next">"Suivant"</string>
|
||||
<string name="action_no">"Non"</string>
|
||||
<string name="action_not_now">"Pas maintenant"</string>
|
||||
<string name="action_ok">"OK"</string>
|
||||
<string name="action_quick_reply">"Réponse rapide"</string>
|
||||
<string name="action_quote">"Citer"</string>
|
||||
<string name="action_remove">"Supprimer"</string>
|
||||
<string name="action_reply">"Répondre"</string>
|
||||
<string name="action_report_bug">"Signaler un bug"</string>
|
||||
<string name="action_report_content">"Signaler le contenu"</string>
|
||||
<string name="action_retry">"Réessayer"</string>
|
||||
<string name="action_retry_decryption">"Réessayer le déchiffrement"</string>
|
||||
<string name="action_save">"Enregistrer"</string>
|
||||
<string name="action_search">"Chercher"</string>
|
||||
<string name="action_send">"Envoyer"</string>
|
||||
<string name="action_send_message">"Envoyer un message"</string>
|
||||
<string name="action_share">"Partager"</string>
|
||||
<string name="action_share_link">"Partager le lien"</string>
|
||||
<string name="action_skip">"Passer"</string>
|
||||
<string name="action_start">"Démarrer"</string>
|
||||
<string name="action_start_chat">"Commencer un chat"</string>
|
||||
<string name="action_start_verification">"Commencer la vérification"</string>
|
||||
<string name="action_take_photo">"Prendre une photo"</string>
|
||||
<string name="action_view_source">"Voir la source"</string>
|
||||
<string name="action_yes">"Oui"</string>
|
||||
<string name="common_about">"À propos"</string>
|
||||
<string name="common_audio">"Audio"</string>
|
||||
<string name="common_bubbles">"Bulles"</string>
|
||||
<string name="common_creating_room">"Création de la salle…"</string>
|
||||
<string name="common_current_user_left_room">"La salle a été quittée"</string>
|
||||
<string name="common_decryption_error">"Erreur de déchiffrement"</string>
|
||||
<string name="common_developer_options">"Options de développement"</string>
|
||||
<string name="common_edited_suffix">"(édité)"</string>
|
||||
<string name="common_editing">"Modification en cours"</string>
|
||||
<string name="common_encryption_enabled">"Chiffrement activé"</string>
|
||||
<string name="common_error">"Erreur"</string>
|
||||
<string name="common_file">"Fichier"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Image"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Lien copié dans le presse-papiers"</string>
|
||||
<string name="common_loading">"Chargement…"</string>
|
||||
<string name="common_message">"Message"</string>
|
||||
<string name="common_message_layout">"Mise en page du message"</string>
|
||||
<string name="common_message_removed">"Message supprimé"</string>
|
||||
<string name="common_modern">"Moderne"</string>
|
||||
<string name="common_no_results">"Aucun résultat"</string>
|
||||
<string name="common_offline">"Hors ligne"</string>
|
||||
<string name="common_password">"Mot de passe"</string>
|
||||
<string name="common_people">"Personnes"</string>
|
||||
<string name="common_permalink">"Permalien"</string>
|
||||
<string name="common_reactions">"Réactions"</string>
|
||||
<string name="common_replying_to">"En réponse à %1$s"</string>
|
||||
<string name="common_report_a_bug">"Signaler un problème"</string>
|
||||
<string name="common_report_submitted">"Rapport envoyé"</string>
|
||||
<string name="common_room_name">"Nom de la salle"</string>
|
||||
<string name="common_search_for_someone">"Rechercher quelqu\'un"</string>
|
||||
<string name="common_security">"Sécurité"</string>
|
||||
<string name="common_select_your_server">"Sélectionnez votre serveur"</string>
|
||||
<string name="common_sending">"Envoi en cours…"</string>
|
||||
<string name="common_server_not_supported">"Serveur non pris en charge"</string>
|
||||
<string name="common_server_url">"URL du serveur"</string>
|
||||
<string name="common_settings">"Paramètres"</string>
|
||||
<string name="common_sticker">"Autocollant"</string>
|
||||
<string name="common_success">"Succès"</string>
|
||||
<string name="common_suggestions">"Suggestions"</string>
|
||||
<string name="common_topic">"Sujet"</string>
|
||||
<string name="common_unable_to_decrypt">"Incapable de décrypter"</string>
|
||||
<string name="common_unsupported_event">"Événement non pris en charge"</string>
|
||||
<string name="common_username">"Nom d\'utilisateur"</string>
|
||||
<string name="common_verification_cancelled">"Vérification annulée"</string>
|
||||
<string name="common_verification_complete">"Vérification terminée"</string>
|
||||
<string name="common_video">"Vidéo"</string>
|
||||
<string name="common_waiting">"Patientez…"</string>
|
||||
<string name="dialog_title_confirmation">"Confirmation"</string>
|
||||
<string name="dialog_title_warning">"Attention"</string>
|
||||
<string name="emoji_picker_category_activity">"Activités"</string>
|
||||
<string name="emoji_picker_category_flags">"Drapeaux"</string>
|
||||
<string name="emoji_picker_category_foods">"Nourriture et boissons"</string>
|
||||
<string name="emoji_picker_category_nature">"Animaux et nature"</string>
|
||||
<string name="emoji_picker_category_objects">"Objets"</string>
|
||||
<string name="emoji_picker_category_people">"Émoticônes et personnes"</string>
|
||||
<string name="emoji_picker_category_places">"Voyages & lieux"</string>
|
||||
<string name="emoji_picker_category_symbols">"Symboles"</string>
|
||||
<string name="error_failed_creating_the_permalink">"Échec de la création du permalien"</string>
|
||||
<string name="error_failed_loading_messages">"Échec du chargement des messages"</string>
|
||||
<string name="error_some_messages_have_not_been_sent">"Certains messages n\'ont pas été envoyés"</string>
|
||||
<string name="error_unknown">"Désolé, une erreur est survenue."</string>
|
||||
<string name="invite_friends_text">"Salut, parle-moi sur %1$s : %2$s"</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Êtes-vous sûr de vouloir quitter cette salle ? Vous êtes la seule personne ici. Si vous partez, personne ne pourra rejoindre la salle à l\'avenir, y compris vous."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Êtes-vous sûr de vouloir quitter cette salle ? Cette salle n\'est pas publique et vous ne pourrez pas la rejoindre sans invitation."</string>
|
||||
<string name="leave_room_alert_subtitle">"Êtes-vous sûr de vouloir quitter la salle ?"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<plurals name="common_member_count">
|
||||
<item quantity="one">"%1$d membre"</item>
|
||||
<item quantity="other">"%1$d membres"</item>
|
||||
</plurals>
|
||||
<string name="preference_rageshake">"Rageshake pour signaler un bug"</string>
|
||||
<string name="rageshake_dialog_content">"Vous semblez secouer le téléphone de frustration. Voulez-vous ouvrir le formulaire de rapport de problème ?"</string>
|
||||
<string name="report_content_explanation">"Ce message sera signalé à l’administrateur de votre serveur d\'accueil. Ils ne pourront lire aucun message crypté."</string>
|
||||
<string name="report_content_hint">"Raison du signalement de ce contenu"</string>
|
||||
<string name="room_timeline_beginning_of_room">"Ceci est le début de %1$s."</string>
|
||||
<string name="room_timeline_beginning_of_room_no_name">"Ceci est le début de cette conversation."</string>
|
||||
<string name="room_timeline_read_marker_title">"Nouveau"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Cochez si vous souhaitez masquer tous les messages actuels et futurs de cet utilisateur."</string>
|
||||
<string name="settings_rageshake">"Rageshake"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Seuil de détection"</string>
|
||||
<string name="settings_title_general">"Général"</string>
|
||||
<string name="settings_version_number">"Version: %1$s ( %2$s )"</string>
|
||||
<string name="test_language_identifier">"fr"</string>
|
||||
<string name="dialog_title_error">"Erreur"</string>
|
||||
<string name="dialog_title_success">"Succès"</string>
|
||||
<string name="screen_report_content_block_user">"Bloquer l\'utilisateur"</string>
|
||||
</resources>
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
<string name="action_edit">"Editați"</string>
|
||||
<string name="action_enable">"Activați"</string>
|
||||
<string name="action_invite">"Invitați"</string>
|
||||
<string name="action_invite_friends">"Invitați prieteni"</string>
|
||||
<string name="action_invite_friends_to_app">"Invitați prieteni în %1$s"</string>
|
||||
<string name="action_invites_list">"Invitații"</string>
|
||||
<string name="action_learn_more">"Aflați mai multe"</string>
|
||||
|
|
@ -63,11 +64,13 @@
|
|||
<string name="common_developer_options">"Opțiuni programator"</string>
|
||||
<string name="common_edited_suffix">"(editat)"</string>
|
||||
<string name="common_editing">"Editare"</string>
|
||||
<string name="common_emote">"* %1$s %2$s"</string>
|
||||
<string name="common_encryption_enabled">"Criptare activată"</string>
|
||||
<string name="common_error">"Eroare"</string>
|
||||
<string name="common_file">"Fişier"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Imagine"</string>
|
||||
<string name="common_leaving_room">"Se părăsește conversația"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Linkul a fost copiat în clipboard"</string>
|
||||
<string name="common_loading">"Se încarcă…"</string>
|
||||
<string name="common_message">"Mesaj"</string>
|
||||
|
|
@ -83,18 +86,23 @@
|
|||
<string name="common_replying_to">"Răspuns pentru %1$s"</string>
|
||||
<string name="common_report_a_bug">"Raportați o eroare"</string>
|
||||
<string name="common_report_submitted">"Raport trimis"</string>
|
||||
<string name="common_room_name">"Numele camerei"</string>
|
||||
<string name="common_search_for_someone">"Căutați pe cineva"</string>
|
||||
<string name="common_search_results">"Rezultatele căutării"</string>
|
||||
<string name="common_security">"Securitate"</string>
|
||||
<string name="common_select_your_server">"Selectați serverul"</string>
|
||||
<string name="common_sending">"Se trimite…"</string>
|
||||
<string name="common_server_not_supported">"Serverul nu este compatibil"</string>
|
||||
<string name="common_server_url">"Adresa URL a serverului"</string>
|
||||
<string name="common_settings">"Setări"</string>
|
||||
<string name="common_starting_chat">"Se începe conversația…"</string>
|
||||
<string name="common_sticker">"Autocolant"</string>
|
||||
<string name="common_success">"Succes"</string>
|
||||
<string name="common_suggestions">"Sugestii"</string>
|
||||
<string name="common_topic">"Subiect"</string>
|
||||
<string name="common_unable_to_decrypt">"Nu s-a putut decripta"</string>
|
||||
<string name="common_unable_to_invite_message">"Nu am putut trimite cu succes invitații unuia sau mai multor utilizatori."</string>
|
||||
<string name="common_unable_to_invite_title">"Nu s-a putut trimite invitația (invitațiile)"</string>
|
||||
<string name="common_unsupported_event">"Eveniment neacceptat"</string>
|
||||
<string name="common_username">"Utilizator"</string>
|
||||
<string name="common_verification_cancelled">"Verificare anulată"</string>
|
||||
|
|
@ -115,6 +123,7 @@
|
|||
<string name="error_failed_loading_messages">"Încărcarea mesajelor a eșuat"</string>
|
||||
<string name="error_some_messages_have_not_been_sent">"Unele mesaje nu au fost trimise"</string>
|
||||
<string name="error_unknown">"Ne pare rău, a apărut o eroare"</string>
|
||||
<string name="invite_friends_rich_title">"🔐️ Alăturați-vă mie pe %1$s"</string>
|
||||
<string name="invite_friends_text">"Hei, vorbește cu mine pe %1$s: %2$s"</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Sunteți sigur că vreți să părăsiți această cameră? Sunteți singura persoană de aici. Dacă o părasiți, nimeni nu se va mai putea alătura în viitor, inclusiv dumneavoastra."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Sunteți sigur că vrei să părăsiți această cameră? Această cameră nu este publică și nu va veti putea alătura din nou fără o invitație."</string>
|
||||
|
|
@ -139,6 +148,10 @@
|
|||
<string name="screen_analytics_prompt_settings">"Puteți dezactiva această opțiune oricând din setări"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing"><b>"Nu"</b>" împărtășim informații cu terți"</string>
|
||||
<string name="screen_analytics_prompt_title">"Ajutați la îmbunătățirea %1$s"</string>
|
||||
<string name="screen_analytics_settings_share_data">"Partajați datele analitice"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Selectarea fișierelor media a eșuat, încercați din nou."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Procesarea datelor media a eșuat, vă rugăm să încercați din nou."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Încărcarea fișierelor media a eșuat, încercați din nou."</string>
|
||||
<string name="screen_report_content_block_user_hint">"Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator"</string>
|
||||
<string name="settings_rageshake">"Rageshake"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Prag de detecție"</string>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
<string name="action_done">"Done"</string>
|
||||
<string name="action_edit">"Edit"</string>
|
||||
<string name="action_enable">"Enable"</string>
|
||||
<string name="action_forgot_password">"Forgot password?"</string>
|
||||
<string name="action_invite">"Invite"</string>
|
||||
<string name="action_invite_friends">"Invite friends"</string>
|
||||
<string name="action_invite_friends_to_app">"Invite friends to %1$s"</string>
|
||||
|
|
@ -70,6 +71,7 @@
|
|||
<string name="common_file">"File"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Image"</string>
|
||||
<string name="common_invite_unknown_profile">"We can’t validate this user’s Matrix ID. The invite might not be received."</string>
|
||||
<string name="common_leaving_room">"Leaving room"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Link copied to clipboard"</string>
|
||||
<string name="common_loading">"Loading…"</string>
|
||||
|
|
@ -86,6 +88,7 @@
|
|||
<string name="common_replying_to">"Replying to %1$s"</string>
|
||||
<string name="common_report_a_bug">"Report a bug"</string>
|
||||
<string name="common_report_submitted">"Report submitted"</string>
|
||||
<string name="common_room_name">"Room name"</string>
|
||||
<string name="common_search_for_someone">"Search for someone"</string>
|
||||
<string name="common_search_results">"Search results"</string>
|
||||
<string name="common_security">"Security"</string>
|
||||
|
|
@ -151,6 +154,12 @@
|
|||
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
|
||||
<string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Change account provider"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"A private server for Element employees."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix is an open network for secure, decentralised communication."</string>
|
||||
<string name="screen_server_confirmation_message_register">"This is where your conversations will live — just like you would use an email provider to keep your emails."</string>
|
||||
<string name="screen_server_confirmation_title_login">"You’re about to sign in to %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"You’re about to create an account on %1$s"</string>
|
||||
<string name="settings_rageshake">"Rageshake"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Detection threshold"</string>
|
||||
<string name="settings_title_general">"General"</string>
|
||||
|
|
@ -163,4 +172,4 @@
|
|||
<string name="screen_analytics_settings_read_terms">"You can read all our terms %1$s."</string>
|
||||
<string name="screen_analytics_settings_read_terms_content_link">"here"</string>
|
||||
<string name="screen_report_content_block_user">"Block user"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
@ -16,10 +16,9 @@
|
|||
|
||||
package io.element.android.libraries.usersearch.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface UserRepository {
|
||||
|
||||
suspend fun search(query: String): Flow<List<MatrixUser>>
|
||||
suspend fun search(query: String): Flow<List<UserSearchResult>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.usersearch.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
data class UserSearchResult(
|
||||
val matrixUser: MatrixUser,
|
||||
val isUnresolved: Boolean = false,
|
||||
)
|
||||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
|
@ -33,24 +34,26 @@ class MatrixUserRepository @Inject constructor(
|
|||
private val dataSource: UserListDataSource
|
||||
) : UserRepository {
|
||||
|
||||
override suspend fun search(query: String): Flow<List<MatrixUser>> = flow {
|
||||
override suspend fun search(query: String): Flow<List<UserSearchResult>> = flow {
|
||||
// Manually add a fake result with the matrixId, if any
|
||||
val isUserId = MatrixPatterns.isUserId(query)
|
||||
if (isUserId) {
|
||||
emit(listOf(MatrixUser(UserId(query))))
|
||||
emit(listOf(UserSearchResult(MatrixUser(UserId(query)))))
|
||||
}
|
||||
|
||||
if (query.length >= MINIMUM_SEARCH_LENGTH) {
|
||||
// Debounce
|
||||
delay(DEBOUNCE_TIME_MILLIS)
|
||||
|
||||
val results = dataSource.search(query).toMutableList()
|
||||
val results = dataSource.search(query).map { UserSearchResult(it) }.toMutableList()
|
||||
|
||||
// If the query is a user ID and the result doesn't contain that user ID, query the profile information explicitly
|
||||
if (isUserId && results.none { it.userId.value == query }) {
|
||||
val getProfileResult: MatrixUser? = dataSource.getProfile(UserId(query))
|
||||
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
||||
results.add(0, profile)
|
||||
if (isUserId && results.none { it.matrixUser.userId.value == query }) {
|
||||
results.add(
|
||||
0,
|
||||
dataSource.getProfile(UserId(query))
|
||||
?.let { UserSearchResult(it) }
|
||||
?: UserSearchResult(MatrixUser(UserId(query)), isUnresolved = true))
|
||||
}
|
||||
|
||||
emit(results)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import io.element.android.libraries.usersearch.test.FakeUserListDataSource
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -63,7 +64,7 @@ internal class MatrixUserRepositoryTest {
|
|||
val result = repository.search("some query")
|
||||
|
||||
result.test {
|
||||
assertThat(awaitItem()).isEqualTo(aMatrixUserList())
|
||||
assertThat(awaitItem()).isEqualTo(aMatrixUserList().toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
|
@ -76,7 +77,7 @@ internal class MatrixUserRepositoryTest {
|
|||
val result = repository.search(A_USER_ID.value)
|
||||
|
||||
result.test {
|
||||
assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)))
|
||||
assertThat(awaitItem()).isEqualTo(listOf(placeholderResult()))
|
||||
skipItems(1)
|
||||
awaitComplete()
|
||||
}
|
||||
|
|
@ -93,7 +94,7 @@ internal class MatrixUserRepositoryTest {
|
|||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(searchResults)
|
||||
assertThat(awaitItem()).isEqualTo(searchResults.toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
|
@ -112,13 +113,13 @@ internal class MatrixUserRepositoryTest {
|
|||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(listOf(userProfile) + searchResults)
|
||||
assertThat(awaitItem()).isEqualTo((listOf(userProfile) + searchResults).toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search - just shows id if profile can't be loaded`() = runTest {
|
||||
fun `search - returns unresolved user if profile can't be loaded`() = runTest {
|
||||
val searchResults = aMatrixUserListWithoutUserId(A_USER_ID)
|
||||
|
||||
val dataSource = FakeUserListDataSource()
|
||||
|
|
@ -130,11 +131,15 @@ internal class MatrixUserRepositoryTest {
|
|||
|
||||
result.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)) + searchResults)
|
||||
assertThat(awaitItem()).isEqualTo(listOf(placeholderResult(isUnresolved = true)) + searchResults.toUserSearchResults())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun aMatrixUserListWithoutUserId(userId: UserId) = aMatrixUserList().filterNot { it.userId == userId }
|
||||
|
||||
private fun List<MatrixUser>.toUserSearchResults() = map { UserSearchResult(it) }
|
||||
|
||||
private fun placeholderResult(id: UserId = A_USER_ID, isUnresolved: Boolean = false) = UserSearchResult(MatrixUser(id), isUnresolved = isUnresolved)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
package io.element.android.libraries.usersearch.test
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
|
|
@ -26,14 +26,14 @@ class FakeUserRepository : UserRepository {
|
|||
var providedQuery: String? = null
|
||||
private set
|
||||
|
||||
private val flow = MutableSharedFlow<List<MatrixUser>>()
|
||||
private val flow = MutableSharedFlow<List<UserSearchResult>>()
|
||||
|
||||
override suspend fun search(query: String): Flow<List<MatrixUser>> {
|
||||
override suspend fun search(query: String): Flow<List<UserSearchResult>> {
|
||||
providedQuery = query
|
||||
return flow
|
||||
}
|
||||
|
||||
suspend fun emitResult(result: List<MatrixUser>) {
|
||||
suspend fun emitResult(result: List<UserSearchResult>) {
|
||||
flow.emit(result)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue