Merge branch 'develop' into renovate/kotlin

This commit is contained in:
Benoit Marty 2024-10-21 08:44:21 +02:00 committed by GitHub
commit 431467af5b
363 changed files with 3030 additions and 2267 deletions

View file

@ -79,7 +79,7 @@ jobs:
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: elementx-apk-maestro name: elementx-apk-maestro
- uses: mobile-dev-inc/action-maestro-cloud@v1.9.1 - uses: mobile-dev-inc/action-maestro-cloud@v1.9.2
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch' if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with: with:
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Αποσύνδεση &amp;amp; Αναβάθμιση"</string> <string name="banner_migrate_to_native_sliding_sync_action">"Αποσύνδεση &amp; Αναβάθμιση"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή."</string> <string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή."</string>
</resources> </resources>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Выйти и обновить"</string> <string name="banner_migrate_to_native_sliding_sync_action">"Выйти и обновить"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ваш homeserver больше не поддерживает старый протокол. Пожалуйста, выйдите из системы и войдите снова, чтобы продолжить использование приложения."</string> <string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ваш домашний сервер больше не поддерживает старый протокол. Пожалуйста, выйдите и войдите в свою учётную запись снова, чтобы продолжить использование приложения."</string>
</resources> </resources>

View file

@ -2,7 +2,7 @@ Element X brings you both sovereign & seamless collaboration built on Matrix.
The collaboration capabilities include chat & video calls with the modern set of features such as: The collaboration capabilities include chat & video calls with the modern set of features such as:
• public & private channels • public & private channels
• room moderation & access conUpdatetrol • room moderation & access control
• replies, reactions, polls, read receipts, pinned messages, etc. • replies, reactions, polls, read receipts, pinned messages, etc.
• simultaneous chat & calls (picture in picture) • simultaneous chat & calls (picture in picture)
• decentralized & federated communication across organizations • decentralized & federated communication across organizations
@ -33,3 +33,7 @@ Enjoy your right to private conversations - free from data mining, ads and all t
<b>Chat across multiple devices</b> <b>Chat across multiple devices</b>
Stay in touch wherever you are with fully synchronized message history across all your devices, even those running Element legacy app, and on the web at https://app.element.io Stay in touch wherever you are with fully synchronized message history across all your devices, even those running Element legacy app, and on the web at https://app.element.io
The application requires the android.permission.REQUEST_INSTALL_PACKAGES permission to enable the installation of applications received as attachments, ensuring seamless and convenient access to new software within the app.
The application requires the USE_FULL_SCREEN_INTENT permission to ensure our users can effectively receive call notifications even when their devices are locked.

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы."</string> <string name="screen_analytics_settings_help_us_improve">"Предоставьте разработчикам анонимные данные об использовании, чтобы помочь им выявлять проблемы эффективнее."</string>
<string name="screen_analytics_settings_read_terms">"Вы можете ознакомиться со всеми нашими условиями %1$s."</string> <string name="screen_analytics_settings_read_terms">"Вы можете ознакомиться со всеми нашими условиями %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"здесь"</string> <string name="screen_analytics_settings_read_terms_content_link">"здесь"</string>
<string name="screen_analytics_settings_share_data">"Делитесь данными аналитики"</string> <string name="screen_analytics_settings_share_data">"Отправлять аналитические данные"</string>
</resources> </resources>

View file

@ -4,7 +4,7 @@
<string name="screen_analytics_prompt_help_us_improve">"Partilhe dados de utilização anónimos para nos ajudar a identificar problemas."</string> <string name="screen_analytics_prompt_help_us_improve">"Partilhe dados de utilização anónimos para nos ajudar a identificar problemas."</string>
<string name="screen_analytics_prompt_read_terms">"Podes ler todos os nossos termos %1$s."</string> <string name="screen_analytics_prompt_read_terms">"Podes ler todos os nossos termos %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"aqui"</string> <string name="screen_analytics_prompt_read_terms_content_link">"aqui"</string>
<string name="screen_analytics_prompt_settings">"Podes desligar qualquer momento"</string> <string name="screen_analytics_prompt_settings">"Pode desactivar a qualquer momento"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Não partilharemos os teus dados com terceiros"</string> <string name="screen_analytics_prompt_third_party_sharing">"Não partilharemos os teus dados com terceiros"</string>
<string name="screen_analytics_prompt_title">"Ajude a melhorar a %1$s"</string> <string name="screen_analytics_prompt_title">"Ajude a melhorar a %1$s"</string>
</resources> </resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Мы не будем записывать или профилировать какие-либо персональные данные"</string> <string name="screen_analytics_prompt_data_usage">"Мы не будем записывать или профилировать какие-либо персональные данные"</string>
<string name="screen_analytics_prompt_help_us_improve">"Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы."</string> <string name="screen_analytics_prompt_help_us_improve">"Предоставьте разработчикам анонимные данные об использовании, чтобы помочь им выявлять проблемы эффективнее."</string>
<string name="screen_analytics_prompt_read_terms">"Вы можете ознакомиться со всеми нашими условиями %1$s."</string> <string name="screen_analytics_prompt_read_terms">"Вы можете ознакомиться со всеми нашими условиями %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"здесь"</string> <string name="screen_analytics_prompt_read_terms_content_link">"здесь"</string>
<string name="screen_analytics_prompt_settings">"Вы можете отключить эту функцию в любое время"</string> <string name="screen_analytics_prompt_settings">"Вы можете отключить эту функцию в любое время"</string>

View file

@ -21,8 +21,8 @@
<!-- Permissions for call foreground services --> <!-- Permissions for call foreground services -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<application> <application>
@ -80,7 +80,7 @@
android:name=".services.CallForegroundService" android:name=".services.CallForegroundService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="phoneCall" /> android:foregroundServiceType="microphone" />
<receiver <receiver
android:name=".receivers.DeclineCallBroadcastReceiver" android:name=".receivers.DeclineCallBroadcastReceiver"

View file

@ -7,9 +7,11 @@
package io.element.android.features.call.impl.services package io.element.android.features.call.impl.services
import android.Manifest
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
@ -33,8 +35,12 @@ import timber.log.Timber
class CallForegroundService : Service() { class CallForegroundService : Service() {
companion object { companion object {
fun start(context: Context) { fun start(context: Context) {
val intent = Intent(context, CallForegroundService::class.java) if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
ContextCompat.startForegroundService(context, intent) val intent = Intent(context, CallForegroundService::class.java)
ContextCompat.startForegroundService(context, intent)
} else {
Timber.w("Microphone permission is not granted, cannot start the call foreground service")
}
} }
fun stop(context: Context) { fun stop(context: Context) {
@ -67,8 +73,8 @@ class CallForegroundService : Service() {
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.build() .build()
val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.ONGOING_CALL) val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.ONGOING_CALL)
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
} else { } else {
0 0
} }

View file

@ -180,6 +180,7 @@ class CallScreenPresenter @AssistedInject constructor(
urlState = urlState.value, urlState = urlState.value,
webViewError = webViewError, webViewError = webViewError,
userAgent = userAgent, userAgent = userAgent,
isCallActive = isJoinedCall,
isInWidgetMode = isInWidgetMode, isInWidgetMode = isInWidgetMode,
eventSink = { handleEvents(it) }, eventSink = { handleEvents(it) },
) )

View file

@ -13,6 +13,7 @@ data class CallScreenState(
val urlState: AsyncData<String>, val urlState: AsyncData<String>,
val webViewError: String?, val webViewError: String?,
val userAgent: String, val userAgent: String,
val isCallActive: Boolean,
val isInWidgetMode: Boolean, val isInWidgetMode: Boolean,
val eventSink: (CallScreenEvents) -> Unit, val eventSink: (CallScreenEvents) -> Unit,
) )

View file

@ -24,6 +24,7 @@ internal fun aCallScreenState(
urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"), urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
webViewError: String? = null, webViewError: String? = null,
userAgent: String = "", userAgent: String = "",
isCallActive: Boolean = true,
isInWidgetMode: Boolean = false, isInWidgetMode: Boolean = false,
eventSink: (CallScreenEvents) -> Unit = {}, eventSink: (CallScreenEvents) -> Unit = {},
): CallScreenState { ): CallScreenState {
@ -31,6 +32,7 @@ internal fun aCallScreenState(
urlState = urlState, urlState = urlState,
webViewError = webViewError, webViewError = webViewError,
userAgent = userAgent, userAgent = userAgent,
isCallActive = isCallActive,
isInWidgetMode = isInWidgetMode, isInWidgetMode = isInWidgetMode,
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -26,6 +26,7 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
@ -95,7 +96,6 @@ class ElementCallActivity :
pictureInPicturePresenter.setPipView(this) pictureInPicturePresenter.setPipView(this)
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
requestAudioFocus()
setContent { setContent {
val pipState = pictureInPicturePresenter.present() val pipState = pictureInPicturePresenter.present()
@ -103,6 +103,12 @@ class ElementCallActivity :
ElementThemeApp(appPreferencesStore) { ElementThemeApp(appPreferencesStore) {
val state = presenter.present() val state = presenter.present()
eventSink = state.eventSink eventSink = state.eventSink
LaunchedEffect(state.isCallActive, state.isInWidgetMode) {
// Note when not in WidgetMode, isCallActive will never be true, so consider the call is active
if (state.isCallActive || !state.isInWidgetMode) {
setCallIsActive()
}
}
CallScreenView( CallScreenView(
state = state, state = state,
pipState = pipState, pipState = pipState,
@ -115,6 +121,11 @@ class ElementCallActivity :
} }
} }
private fun setCallIsActive() {
requestAudioFocus()
CallForegroundService.start(this)
}
@Composable @Composable
private fun ListenToAndroidEvents(pipState: PictureInPictureState) { private fun ListenToAndroidEvents(pipState: PictureInPictureState) {
val pipEventSink by rememberUpdatedState(pipState.eventSink) val pipEventSink by rememberUpdatedState(pipState.eventSink)
@ -156,18 +167,6 @@ class ElementCallActivity :
setCallType(intent) setCallType(intent)
} }
override fun onStart() {
super.onStart()
CallForegroundService.stop(this)
}
override fun onStop() {
super.onStop()
if (!isFinishing && !isChangingConfigurations) {
CallForegroundService.start(this)
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
releaseAudioFocus() releaseAudioFocus()
@ -231,10 +230,10 @@ class ElementCallActivity :
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun requestAudioFocus() { private fun requestAudioFocus() {
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build()
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(audioAttributes) .setAudioAttributes(audioAttributes)
.build() .build()
@ -247,7 +246,6 @@ class ElementCallActivity :
AudioManager.STREAM_VOICE_CALL, AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,
) )
audioFocusChangeListener = listener audioFocusChangeListener = listener
} }
} }

View file

@ -73,6 +73,7 @@ class CallScreenPresenterTest {
assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io")) assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
assertThat(initialState.webViewError).isNull() assertThat(initialState.webViewError).isNull()
assertThat(initialState.isInWidgetMode).isFalse() assertThat(initialState.isInWidgetMode).isFalse()
assertThat(initialState.isCallActive).isFalse()
analyticsLambda.assertions().isNeverCalled() analyticsLambda.assertions().isNeverCalled()
joinedCallLambda.assertions().isCalledOnce() joinedCallLambda.assertions().isCalledOnce()
} }
@ -106,6 +107,7 @@ class CallScreenPresenterTest {
joinedCallLambda.assertions().isCalledOnce() joinedCallLambda.assertions().isCalledOnce()
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java) assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.isCallActive).isFalse()
assertThat(initialState.isInWidgetMode).isTrue() assertThat(initialState.isInWidgetMode).isTrue()
assertThat(widgetProvider.getWidgetCalled).isTrue() assertThat(widgetProvider.getWidgetCalled).isTrue()
assertThat(widgetDriver.runCalledCount).isEqualTo(1) assertThat(widgetDriver.runCalledCount).isEqualTo(1)
@ -203,6 +205,44 @@ class CallScreenPresenterTest {
} }
} }
@Test
fun `present - a received room member message makes the call to be active`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver,
navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
screenTracker = FakeScreenTracker {},
)
val messageInterceptor = FakeWidgetMessageInterceptor()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.isCallActive).isFalse()
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
messageInterceptor.givenInterceptedMessage(
"""
{
"action":"send_event",
"api":"fromWidget",
"widgetId":"1",
"requestId":"1",
"data":{
"type":"org.matrix.msc3401.call.member"
}
}
""".trimIndent()
)
skipItems(1)
val finalState = awaitItem()
assertThat(finalState.isCallActive).isTrue()
}
}
@Test @Test
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest { fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
val navigator = FakeCallScreenNavigator() val navigator = FakeCallScreenNavigator()

View file

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"Новая комната"</string> <string name="screen_create_room_action_create_room">"Создать новую комнату"</string>
<string name="screen_create_room_add_people_title">"Пригласить в комнату"</string> <string name="screen_create_room_add_people_title">"Пригласить в комнату"</string>
<string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string> <string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string>
<string name="screen_create_room_private_option_description">"Сообщения в этой комнате зашифрованы. Отключить шифрование позже будет невозможно."</string> <string name="screen_create_room_private_option_description">"Сообщения в этой комнате будут зашифрованы. Отключить шифрование позже будет невозможно."</string>
<string name="screen_create_room_private_option_title">"Приватная комната (только по приглашению)"</string> <string name="screen_create_room_private_option_title">"Частная комната (только по приглашениям)"</string>
<string name="screen_create_room_public_option_description">"Сообщения не зашифрованы, каждый может их прочитать. Вы можете включить шифрование позже."</string> <string name="screen_create_room_public_option_description">"Сообщения не будут зашифрованы и каждый сможет их прочитать. Шифрование можно будет включить позже."</string>
<string name="screen_create_room_public_option_title">"Публичная комната (любой)"</string> <string name="screen_create_room_public_option_title">"Общедоступная комната (для всех)"</string>
<string name="screen_create_room_room_name_label">"Название комнаты"</string> <string name="screen_create_room_room_name_label">"Название комнаты"</string>
<string name="screen_create_room_title">"Создать комнату"</string> <string name="screen_create_room_title">"Создать комнату"</string>
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string> <string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при попытке открытия комнаты"</string> <string name="screen_start_chat_error_starting_chat">"Произошла ошибка при запуске чата"</string>
</resources> </resources>

View file

@ -51,7 +51,7 @@ class AccountDeactivationPresenter @Inject constructor(
action action
) )
} else { } else {
action.value = AsyncAction.Confirming action.value = AsyncAction.ConfirmingNoParams
} }
AccountDeactivationEvents.CloseDialogs -> { AccountDeactivationEvents.CloseDialogs -> {
action.value = AsyncAction.Uninitialized action.value = AsyncAction.Uninitialized

View file

@ -20,7 +20,7 @@ open class AccountDeactivationStateProvider : PreviewParameterProvider<AccountDe
), ),
anAccountDeactivationState( anAccountDeactivationState(
deactivateFormState = filledForm, deactivateFormState = filledForm,
accountDeactivationAction = AsyncAction.Confirming, accountDeactivationAction = AsyncAction.ConfirmingNoParams,
), ),
anAccountDeactivationState( anAccountDeactivationState(
deactivateFormState = filledForm, deactivateFormState = filledForm,

View file

@ -24,7 +24,7 @@ fun AccountDeactivationActionDialog(
when (state) { when (state) {
AsyncAction.Uninitialized -> AsyncAction.Uninitialized ->
Unit Unit
AsyncAction.Confirming -> is AsyncAction.Confirming ->
AccountDeactivationConfirmationDialog( AccountDeactivationConfirmationDialog(
onSubmitClick = onConfirmClick, onSubmitClick = onConfirmClick,
onDismiss = onDismissDialog onDismiss = onDismissDialog

View file

@ -1,4 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Калі ласка, пацвердзіце, што вы хочаце дэактываваць свой уліковы запіс. Гэта дзеянне нельга адмяніць."</string>
<string name="screen_deactivate_account_delete_all_messages">"Выдаліць усе мае паведамленні"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Увага: будучыя карыстальнікі могуць бачыць няпоўныя размовы."</string>
<string name="screen_deactivate_account_description_bold_part">"незваротны"</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Назаўсёды адключыць"</string>
<string name="screen_deactivate_account_list_item_2">"Выдаліць вас з усіх чатаў."</string>
<string name="screen_deactivate_account_list_item_3">"Выдаліце інфармацыю аб сваім уліковым запісе з нашага сервера ідэнтыфікацыі."</string>
<string name="screen_deactivate_account_title">"Дэактываваць уліковы запіс"</string> <string name="screen_deactivate_account_title">"Дэактываваць уліковы запіс"</string>
</resources> </resources>

View file

@ -74,7 +74,7 @@ class AccountDeactivationPresenterTest {
skipItems(1) skipItems(1)
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
val updatedState = awaitItem() val updatedState = awaitItem()
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.Confirming) assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
val updatedState2 = awaitItem() val updatedState2 = awaitItem()
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading) assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
@ -102,7 +102,7 @@ class AccountDeactivationPresenterTest {
skipItems(2) skipItems(2)
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
val updatedState = awaitItem() val updatedState = awaitItem()
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.Confirming) assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
val updatedState2 = awaitItem() val updatedState2 = awaitItem()
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading) assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
@ -135,7 +135,7 @@ class AccountDeactivationPresenterTest {
skipItems(2) skipItems(2)
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
val updatedState = awaitItem() val updatedState = awaitItem()
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.Confirming) assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
val updatedState2 = awaitItem() val updatedState2 = awaitItem()
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading) assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)

View file

@ -71,7 +71,7 @@ class AccountDeactivationViewTest {
deactivateFormState = aDeactivateFormState( deactivateFormState = aDeactivateFormState(
password = A_PASSWORD, password = A_PASSWORD,
), ),
accountDeactivationAction = AsyncAction.Confirming, accountDeactivationAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string> <string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
<string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string> <string name="screen_notification_optin_title">"Разрешите отправку уведомлений и ни одно сообщение не будет пропущено"</string>
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string> <string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string> <string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string> <string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>

View file

@ -9,10 +9,8 @@ package io.element.android.features.invite.api.response
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import java.util.Optional
data class AcceptDeclineInviteState( data class AcceptDeclineInviteState(
val invite: Optional<InviteData>,
val acceptAction: AsyncAction<RoomId>, val acceptAction: AsyncAction<RoomId>,
val declineAction: AsyncAction<RoomId>, val declineAction: AsyncAction<RoomId>,
val eventSink: (AcceptDeclineInviteEvents) -> Unit, val eventSink: (AcceptDeclineInviteEvents) -> Unit,

View file

@ -10,23 +10,20 @@ package io.element.android.features.invite.api.response
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import java.util.Optional
open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> { open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> {
override val values: Sequence<AcceptDeclineInviteState> override val values: Sequence<AcceptDeclineInviteState>
get() = sequenceOf( get() = sequenceOf(
anAcceptDeclineInviteState(), anAcceptDeclineInviteState(),
anAcceptDeclineInviteState( anAcceptDeclineInviteState(
invite = Optional.of( declineAction = ConfirmingDeclineInvite(
InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice"), InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice")
), ),
declineAction = AsyncAction.Confirming,
), ),
anAcceptDeclineInviteState( anAcceptDeclineInviteState(
invite = Optional.of( declineAction = ConfirmingDeclineInvite(
InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room"), InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room")
), ),
declineAction = AsyncAction.Confirming,
), ),
anAcceptDeclineInviteState( anAcceptDeclineInviteState(
acceptAction = AsyncAction.Failure(Throwable("Whoops")), acceptAction = AsyncAction.Failure(Throwable("Whoops")),
@ -38,12 +35,10 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDec
} }
fun anAcceptDeclineInviteState( fun anAcceptDeclineInviteState(
invite: Optional<InviteData> = Optional.empty(),
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized, acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized, declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
eventSink: (AcceptDeclineInviteEvents) -> Unit = {} eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
) = AcceptDeclineInviteState( ) = AcceptDeclineInviteState(
invite = invite,
acceptAction = acceptAction, acceptAction = acceptAction,
declineAction = declineAction, declineAction = declineAction,
eventSink = eventSink, eventSink = eventSink,

View file

@ -0,0 +1,14 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.invite.api.response
import io.element.android.libraries.architecture.AsyncAction
data class ConfirmingDeclineInvite(
val inviteData: InviteData,
) : AsyncAction.Confirming

View file

@ -9,15 +9,13 @@ package io.element.android.features.invite.impl.response
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
@ -29,9 +27,7 @@ import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.push.api.notifications.NotificationCleaner import io.element.android.libraries.push.api.notifications.NotificationCleaner
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Optional
import javax.inject.Inject import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
class AcceptDeclineInvitePresenter @Inject constructor( class AcceptDeclineInvitePresenter @Inject constructor(
private val client: MatrixClient, private val client: MatrixClient,
@ -43,35 +39,22 @@ class AcceptDeclineInvitePresenter @Inject constructor(
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val acceptedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val acceptedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val declinedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val declinedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
var currentInvite by remember {
mutableStateOf<Optional<InviteData>>(Optional.empty())
}
fun handleEvents(event: AcceptDeclineInviteEvents) { fun handleEvents(event: AcceptDeclineInviteEvents) {
when (event) { when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> { is AcceptDeclineInviteEvents.AcceptInvite -> {
// currentInvite is used to render the decline confirmation dialog
// and to reuse the roomId when the user confirm the rejection of the invitation.
// Just set it to empty here.
currentInvite = Optional.empty()
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
} }
is AcceptDeclineInviteEvents.DeclineInvite -> { is AcceptDeclineInviteEvents.DeclineInvite -> {
currentInvite = Optional.of(event.invite) declinedAction.value = ConfirmingDeclineInvite(event.invite)
declinedAction.value = AsyncAction.Confirming
} }
is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> { is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
declinedAction.value = AsyncAction.Uninitialized localCoroutineScope.declineInvite(event.roomId, declinedAction)
currentInvite.getOrNull()?.let {
localCoroutineScope.declineInvite(it.roomId, declinedAction)
}
currentInvite = Optional.empty()
} }
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> { is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
currentInvite = Optional.empty()
declinedAction.value = AsyncAction.Uninitialized declinedAction.value = AsyncAction.Uninitialized
} }
@ -86,7 +69,6 @@ class AcceptDeclineInvitePresenter @Inject constructor(
} }
return AcceptDeclineInviteState( return AcceptDeclineInviteState(
invite = currentInvite,
acceptAction = acceptedAction.value, acceptAction = acceptedAction.value,
declineAction = declinedAction.value, declineAction = declinedAction.value,
eventSink = ::handleEvents eventSink = ::handleEvents

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.invite.impl.R import io.element.android.features.invite.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionView
@ -22,7 +23,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlin.jvm.optionals.getOrNull
@Composable @Composable
fun AcceptDeclineInviteView( fun AcceptDeclineInviteView(
@ -45,13 +45,13 @@ fun AcceptDeclineInviteView(
onErrorDismiss = { onErrorDismiss = {
state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError) state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
}, },
confirmationDialog = { confirmationDialog = { confirming ->
val invite = state.invite.getOrNull() // Note: confirming will always be of type ConfirmingDeclineInvite.
if (invite != null) { if (confirming is ConfirmingDeclineInvite) {
DeclineConfirmationDialog( DeclineConfirmationDialog(
invite = invite, invite = confirming.inviteData,
onConfirmClick = { onConfirmClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite) state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(confirming.inviteData.roomId))
}, },
onDismissClick = { onDismissClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite) state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)

View file

@ -8,9 +8,10 @@
package io.element.android.features.invite.impl.response package io.element.android.features.invite.impl.response
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.libraries.matrix.api.core.RoomId
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents { sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents data class ConfirmDeclineInvite(val roomId: RoomId) : InternalAcceptDeclineInviteEvents
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
data object DismissAcceptError : InternalAcceptDeclineInviteEvents data object DismissAcceptError : InternalAcceptDeclineInviteEvents
data object DismissDeclineError : InternalAcceptDeclineInviteEvents data object DismissDeclineError : InternalAcceptDeclineInviteEvents

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Tens a certeza que queres rejeitar o convite para %1$s?"</string> <string name="screen_invites_decline_chat_message">"Tens a certeza que queres rejeitar o convite para entra em %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Rejeitar conite"</string> <string name="screen_invites_decline_chat_title">"Rejeitar convite"</string>
<string name="screen_invites_decline_direct_chat_message">"Tens a certeza que queres rejeitar esta conversa privada com %1$s?"</string> <string name="screen_invites_decline_direct_chat_message">"Tem a certeza que queres rejeitar esta conversa privada com %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Rejeitar conversa"</string> <string name="screen_invites_decline_direct_chat_title">"Rejeitar conversa"</string>
<string name="screen_invites_empty_list">"Sem convites"</string> <string name="screen_invites_empty_list">"Sem convites"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</string> <string name="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</string>

View file

@ -10,6 +10,7 @@ package io.element.android.features.invite.impl.response
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.InviteData
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -33,7 +34,6 @@ import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.util.Optional
class AcceptDeclineInvitePresenterTest { class AcceptDeclineInvitePresenterTest {
@get:Rule @get:Rule
@ -46,7 +46,6 @@ class AcceptDeclineInvitePresenterTest {
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
} }
} }
} }
@ -61,17 +60,13 @@ class AcceptDeclineInvitePresenterTest {
AcceptDeclineInviteEvents.DeclineInvite(inviteData) AcceptDeclineInviteEvents.DeclineInvite(inviteData)
) )
} }
skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.of(inviteData)) assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
assertThat(state.declineAction).isInstanceOf(AsyncAction.Confirming::class.java)
state.eventSink( state.eventSink(
InternalAcceptDeclineInviteEvents.CancelDeclineInvite InternalAcceptDeclineInviteEvents.CancelDeclineInvite
) )
} }
skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
} }
@ -93,22 +88,20 @@ class AcceptDeclineInvitePresenterTest {
AcceptDeclineInviteEvents.DeclineInvite(inviteData) AcceptDeclineInviteEvents.DeclineInvite(inviteData)
) )
} }
skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
state.eventSink( state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId)
) )
} }
skipItems(2) assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink( state.eventSink(
InternalAcceptDeclineInviteEvents.DismissDeclineError InternalAcceptDeclineInviteEvents.DismissDeclineError
) )
} }
skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
cancelAndConsumeRemainingEvents() cancelAndConsumeRemainingEvents()
@ -141,13 +134,13 @@ class AcceptDeclineInvitePresenterTest {
AcceptDeclineInviteEvents.DeclineInvite(inviteData) AcceptDeclineInviteEvents.DeclineInvite(inviteData)
) )
} }
skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
state.eventSink( state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId)
) )
} }
skipItems(2) assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java) assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
} }
@ -173,7 +166,6 @@ class AcceptDeclineInvitePresenterTest {
) )
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
} }
awaitItem().also { state -> awaitItem().also { state ->
@ -183,7 +175,6 @@ class AcceptDeclineInvitePresenterTest {
) )
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
cancelAndConsumeRemainingEvents() cancelAndConsumeRemainingEvents()
@ -220,7 +211,6 @@ class AcceptDeclineInvitePresenterTest {
) )
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
} }
awaitItem().also { state -> awaitItem().also { state ->

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_join_action">"Join room"</string> <string name="screen_join_room_join_action">"Join room"</string>
<string name="screen_join_room_knock_action">"Knock to join"</string> <string name="screen_join_room_knock_action">"Send request to join"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s does not support spaces yet. You can access spaces on web."</string> <string name="screen_join_room_space_not_supported_description">"%1$s does not support spaces yet. You can access spaces on web."</string>
<string name="screen_join_room_space_not_supported_title">"Spaces are not supported yet"</string> <string name="screen_join_room_space_not_supported_title">"Spaces are not supported yet"</string>
<string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. Youll be able to join the conversation once approved."</string> <string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. Youll be able to join the conversation once approved."</string>

View file

@ -9,9 +9,12 @@ package io.element.android.features.joinroom.impl
import io.element.android.features.joinroom.impl.di.KnockRoom import io.element.android.features.joinroom.impl.di.KnockRoom
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.tests.testutils.simulateLongTask
class FakeKnockRoom( class FakeKnockRoom(
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) } var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
) : KnockRoom { ) : KnockRoom {
override suspend fun invoke(roomId: RoomId) = lambda(roomId) override suspend fun invoke(roomId: RoomId) = simulateLongTask {
lambda(roomId)
}
} }

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Вы уверены, что хотите покинуть беседу?"</string> <string name="leave_conversation_alert_subtitle">"Вы уверены, что хотите покинуть беседу? Эта беседа не является общедоступной, и Вы не сможете присоединиться к ней без приглашения."</string>
<string name="leave_room_alert_empty_subtitle">"Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас."</string> <string name="leave_room_alert_empty_subtitle">"Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас."</string>
<string name="leave_room_alert_private_subtitle">"Вы уверены, что хотите покинуть эту комнату? Эта комната не является публичной, и Вы не сможете присоединиться к ней без приглашения."</string> <string name="leave_room_alert_private_subtitle">"Вы уверены, что хотите покинуть эту комнату? Эта комната не является общедоступной, и Вы не сможете присоединиться к ней без приглашения."</string>
<string name="leave_room_alert_subtitle">"Вы уверены, что хотите покинуть комнату?"</string> <string name="leave_room_alert_subtitle">"Вы уверены, что хотите покинуть комнату?"</string>
</resources> </resources>

View file

@ -22,6 +22,8 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS
import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.MessageComposerMode
@ -397,8 +399,7 @@ class SendLocationPresenterTest {
) )
fakeMessageComposerContext.apply { fakeMessageComposerContext.apply {
composerMode = MessageComposerMode.Edit( composerMode = MessageComposerMode.Edit(
eventId = null, eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
transactionId = null,
content = "" content = ""
) )
} }
@ -446,8 +447,7 @@ class SendLocationPresenterTest {
) )
fakeMessageComposerContext.apply { fakeMessageComposerContext.apply {
composerMode = MessageComposerMode.Edit( composerMode = MessageComposerMode.Edit(
eventId = null, eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
transactionId = null,
content = "" content = ""
) )
} }

View file

@ -96,7 +96,7 @@ fun PinUnlockView(
latestOnSuccessLogout(state.signOutAction.data) latestOnSuccessLogout(state.signOutAction.data)
} }
} }
AsyncAction.Confirming, is AsyncAction.Confirming,
is AsyncAction.Failure, is AsyncAction.Failure,
AsyncAction.Uninitialized -> Unit AsyncAction.Uninitialized -> Unit
} }

View file

@ -4,7 +4,7 @@
<string name="screen_app_lock_biometric_unlock">"биометрическая разблокировка"</string> <string name="screen_app_lock_biometric_unlock">"биометрическая разблокировка"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Разблокировать с помощью биометрии"</string> <string name="screen_app_lock_biometric_unlock_title_android">"Разблокировать с помощью биометрии"</string>
<string name="screen_app_lock_forgot_pin">"Забыли PIN-код?"</string> <string name="screen_app_lock_forgot_pin">"Забыли PIN-код?"</string>
<string name="screen_app_lock_settings_change_pin">"Измените PIN-код"</string> <string name="screen_app_lock_settings_change_pin">"Изменить PIN-код"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Разрешить биометрическую разблокировку"</string> <string name="screen_app_lock_settings_enable_biometric_unlock">"Разрешить биометрическую разблокировку"</string>
<string name="screen_app_lock_settings_remove_pin">"Удалить PIN-код"</string> <string name="screen_app_lock_settings_remove_pin">"Удалить PIN-код"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Вы действительно хотите удалить PIN-код?"</string> <string name="screen_app_lock_settings_remove_pin_alert_message">"Вы действительно хотите удалить PIN-код?"</string>
@ -22,16 +22,16 @@
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Повторите PIN-код"</string> <string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Повторите PIN-код"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-коды не совпадают"</string> <string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-коды не совпадают"</string>
<string name="screen_app_lock_signout_alert_message">"Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код"</string> <string name="screen_app_lock_signout_alert_message">"Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код"</string>
<string name="screen_app_lock_signout_alert_title">"Вы выходите из системы"</string> <string name="screen_app_lock_signout_alert_title">"Выполняется выход из системы"</string>
<plurals name="screen_app_lock_subtitle"> <plurals name="screen_app_lock_subtitle">
<item quantity="one">"Вы попытались разблокировать %1$d раз"</item> <item quantity="one">"У вас осталась %1$d попытка на разблокировку"</item>
<item quantity="few">"Вы попытались разблокировать %1$d раз"</item> <item quantity="few">"У вас остались %1$d попытки на разблокировку"</item>
<item quantity="many">"Вы попытались разблокировать много раз"</item> <item quantity="many">"У вас осталось %1$d попыток на разблокировку"</item>
</plurals> </plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin"> <plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Неверный PIN-код. У вас остался %1$d шанс"</item> <item quantity="one">"Неверный PIN-код. У вас осталась %1$d попытка"</item>
<item quantity="few">"Неверный PIN-код. У вас остался %1$d шансов"</item> <item quantity="few">"Неверный PIN-код. У вас остались %1$d попытки"</item>
<item quantity="many">"Неверный PIN-код. У вас остался %1$d шанса"</item> <item quantity="many">"Неверный PIN-код. У вас осталось %1$d попыток"</item>
</plurals> </plurals>
<string name="screen_app_lock_use_biometric_android">"Использовать биометрию"</string> <string name="screen_app_lock_use_biometric_android">"Использовать биометрию"</string>
<string name="screen_app_lock_use_pin_android">"Использовать PIN-код"</string> <string name="screen_app_lock_use_pin_android">"Использовать PIN-код"</string>

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.OidcPrompt
import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.oidc.api.OidcActionFlow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -92,7 +93,8 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!! val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!!
if (matrixHomeServerDetails.supportsOidcLogin) { if (matrixHomeServerDetails.supportsOidcLogin) {
// Retrieve the details right now // Retrieve the details right now
LoginFlow.OidcFlow(authenticationService.getOidcUrl().getOrThrow()) val oidcPrompt = if (params.isAccountCreation) OidcPrompt.Create else OidcPrompt.Consent
LoginFlow.OidcFlow(authenticationService.getOidcUrl(oidcPrompt).getOrThrow())
} else if (params.isAccountCreation) { } else if (params.isAccountCreation) {
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl) val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)
LoginFlow.AccountCreationFlow(url) LoginFlow.AccountCreationFlow(url)

View file

@ -9,7 +9,7 @@ package io.element.android.features.login.impl.screens.qrcode.confirmation
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
class QrCodeConfirmationStepPreviewProvider : PreviewParameterProvider<QrCodeConfirmationStep> { class QrCodeConfirmationStepProvider : PreviewParameterProvider<QrCodeConfirmationStep> {
override val values: Sequence<QrCodeConfirmationStep> override val values: Sequence<QrCodeConfirmationStep>
get() = sequenceOf( get() = sequenceOf(
QrCodeConfirmationStep.DisplayCheckCode("12"), QrCodeConfirmationStep.DisplayCheckCode("12"),

View file

@ -148,7 +148,7 @@ private fun Buttons(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun QrCodeConfirmationViewPreview(@PreviewParameter(QrCodeConfirmationStepPreviewProvider::class) step: QrCodeConfirmationStep) { internal fun QrCodeConfirmationViewPreview(@PreviewParameter(QrCodeConfirmationStepProvider::class) step: QrCodeConfirmationStep) {
ElementPreview { ElementPreview {
QrCodeConfirmationView( QrCodeConfirmationView(
step = step, step = step,

View file

@ -189,7 +189,7 @@ private fun ColumnScope.Buttons(
} }
} }
AsyncAction.Uninitialized, AsyncAction.Uninitialized,
AsyncAction.Confirming -> Unit is AsyncAction.Confirming -> Unit
} }
} }
} }

View file

@ -60,6 +60,7 @@
<string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Адсканіруйце QR-код з дапамогай гэтай прылады"</string> <string name="screen_qr_code_login_initial_state_item_4">"Адсканіруйце QR-код з дапамогай гэтай прылады"</string>
<string name="screen_qr_code_login_initial_state_subtitle">"Даступна толькі ў тым выпадку, калі ваш правайдар уліковага запісу гэта падтрымлівае."</string>
<string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string> <string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>

View file

@ -60,6 +60,7 @@
<string name="screen_qr_code_login_initial_state_item_3">"Επιλογή %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Επιλογή %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"«Σύνδεση νέας συσκευής»"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"«Σύνδεση νέας συσκευής»"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Σάρωσε τον κωδικό QR με αυτήν τη συσκευή"</string> <string name="screen_qr_code_login_initial_state_item_4">"Σάρωσε τον κωδικό QR με αυτήν τη συσκευή"</string>
<string name="screen_qr_code_login_initial_state_subtitle">"Διατίθεται μόνο εάν ο πάροχος του λογαριασμού σου το υποστηρίζει."</string>
<string name="screen_qr_code_login_initial_state_title">"Άνοιγμα %1$s σε άλλη συσκευή για να λήψη κωδικού QR"</string> <string name="screen_qr_code_login_initial_state_title">"Άνοιγμα %1$s σε άλλη συσκευή για να λήψη κωδικού QR"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Χρησιμοποίησε τον κωδικό QR που εμφανίζεται στην άλλη συσκευή."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Χρησιμοποίησε τον κωδικό QR που εμφανίζεται στην άλλη συσκευή."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Προσπάθησε ξανά"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Προσπάθησε ξανά"</string>

View file

@ -60,6 +60,7 @@ Tenta iniciar a sessão manualmente ou digitaliza o código QR com outro disposi
<string name="screen_qr_code_login_initial_state_item_3">"Seleciona %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Seleciona %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Ligar novo dispositivo”"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"“Ligar novo dispositivo”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Lê o código QR com este dispositivo"</string> <string name="screen_qr_code_login_initial_state_item_4">"Lê o código QR com este dispositivo"</string>
<string name="screen_qr_code_login_initial_state_subtitle">"Disponível apenas se o seu fornecedor de conta o suportar."</string>
<string name="screen_qr_code_login_initial_state_title">"Abre a %1$s noutro dispositivo para obteres o código QR"</string> <string name="screen_qr_code_login_initial_state_title">"Abre a %1$s noutro dispositivo para obteres o código QR"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Lê o código QR apresentado no outro dispositivo."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Lê o código QR apresentado no outro dispositivo."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Tentar novamente"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Tentar novamente"</string>

View file

@ -5,9 +5,9 @@
<string name="screen_account_provider_form_notice">"Введите поисковый запрос или адрес домена."</string> <string name="screen_account_provider_form_notice">"Введите поисковый запрос или адрес домена."</string>
<string name="screen_account_provider_form_subtitle">"Поиск компании, сообщества или частного сервера."</string> <string name="screen_account_provider_form_subtitle">"Поиск компании, сообщества или частного сервера."</string>
<string name="screen_account_provider_form_title">"Поиск сервера учетной записи"</string> <string name="screen_account_provider_form_title">"Поиск сервера учетной записи"</string>
<string name="screen_account_provider_signin_subtitle">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string> <string name="screen_account_provider_signin_subtitle">"Здесь будут храниться ваши разговоры — точно так же, как если бы вы использовали почтового провайдера для хранения своих писем."</string>
<string name="screen_account_provider_signin_title">"Вы собираетесь войти в %s"</string> <string name="screen_account_provider_signin_title">"Вы собираетесь войти в %s"</string>
<string name="screen_account_provider_signup_subtitle">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string> <string name="screen_account_provider_signup_subtitle">"Здесь будут храниться ваши разговоры — точно так же, как если бы вы использовали почтового провайдера для хранения своих писем."</string>
<string name="screen_account_provider_signup_title">"Вы собираетесь создать учетную запись на %s"</string> <string name="screen_account_provider_signup_title">"Вы собираетесь создать учетную запись на %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org — это большой бесплатный сервер в общедоступной сети Matrix для безопасной децентрализованной связи, управляемый Matrix.org Foundation."</string> <string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org — это большой бесплатный сервер в общедоступной сети Matrix для безопасной децентрализованной связи, управляемый Matrix.org Foundation."</string>
<string name="screen_change_account_provider_other">"Другое"</string> <string name="screen_change_account_provider_other">"Другое"</string>
@ -27,14 +27,14 @@
<string name="screen_login_error_invalid_user_id">"Это не корректный идентификатор пользователя. Ожидаемый формат: \'@user:homeserver.org\'"</string> <string name="screen_login_error_invalid_user_id">"Это не корректный идентификатор пользователя. Ожидаемый формат: \'@user:homeserver.org\'"</string>
<string name="screen_login_error_refresh_tokens">"Этот сервер настроен на использование токенов обновления. Они не поддерживаются при использовании входа на основе пароля."</string> <string name="screen_login_error_refresh_tokens">"Этот сервер настроен на использование токенов обновления. Они не поддерживаются при использовании входа на основе пароля."</string>
<string name="screen_login_error_unsupported_authentication">"Выбранный домашний сервер не поддерживает пароль или логин OIDC. Пожалуйста, свяжитесь с администратором или выберите другой домашний сервер."</string> <string name="screen_login_error_unsupported_authentication">"Выбранный домашний сервер не поддерживает пароль или логин OIDC. Пожалуйста, свяжитесь с администратором или выберите другой домашний сервер."</string>
<string name="screen_login_form_header">"Введите сведения о себе"</string> <string name="screen_login_form_header">"Введите свои данные"</string>
<string name="screen_login_subtitle">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string> <string name="screen_login_subtitle">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
<string name="screen_login_title">"Рады видеть вас снова!"</string> <string name="screen_login_title">"Рады видеть вас снова!"</string>
<string name="screen_login_title_with_homeserver">"Войти в %1$s"</string> <string name="screen_login_title_with_homeserver">"Войти в %1$s"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Установление безопасного соединения"</string> <string name="screen_qr_code_login_connecting_subtitle">"Установление безопасного соединения"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Что теперь?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Что теперь?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Попробуйте снова войти в систему с помощью QR-кода, если это была сетевая проблема"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Попробуйте снова войти в систему с помощью QR-кода, если это была проблема с соединением"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
@ -55,11 +55,12 @@
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Поставщик учетной записи не поддерживает %1$s."</string> <string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Поставщик учетной записи не поддерживает %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s не поддерживается"</string> <string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s не поддерживается"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Готово к сканированию"</string> <string name="screen_qr_code_login_initial_state_button_title">"Готово к сканированию"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string> <string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на компьютере"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string> <string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Выберите %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string> <string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Отсканируйте QR-код с помощью этого устройства"</string> <string name="screen_qr_code_login_initial_state_item_4">"Отсканируйте QR-код с помощью этого устройства"</string>
<string name="screen_qr_code_login_initial_state_subtitle">"Доступно только в том случае, если ваш поставщик учетной записи поддерживает это."</string>
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string> <string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>
@ -76,7 +77,7 @@
<string name="screen_server_confirmation_change_server">"Сменить поставщика учетной записи"</string> <string name="screen_server_confirmation_change_server">"Сменить поставщика учетной записи"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Частный сервер для сотрудников Element."</string> <string name="screen_server_confirmation_message_login_element_dot_io">"Частный сервер для сотрудников Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string> <string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
<string name="screen_server_confirmation_message_register">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string> <string name="screen_server_confirmation_message_register">"Здесь будут храниться ваши разговоры — точно так же, как если бы вы использовали почтового провайдера для хранения своих писем."</string>
<string name="screen_server_confirmation_title_login">"Вы собираетесь войти в %1$s"</string> <string name="screen_server_confirmation_title_login">"Вы собираетесь войти в %1$s"</string>
<string name="screen_server_confirmation_title_register">"Вы собираетесь создать учетную запись на %1$s"</string> <string name="screen_server_confirmation_title_register">"Вы собираетесь создать учетную запись на %1$s"</string>
</resources> </resources>

View file

@ -14,7 +14,7 @@ open class DirectLogoutStateProvider : PreviewParameterProvider<DirectLogoutStat
override val values: Sequence<DirectLogoutState> override val values: Sequence<DirectLogoutState>
get() = sequenceOf( get() = sequenceOf(
aDirectLogoutState(), aDirectLogoutState(),
aDirectLogoutState(logoutAction = AsyncAction.Confirming), aDirectLogoutState(logoutAction = AsyncAction.ConfirmingNoParams),
aDirectLogoutState(logoutAction = AsyncAction.Loading), aDirectLogoutState(logoutAction = AsyncAction.Loading),
aDirectLogoutState(logoutAction = AsyncAction.Failure(Exception("Error"))), aDirectLogoutState(logoutAction = AsyncAction.Failure(Exception("Error"))),
aDirectLogoutState(logoutAction = AsyncAction.Success("success")), aDirectLogoutState(logoutAction = AsyncAction.Success("success")),

View file

@ -64,7 +64,7 @@ class LogoutPresenter @Inject constructor(
if (logoutAction.value.isConfirming() || event.ignoreSdkError) { if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
localCoroutineScope.logout(logoutAction, event.ignoreSdkError) localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
} else { } else {
logoutAction.value = AsyncAction.Confirming logoutAction.value = AsyncAction.ConfirmingNoParams
} }
} }
LogoutEvents.CloseDialogs -> { LogoutEvents.CloseDialogs -> {

View file

@ -21,7 +21,7 @@ open class LogoutStateProvider : PreviewParameterProvider<LogoutState> {
aLogoutState(isLastDevice = true), aLogoutState(isLastDevice = true),
aLogoutState(isLastDevice = false, backupUploadState = BackupUploadState.Uploading(66, 200)), aLogoutState(isLastDevice = false, backupUploadState = BackupUploadState.Uploading(66, 200)),
aLogoutState(isLastDevice = true, backupUploadState = BackupUploadState.Done), aLogoutState(isLastDevice = true, backupUploadState = BackupUploadState.Done),
aLogoutState(logoutAction = AsyncAction.Confirming), aLogoutState(logoutAction = AsyncAction.ConfirmingNoParams),
aLogoutState(logoutAction = AsyncAction.Loading), aLogoutState(logoutAction = AsyncAction.Loading),
aLogoutState(logoutAction = AsyncAction.Failure(Exception("Failed to logout"))), aLogoutState(logoutAction = AsyncAction.Failure(Exception("Failed to logout"))),
aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))), aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))),

View file

@ -52,7 +52,7 @@ class DirectLogoutPresenter @Inject constructor(
if (logoutAction.value.isConfirming() || event.ignoreSdkError) { if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
localCoroutineScope.logout(logoutAction, event.ignoreSdkError) localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
} else { } else {
logoutAction.value = AsyncAction.Confirming logoutAction.value = AsyncAction.ConfirmingNoParams
} }
} }
DirectLogoutEvents.CloseDialogs -> { DirectLogoutEvents.CloseDialogs -> {

View file

@ -29,7 +29,7 @@ fun LogoutActionDialog(
when (state) { when (state) {
AsyncAction.Uninitialized -> AsyncAction.Uninitialized ->
Unit Unit
AsyncAction.Confirming -> is AsyncAction.Confirming ->
LogoutConfirmationDialog( LogoutConfirmationDialog(
onSubmitClick = onConfirmClick, onSubmitClick = onConfirmClick,
onDismiss = onDismissDialog onDismiss = onDismissDialog

View file

@ -107,7 +107,7 @@ class LogoutPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming) assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
initialState.eventSink.invoke(LogoutEvents.CloseDialogs) initialState.eventSink.invoke(LogoutEvents.CloseDialogs)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized) assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
@ -123,7 +123,7 @@ class LogoutPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming) assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
@ -148,7 +148,7 @@ class LogoutPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming) assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
@ -180,7 +180,7 @@ class LogoutPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming) assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)

View file

@ -48,7 +48,7 @@ class LogoutViewTest {
val eventsRecorder = EventsRecorder<LogoutEvents>() val eventsRecorder = EventsRecorder<LogoutEvents>()
rule.setLogoutView( rule.setLogoutView(
aLogoutState( aLogoutState(
logoutAction = AsyncAction.Confirming, logoutAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )

View file

@ -36,7 +36,7 @@ class DefaultDirectLogoutViewTest {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>() val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView( rule.setDefaultDirectLogoutView(
state = aDirectLogoutState( state = aDirectLogoutState(
logoutAction = AsyncAction.Confirming, logoutAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
@ -49,7 +49,7 @@ class DefaultDirectLogoutViewTest {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>() val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView( rule.setDefaultDirectLogoutView(
state = aDirectLogoutState( state = aDirectLogoutState(
logoutAction = AsyncAction.Confirming, logoutAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
@ -63,7 +63,7 @@ class DefaultDirectLogoutViewTest {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>() val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView( rule.setDefaultDirectLogoutView(
state = aDirectLogoutState( state = aDirectLogoutState(
logoutAction = AsyncAction.Confirming, logoutAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )

View file

@ -88,7 +88,7 @@ class DirectLogoutPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming) assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
initialState.eventSink.invoke(DirectLogoutEvents.CloseDialogs) initialState.eventSink.invoke(DirectLogoutEvents.CloseDialogs)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized) assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
@ -104,7 +104,7 @@ class DirectLogoutPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming) assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
@ -129,7 +129,7 @@ class DirectLogoutPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming) assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
@ -161,7 +161,7 @@ class DirectLogoutPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming) assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)

View file

@ -9,11 +9,12 @@ package io.element.android.features.logout.test
import io.element.android.features.logout.api.LogoutUseCase import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
class FakeLogoutUseCase( class FakeLogoutUseCase(
var logoutLambda: (Boolean) -> String? = { lambdaError() } var logoutLambda: (Boolean) -> String? = { lambdaError() }
) : LogoutUseCase { ) : LogoutUseCase {
override suspend fun logout(ignoreSdkError: Boolean): String? { override suspend fun logout(ignoreSdkError: Boolean): String? = simulateLongTask {
return logoutLambda(ignoreSdkError) logoutLambda(ignoreSdkError)
} }
} }

View file

@ -9,11 +9,11 @@ package io.element.android.features.messages.impl
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
sealed interface MessagesEvents { sealed interface MessagesEvents {
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
data class ToggleReaction(val emoji: String, val uniqueId: UniqueId) : MessagesEvents data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvents
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
data object Dismiss : MessagesEvents data object Dismiss : MessagesEvents
} }

View file

@ -50,7 +50,6 @@ import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BackstackWithOverlayBox
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.architecture.overlay.Overlay import io.element.android.libraries.architecture.overlay.Overlay
import io.element.android.libraries.architecture.overlay.operation.show import io.element.android.libraries.architecture.overlay.operation.show
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
@ -324,7 +323,8 @@ class MessagesFlowNode @AssistedInject constructor(
is TimelineItemImageContent -> { is TimelineItemImageContent -> {
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.filename ?: event.content.body, filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension
@ -341,7 +341,8 @@ class MessagesFlowNode @AssistedInject constructor(
if (event.content.preferredMediaSource != null) { if (event.content.preferredMediaSource != null) {
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.body, filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension
@ -358,7 +359,8 @@ class MessagesFlowNode @AssistedInject constructor(
is TimelineItemVideoContent -> { is TimelineItemVideoContent -> {
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.filename ?: event.content.body, filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension
@ -372,7 +374,8 @@ class MessagesFlowNode @AssistedInject constructor(
is TimelineItemFileContent -> { is TimelineItemFileContent -> {
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.body, filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension
@ -386,7 +389,8 @@ class MessagesFlowNode @AssistedInject constructor(
is TimelineItemAudioContent -> { is TimelineItemAudioContent -> {
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.body, filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension

View file

@ -62,7 +62,6 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
@ -73,6 +72,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.ui.messages.reply.map import io.element.android.libraries.matrix.ui.messages.reply.map
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.room.canCall import io.element.android.libraries.matrix.ui.room.canCall
@ -191,7 +191,7 @@ class MessagesPresenter @AssistedInject constructor(
) )
} }
is MessagesEvents.ToggleReaction -> { is MessagesEvents.ToggleReaction -> {
localCoroutineScope.toggleReaction(event.emoji, event.uniqueId) localCoroutineScope.toggleReaction(event.emoji, event.eventOrTransactionId)
} }
is MessagesEvents.InviteDialogDismissed -> { is MessagesEvents.InviteDialogDismissed -> {
hasDismissedInviteDialog = true hasDismissedInviteDialog = true
@ -327,10 +327,10 @@ class MessagesPresenter @AssistedInject constructor(
private fun CoroutineScope.toggleReaction( private fun CoroutineScope.toggleReaction(
emoji: String, emoji: String,
uniqueId: UniqueId, eventOrTransactionId: EventOrTransactionId,
) = launch(dispatchers.io) { ) = launch(dispatchers.io) {
timelineController.invokeOnCurrentTimeline { timelineController.invokeOnCurrentTimeline {
toggleReaction(emoji, uniqueId) toggleReaction(emoji, eventOrTransactionId)
.onFailure { Timber.e(it) } .onFailure { Timber.e(it) }
} }
} }
@ -360,7 +360,7 @@ class MessagesPresenter @AssistedInject constructor(
private suspend fun handleActionRedact(event: TimelineItem.Event) { private suspend fun handleActionRedact(event: TimelineItem.Event) {
timelineController.invokeOnCurrentTimeline { timelineController.invokeOnCurrentTimeline {
redactEvent(eventId = event.eventId, transactionId = event.transactionId, reason = null) redactEvent(eventOrTransactionId = event.eventOrTransactionId, reason = null)
.onFailure { Timber.e(it) } .onFailure { Timber.e(it) }
} }
} }
@ -377,8 +377,7 @@ class MessagesPresenter @AssistedInject constructor(
} }
else -> { else -> {
val composerMode = MessageComposerMode.Edit( val composerMode = MessageComposerMode.Edit(
targetEvent.eventId, targetEvent.eventOrTransactionId,
targetEvent.transactionId,
(targetEvent.content as? TimelineItemTextBasedContent)?.let { (targetEvent.content as? TimelineItemTextBasedContent)?.let {
if (enableTextFormatting) { if (enableTextFormatting) {
it.htmlBody ?: it.body it.htmlBody ?: it.body

View file

@ -168,7 +168,7 @@ fun MessagesView(
} }
fun onEmojiReactionClick(emoji: String, event: TimelineItem.Event) { fun onEmojiReactionClick(emoji: String, event: TimelineItem.Event) {
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.id)) state.eventSink(MessagesEvents.ToggleReaction(emoji, event.eventOrTransactionId))
} }
fun onEmojiReactionLongClick(emoji: String, event: TimelineItem.Event) { fun onEmojiReactionLongClick(emoji: String, event: TimelineItem.Event) {

View file

@ -31,103 +31,109 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
return sequenceOf( return sequenceOf(
anActionListState(), anActionListState(),
anActionListState().copy(target = ActionListState.Target.Loading(aTimelineItemEvent())), anActionListState().copy(target = ActionListState.Target.Loading(aTimelineItemEvent())),
anActionListState().copy( anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent().copy( event = aTimelineItemEvent(
reactionsState = reactionsState timelineItemReactions = reactionsState
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
anActionListState().copy( anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent( event = aTimelineItemEvent(
content = aTimelineItemImageContent(), content = aTimelineItemImageContent(),
displayNameAmbiguous = true, displayNameAmbiguous = true,
).copy( timelineItemReactions = reactionsState,
reactionsState = reactionsState,
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
anActionListState().copy( anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent(content = aTimelineItemVideoContent()).copy( event = aTimelineItemEvent(
reactionsState = reactionsState content = aTimelineItemVideoContent(),
timelineItemReactions = reactionsState
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
anActionListState().copy( anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent(content = aTimelineItemFileContent()).copy( event = aTimelineItemEvent(
reactionsState = reactionsState content = aTimelineItemFileContent(),
timelineItemReactions = reactionsState
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
anActionListState().copy( anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent(content = aTimelineItemAudioContent()).copy( event = aTimelineItemEvent(
reactionsState = reactionsState content = aTimelineItemAudioContent(),
timelineItemReactions = reactionsState
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
anActionListState().copy( anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent(content = aTimelineItemVoiceContent()).copy( event = aTimelineItemEvent(
reactionsState = reactionsState content = aTimelineItemVoiceContent(),
timelineItemReactions = reactionsState
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
anActionListState().copy( anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent(content = aTimelineItemLocationContent()).copy( event = aTimelineItemEvent(
reactionsState = reactionsState content = aTimelineItemLocationContent(),
timelineItemReactions = reactionsState
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
anActionListState().copy( anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent(content = aTimelineItemLocationContent()).copy( event = aTimelineItemEvent(
reactionsState = reactionsState content = aTimelineItemLocationContent(),
timelineItemReactions = reactionsState
), ),
displayEmojiReactions = false, displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
), ),
), ),
anActionListState().copy( anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent(content = aTimelineItemPollContent()).copy( event = aTimelineItemEvent(
reactionsState = reactionsState content = aTimelineItemPollContent(),
timelineItemReactions = reactionsState
), ),
displayEmojiReactions = false, displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemPollActionList(), actions = aTimelineItemPollActionList(),
), ),
), ),
anActionListState().copy( anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent().copy( event = aTimelineItemEvent(
reactionsState = reactionsState, timelineItemReactions = reactionsState,
messageShield = MessageShield.UnknownDevice(isCritical = true) messageShield = MessageShield.UnknownDevice(isCritical = true)
), ),
displayEmojiReactions = true, displayEmojiReactions = true,
@ -135,7 +141,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
) )
), ),
anActionListState().copy( anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent(), event = aTimelineItemEvent(),
displayEmojiReactions = true, displayEmojiReactions = true,

View file

@ -146,7 +146,7 @@ fun ActionListView(
onDismissRequest = ::onDismiss, onDismissRequest = ::onDismiss,
modifier = modifier, modifier = modifier,
) { ) {
SheetContent( ActionListViewContent(
state = state, state = state,
onActionClick = ::onItemActionClick, onActionClick = ::onItemActionClick,
onEmojiReactionClick = ::onEmojiReactionClick, onEmojiReactionClick = ::onEmojiReactionClick,
@ -161,7 +161,7 @@ fun ActionListView(
} }
@Composable @Composable
private fun SheetContent( private fun ActionListViewContent(
state: ActionListState, state: ActionListState,
onActionClick: (TimelineItemAction) -> Unit, onActionClick: (TimelineItemAction) -> Unit,
onEmojiReactionClick: (String) -> Unit, onEmojiReactionClick: (String) -> Unit,
@ -269,19 +269,19 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
content = { ContentForBody(stringResource(CommonStrings.common_shared_location)) } content = { ContentForBody(stringResource(CommonStrings.common_shared_location)) }
} }
is TimelineItemImageContent -> { is TimelineItemImageContent -> {
content = { ContentForBody(event.content.body) } content = { ContentForBody(event.content.bestDescription) }
} }
is TimelineItemStickerContent -> { is TimelineItemStickerContent -> {
content = { ContentForBody(event.content.body) } content = { ContentForBody(event.content.bestDescription) }
} }
is TimelineItemVideoContent -> { is TimelineItemVideoContent -> {
content = { ContentForBody(event.content.body) } content = { ContentForBody(event.content.bestDescription) }
} }
is TimelineItemFileContent -> { is TimelineItemFileContent -> {
content = { ContentForBody(event.content.body) } content = { ContentForBody(event.content.bestDescription) }
} }
is TimelineItemAudioContent -> { is TimelineItemAudioContent -> {
content = { ContentForBody(event.content.body) } content = { ContentForBody(event.content.bestDescription) }
} }
is TimelineItemVoiceContent -> { is TimelineItemVoiceContent -> {
content = { ContentForBody(textContent) } content = { ContentForBody(textContent) }
@ -442,10 +442,10 @@ private fun EmojiButton(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun SheetContentPreview( internal fun ActionListViewContentPreview(
@PreviewParameter(ActionListStateProvider::class) state: ActionListState @PreviewParameter(ActionListStateProvider::class) state: ActionListState
) = ElementPreview { ) = ElementPreview {
SheetContent( ActionListViewContent(
state = state, state = state,
onActionClick = {}, onActionClick = {},
onEmojiReactionClick = {}, onEmojiReactionClick = {},

View file

@ -105,13 +105,13 @@ class IdentityChangeStatePresenter @Inject constructor(
private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember( private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember(
userId = userId, userId = userId,
disambiguatedDisplayName = disambiguatedDisplayName, displayNameOrDefault = displayNameOrDefault,
avatarData = getAvatarData(AvatarSize.ComposerAlert), avatarData = getAvatarData(AvatarSize.ComposerAlert),
) )
private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember( private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember(
userId = userId, userId = userId,
disambiguatedDisplayName = userId.value, displayNameOrDefault = userId.extractedDisplayName,
avatarData = AvatarData( avatarData = AvatarData(
id = userId.value, id = userId.value,
name = null, name = null,

View file

@ -20,8 +20,16 @@ class IdentityChangeStateProvider : PreviewParameterProvider<IdentityChangeState
anIdentityChangeState(), anIdentityChangeState(),
anIdentityChangeState( anIdentityChangeState(
roomMemberIdentityStateChanges = listOf( roomMemberIdentityStateChanges = listOf(
RoomMemberIdentityStateChange( aRoomMemberIdentityStateChange(
identityRoomMember = anIdentityRoomMember(disambiguatedDisplayName = "Alice"), identityRoomMember = anIdentityRoomMember(),
identityState = IdentityState.PinViolation,
),
),
),
anIdentityChangeState(
roomMemberIdentityStateChanges = listOf(
aRoomMemberIdentityStateChange(
identityRoomMember = anIdentityRoomMember(displayNameOrDefault = "Alice"),
identityState = IdentityState.PinViolation, identityState = IdentityState.PinViolation,
), ),
), ),
@ -29,6 +37,14 @@ class IdentityChangeStateProvider : PreviewParameterProvider<IdentityChangeState
) )
} }
internal fun aRoomMemberIdentityStateChange(
identityRoomMember: IdentityRoomMember = anIdentityRoomMember(),
identityState: IdentityState = IdentityState.PinViolation,
) = RoomMemberIdentityStateChange(
identityRoomMember = identityRoomMember,
identityState = identityState,
)
internal fun anIdentityChangeState( internal fun anIdentityChangeState(
roomMemberIdentityStateChanges: List<RoomMemberIdentityStateChange> = emptyList(), roomMemberIdentityStateChanges: List<RoomMemberIdentityStateChange> = emptyList(),
) = IdentityChangeState( ) = IdentityChangeState(
@ -38,7 +54,7 @@ internal fun anIdentityChangeState(
internal fun anIdentityRoomMember( internal fun anIdentityRoomMember(
userId: UserId = UserId("@alice:example.com"), userId: UserId = UserId("@alice:example.com"),
disambiguatedDisplayName: String = userId.value, displayNameOrDefault: String = userId.extractedDisplayName,
avatarData: AvatarData = AvatarData( avatarData: AvatarData = AvatarData(
id = userId.value, id = userId.value,
name = null, name = null,
@ -47,6 +63,6 @@ internal fun anIdentityRoomMember(
), ),
) = IdentityRoomMember( ) = IdentityRoomMember(
userId = userId, userId = userId,
disambiguatedDisplayName = disambiguatedDisplayName, displayNameOrDefault = displayNameOrDefault,
avatarData = avatarData, avatarData = avatarData,
) )

View file

@ -40,13 +40,27 @@ fun IdentityChangeStateView(
avatar = pinViolationIdentityChange.identityRoomMember.avatarData, avatar = pinViolationIdentityChange.identityRoomMember.avatarData,
content = buildAnnotatedString { content = buildAnnotatedString {
val learnMoreStr = stringResource(CommonStrings.action_learn_more) val learnMoreStr = stringResource(CommonStrings.action_learn_more)
val displayName = pinViolationIdentityChange.identityRoomMember.displayNameOrDefault
val userIdStr = stringResource(
CommonStrings.crypto_identity_change_pin_violation_new_user_id,
pinViolationIdentityChange.identityRoomMember.userId,
)
val fullText = stringResource( val fullText = stringResource(
id = CommonStrings.crypto_identity_change_pin_violation, id = CommonStrings.crypto_identity_change_pin_violation_new,
pinViolationIdentityChange.identityRoomMember.disambiguatedDisplayName, displayName,
userIdStr,
learnMoreStr, learnMoreStr,
) )
val learnMoreStartIndex = fullText.indexOf(learnMoreStr)
append(fullText) append(fullText)
val userIdStartIndex = fullText.indexOf(userIdStr)
addStyle(
style = SpanStyle(
fontWeight = FontWeight.Bold,
),
start = userIdStartIndex,
end = userIdStartIndex + userIdStr.length,
)
val learnMoreStartIndex = fullText.lastIndexOf(learnMoreStr)
addStyle( addStyle(
style = SpanStyle( style = SpanStyle(
textDecoration = TextDecoration.Underline, textDecoration = TextDecoration.Underline,

View file

@ -12,6 +12,6 @@ import io.element.android.libraries.matrix.api.core.UserId
data class IdentityRoomMember( data class IdentityRoomMember(
val userId: UserId, val userId: UserId,
val disambiguatedDisplayName: String, val displayNameOrDefault: String,
val avatarData: AvatarData, val avatarData: AvatarData,
) )

View file

@ -53,6 +53,7 @@ import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.timeline.TimelineException import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.map import io.element.android.libraries.matrix.ui.messages.reply.map
@ -442,12 +443,11 @@ class MessageComposerPresenter @Inject constructor(
intentionalMentions = message.intentionalMentions intentionalMentions = message.intentionalMentions
) )
is MessageComposerMode.Edit -> { is MessageComposerMode.Edit -> {
val eventId = capturedMode.eventId
val transactionId = capturedMode.transactionId
timelineController.invokeOnCurrentTimeline { timelineController.invokeOnCurrentTimeline {
// First try to edit the message in the current timeline // First try to edit the message in the current timeline
editMessage(eventId, transactionId, message.markdown, message.html, message.intentionalMentions) editMessage(capturedMode.eventOrTransactionId, message.markdown, message.html, message.intentionalMentions)
.onFailure { cause -> .onFailure { cause ->
val eventId = capturedMode.eventOrTransactionId.eventId
if (cause is TimelineException.EventNotFound && eventId != null) { if (cause is TimelineException.EventNotFound && eventId != null) {
// if the event is not found in the timeline, try to edit the message directly // if the event is not found in the timeline, try to edit the message directly
room.editMessage(eventId, message.markdown, message.html, message.intentionalMentions) room.editMessage(eventId, message.markdown, message.html, message.intentionalMentions)
@ -581,8 +581,7 @@ class MessageComposerPresenter @Inject constructor(
when (val draftType = draft.draftType) { when (val draftType = draft.draftType) {
ComposerDraftType.NewMessage -> messageComposerContext.composerMode = MessageComposerMode.Normal ComposerDraftType.NewMessage -> messageComposerContext.composerMode = MessageComposerMode.Normal
is ComposerDraftType.Edit -> messageComposerContext.composerMode = MessageComposerMode.Edit( is ComposerDraftType.Edit -> messageComposerContext.composerMode = MessageComposerMode.Edit(
eventId = draftType.eventId, eventOrTransactionId = draftType.eventId.toEventOrTransactionId(),
transactionId = null,
content = htmlText ?: markdownText content = htmlText ?: markdownText
) )
is ComposerDraftType.Reply -> { is ComposerDraftType.Reply -> {
@ -611,7 +610,7 @@ class MessageComposerPresenter @Inject constructor(
val draftType = when (val mode = messageComposerContext.composerMode) { val draftType = when (val mode = messageComposerContext.composerMode) {
is MessageComposerMode.Normal -> ComposerDraftType.NewMessage is MessageComposerMode.Normal -> ComposerDraftType.NewMessage
is MessageComposerMode.Edit -> { is MessageComposerMode.Edit -> {
mode.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) } mode.eventOrTransactionId.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }
} }
is MessageComposerMode.Reply -> ComposerDraftType.Reply(mode.eventId) is MessageComposerMode.Reply -> ComposerDraftType.Reply(mode.eventId)
} }

View file

@ -164,10 +164,10 @@ internal fun aTimelineItemEvent(
groupPosition = groupPosition, groupPosition = groupPosition,
localSendState = sendState, localSendState = sendState,
inReplyTo = inReplyTo, inReplyTo = inReplyTo,
debugInfoProvider = { debugInfo },
isThreaded = isThreaded, isThreaded = isThreaded,
origin = null, origin = null,
messageShield = messageShield, timelineItemDebugInfoProvider = { debugInfo },
messageShieldProvider = { messageShield },
) )
} }

View file

@ -26,7 +26,9 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview {
// For consistency, ensure that there is a message in the timeline (the last one) with an error. // For consistency, ensure that there is a message in the timeline (the last one) with an error.
val messageShield = aCriticalShield() val messageShield = aCriticalShield()
val items = listOf( val items = listOf(
(timelineItems.first() as TimelineItem.Event).copy(messageShield = messageShield) (timelineItems.first() as TimelineItem.Event).copy(
messageShieldProvider = { messageShield },
)
) + timelineItems.drop(1) ) + timelineItems.drop(1)
CompositionLocalProvider( CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(), LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),

View file

@ -29,7 +29,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
@ -101,6 +103,7 @@ fun MessageEventBubble(
val bubbleShape = bubbleShape() val bubbleShape = bubbleShape()
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx() val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx() val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
BoxWithConstraints( BoxWithConstraints(
modifier = modifier modifier = modifier
.graphicsLayer { .graphicsLayer {
@ -112,7 +115,7 @@ fun MessageEventBubble(
drawCircle( drawCircle(
color = Color.Black, color = Color.Black,
center = Offset( center = Offset(
x = 0f, x = if (isRtl) size.width else 0f,
y = yOffsetPx, y = yOffsetPx,
), ),
radius = radiusPx, radius = radiusPx,
@ -129,7 +132,9 @@ fun MessageEventBubble(
.testTag(TestTags.messageBubble) .testTag(TestTags.messageBubble)
.widthIn( .widthIn(
min = MIN_BUBBLE_WIDTH, min = MIN_BUBBLE_WIDTH,
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO).toInt().toDp() max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO)
.toInt()
.toDp()
) )
.clip(bubbleShape) .clip(bubbleShape)
.combinedClickable( .combinedClickable(

View file

@ -27,10 +27,10 @@ class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider<Timel
localSendState = LocalEventSendState.Failed.Unknown("AN_ERROR"), localSendState = LocalEventSendState.Failed.Unknown("AN_ERROR"),
content = aTimelineItemTextContent().copy(isEdited = true), content = aTimelineItemTextContent().copy(isEdited = true),
), ),
aTimelineItemEvent().copy( aTimelineItemEvent(
messageShield = MessageShield.AuthenticityNotGuaranteed(isCritical = false), messageShield = MessageShield.AuthenticityNotGuaranteed(isCritical = false),
), ),
aTimelineItemEvent().copy( aTimelineItemEvent(
messageShield = MessageShield.UnknownDevice(isCritical = true), messageShield = MessageShield.UnknownDevice(isCritical = true),
), ),
) )

View file

@ -301,6 +301,8 @@ private fun TimelineItemEventRowContent(
Modifier Modifier
.constrainAs(sender) { .constrainAs(sender) {
top.linkTo(parent.top) top.linkTo(parent.top)
// Required for correct RTL layout
start.linkTo(parent.start)
} }
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.zIndex(1f) .zIndex(1f)
@ -629,7 +631,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
isMine = isMine, isMine = isMine,
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
aspectRatio = 2.5f aspectRatio = 2.5f
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,

View file

@ -38,7 +38,7 @@ internal fun TimelineItemEventRowForDirectRoomPreview() = ElementPreview {
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
isMine = it, isMine = it,
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
aspectRatio = 5f aspectRatio = 5f
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,

View file

@ -45,7 +45,7 @@ internal fun TimelineItemEventRowShieldPreview() = ElementPreview {
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
isMine = true, isMine = true,
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
aspectRatio = 2.5f aspectRatio = 2.5f
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,
@ -54,7 +54,7 @@ internal fun TimelineItemEventRowShieldPreview() = ElementPreview {
) )
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
aspectRatio = 2.5f aspectRatio = 2.5f
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,

View file

@ -49,7 +49,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(
event = aTimelineItemEvent( event = aTimelineItemEvent(
isMine = it, isMine = it,
timelineItemReactions = aTimelineItemReactions(count = 0), timelineItemReactions = aTimelineItemReactions(count = 0),
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
aspectRatio = 2.5f aspectRatio = 2.5f
), ),
inReplyTo = inReplyToDetails, inReplyTo = inReplyToDetails,

View file

@ -16,13 +16,13 @@ import androidx.compose.ui.Modifier
import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.Emoji
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.hide import io.element.android.libraries.designsystem.theme.components.hide
import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun CustomReactionBottomSheet( fun CustomReactionBottomSheet(
state: CustomReactionState, state: CustomReactionState,
onSelectEmoji: (UniqueId, Emoji) -> Unit, onSelectEmoji: (EventOrTransactionId, Emoji) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
@ -37,7 +37,7 @@ fun CustomReactionBottomSheet(
if (target?.event == null) return if (target?.event == null) return
sheetState.hide(coroutineScope) { sheetState.hide(coroutineScope) {
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet) state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
onSelectEmoji(target.event.id, emoji) onSelectEmoji(target.event.eventOrTransactionId, emoji)
} }
} }

View file

@ -63,7 +63,7 @@ fun TimelineItemAudioView(
Spacer(Modifier.width(spacing)) Spacer(Modifier.width(spacing))
Column { Column {
Text( Text(
text = content.body, text = content.bestDescription,
color = ElementTheme.materialColors.primary, color = ElementTheme.materialColors.primary,
maxLines = 2, maxLines = 2,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,

View file

@ -64,7 +64,7 @@ fun TimelineItemFileView(
Spacer(Modifier.width(spacing)) Spacer(Modifier.width(spacing))
Column { Column {
Text( Text(
text = content.body, text = content.bestDescription,
color = ElementTheme.materialColors.primary, color = ElementTheme.materialColors.primary,
maxLines = 2, maxLines = 2,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,

View file

@ -91,7 +91,7 @@ fun TimelineItemImageView(
model = MediaRequestData( model = MediaRequestData(
source = content.preferredMediaSource, source = content.preferredMediaSource,
kind = MediaRequestData.Kind.File( kind = MediaRequestData.Kind.File(
body = content.filename ?: content.body, fileName = content.filename,
mimeType = content.mimeType, mimeType = content.mimeType,
), ),
), ),
@ -108,7 +108,9 @@ fun TimelineItemImageView(
val caption = if (LocalInspectionMode.current) { val caption = if (LocalInspectionMode.current) {
SpannedString(content.caption) SpannedString(content.caption)
} else { } else {
content.formatted?.body?.takeIf { content.formatted.format == MessageFormat.HTML } ?: SpannedString(content.caption) content.formattedCaption?.body
?.takeIf { content.formattedCaption.format == MessageFormat.HTML }
?: SpannedString(content.caption)
} }
CompositionLocalProvider( CompositionLocalProvider(
LocalContentColor provides ElementTheme.colors.textPrimary, LocalContentColor provides ElementTheme.colors.textPrimary,
@ -158,9 +160,9 @@ internal fun TimelineImageWithCaptionRowPreview() = ElementPreview {
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
isMine = isMine, isMine = isMine,
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
filename = "image.jpg", filename = "image.jpg",
body = "A long caption that may wrap into several lines", caption = "A long caption that may wrap into several lines",
aspectRatio = 2.5f, aspectRatio = 2.5f,
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,
@ -170,9 +172,9 @@ internal fun TimelineImageWithCaptionRowPreview() = ElementPreview {
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
isMine = false, isMine = false,
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
filename = "image.jpg", filename = "image.jpg",
body = "Image with null aspectRatio", caption = "Image with null aspectRatio",
aspectRatio = null, aspectRatio = null,
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,

View file

@ -43,7 +43,7 @@ fun TimelineItemStickerView(
onShowClick: () -> Unit, onShowClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val description = content.body.takeIf { it.isNotEmpty() } ?: stringResource(CommonStrings.common_image) val description = content.bestDescription.takeIf { it.isNotEmpty() } ?: stringResource(CommonStrings.common_image)
Column( Column(
modifier = modifier.semantics { contentDescription = description }, modifier = modifier.semantics { contentDescription = description },
) { ) {
@ -65,7 +65,7 @@ fun TimelineItemStickerView(
model = MediaRequestData( model = MediaRequestData(
source = content.preferredMediaSource, source = content.preferredMediaSource,
kind = MediaRequestData.Kind.File( kind = MediaRequestData.Kind.File(
body = content.body, fileName = content.filename,
mimeType = content.mimeType, mimeType = content.mimeType,
), ),
), ),

View file

@ -76,8 +76,8 @@ fun TimelineItemVideoView(
) { ) {
val containerModifier = if (content.showCaption) { val containerModifier = if (content.showCaption) {
Modifier Modifier
.padding(top = 6.dp) .padding(top = 6.dp)
.clip(RoundedCornerShape(6.dp)) .clip(RoundedCornerShape(6.dp))
} else { } else {
Modifier Modifier
} }
@ -93,12 +93,12 @@ fun TimelineItemVideoView(
var isLoaded by remember { mutableStateOf(false) } var isLoaded by remember { mutableStateOf(false) }
AsyncImage( AsyncImage(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.then(if (isLoaded) Modifier.background(Color.White) else Modifier), .then(if (isLoaded) Modifier.background(Color.White) else Modifier),
model = MediaRequestData( model = MediaRequestData(
source = content.thumbnailSource, source = content.thumbnailSource,
kind = MediaRequestData.Kind.File( kind = MediaRequestData.Kind.File(
body = content.filename ?: content.body, fileName = content.filename,
mimeType = content.mimeType mimeType = content.mimeType
) )
), ),
@ -126,7 +126,9 @@ fun TimelineItemVideoView(
val caption = if (LocalInspectionMode.current) { val caption = if (LocalInspectionMode.current) {
SpannedString(content.caption) SpannedString(content.caption)
} else { } else {
content.formatted?.body?.takeIf { content.formatted.format == MessageFormat.HTML } ?: SpannedString(content.caption) content.formattedCaption?.body
?.takeIf { content.formattedCaption.format == MessageFormat.HTML }
?: SpannedString(content.caption)
} }
CompositionLocalProvider( CompositionLocalProvider(
LocalContentColor provides ElementTheme.colors.textPrimary, LocalContentColor provides ElementTheme.colors.textPrimary,
@ -178,7 +180,7 @@ internal fun TimelineVideoWithCaptionRowPreview() = ElementPreview {
isMine = isMine, isMine = isMine,
content = aTimelineItemVideoContent().copy( content = aTimelineItemVideoContent().copy(
filename = "video.mp4", filename = "video.mp4",
body = "A long caption that may wrap into several lines", caption = "A long caption that may wrap into several lines",
aspectRatio = 2.5f, aspectRatio = 2.5f,
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,
@ -190,7 +192,7 @@ internal fun TimelineVideoWithCaptionRowPreview() = ElementPreview {
isMine = false, isMine = false,
content = aTimelineItemVideoContent().copy( content = aTimelineItemVideoContent().copy(
filename = "video.mp4", filename = "video.mp4",
body = "Video with null aspect ratio", caption = "Video with null aspect ratio",
aspectRatio = null, aspectRatio = null,
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,

View file

@ -89,14 +89,14 @@ fun ReactionSummaryView(
sheetState = sheetState, sheetState = sheetState,
modifier = modifier modifier = modifier
) { ) {
SheetContent(summary = state.target) ReactionSummaryViewContent(summary = state.target)
} }
} }
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun SheetContent( private fun ReactionSummaryViewContent(
summary: ReactionSummaryState.Summary, summary: ReactionSummaryState.Summary,
) { ) {
val animationScope = rememberCoroutineScope() val animationScope = rememberCoroutineScope()
@ -274,8 +274,8 @@ private fun SenderRow(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun SheetContentPreview( internal fun ReactionSummaryViewContentPreview(
@PreviewParameter(ReactionSummaryStateProvider::class) state: ReactionSummaryState @PreviewParameter(ReactionSummaryStateProvider::class) state: ReactionSummaryState
) = ElementPreview { ) = ElementPreview {
SheetContent(summary = state.target as ReactionSummaryState.Summary) ReactionSummaryViewContent(summary = state.target as ReactionSummaryState.Summary)
} }

View file

@ -84,9 +84,9 @@ class TimelineItemContentMessageFactory @Inject constructor(
is ImageMessageType -> { is ImageMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemImageContent( TimelineItemImageContent(
body = messageType.body.trimEnd(),
formatted = messageType.formatted,
filename = messageType.filename, filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
mediaSource = messageType.source, mediaSource = messageType.source,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -95,13 +95,15 @@ class TimelineItemContentMessageFactory @Inject constructor(
height = messageType.info?.height?.toInt(), height = messageType.info?.height?.toInt(),
aspectRatio = aspectRatio, aspectRatio = aspectRatio,
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = messageType.filename?.let { fileExtensionExtractor.extractFromName(it) }.orEmpty() fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
) )
} }
is StickerMessageType -> { is StickerMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemStickerContent( TimelineItemStickerContent(
body = messageType.body.trimEnd(), filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
mediaSource = messageType.source, mediaSource = messageType.source,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -110,7 +112,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
height = messageType.info?.height?.toInt(), height = messageType.info?.height?.toInt(),
aspectRatio = aspectRatio, aspectRatio = aspectRatio,
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtensionExtractor.extractFromName(messageType.body) fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
) )
} }
is LocationMessageType -> { is LocationMessageType -> {
@ -136,9 +138,9 @@ class TimelineItemContentMessageFactory @Inject constructor(
is VideoMessageType -> { is VideoMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemVideoContent( TimelineItemVideoContent(
body = messageType.body.trimEnd(),
formatted = messageType.formatted,
filename = messageType.filename, filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
videoSource = messageType.source, videoSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -148,17 +150,19 @@ class TimelineItemContentMessageFactory @Inject constructor(
blurHash = messageType.info?.blurhash, blurHash = messageType.info?.blurhash,
aspectRatio = aspectRatio, aspectRatio = aspectRatio,
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = messageType.filename?.let { fileExtensionExtractor.extractFromName(it) }.orEmpty(), fileExtension = fileExtensionExtractor.extractFromName(messageType.filename),
) )
} }
is AudioMessageType -> { is AudioMessageType -> {
TimelineItemAudioContent( TimelineItemAudioContent(
body = messageType.body.trimEnd(), filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
mediaSource = messageType.source, mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtensionExtractor.extractFromName(messageType.body), fileExtension = fileExtensionExtractor.extractFromName(messageType.filename),
) )
} }
is VoiceMessageType -> { is VoiceMessageType -> {
@ -166,7 +170,9 @@ class TimelineItemContentMessageFactory @Inject constructor(
true -> { true -> {
TimelineItemVoiceContent( TimelineItemVoiceContent(
eventId = eventId, eventId = eventId,
body = messageType.body.trimEnd(), filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
mediaSource = messageType.source, mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -175,20 +181,24 @@ class TimelineItemContentMessageFactory @Inject constructor(
} }
false -> { false -> {
TimelineItemAudioContent( TimelineItemAudioContent(
body = messageType.body.trimEnd(), filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
mediaSource = messageType.source, mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtensionExtractor.extractFromName(messageType.body), fileExtension = fileExtensionExtractor.extractFromName(messageType.filename),
) )
} }
} }
} }
is FileMessageType -> { is FileMessageType -> {
val fileExtension = fileExtensionExtractor.extractFromName(messageType.body) val fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
TimelineItemFileContent( TimelineItemFileContent(
body = messageType.body.trimEnd(), filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
fileSource = messageType.source, fileSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension), mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),

View file

@ -33,7 +33,9 @@ class TimelineItemContentStickerFactory @Inject constructor(
val aspectRatio = aspectRatioOf(content.info.width, content.info.height) val aspectRatio = aspectRatioOf(content.info.width, content.info.height)
return TimelineItemStickerContent( return TimelineItemStickerContent(
body = content.body, filename = content.filename,
caption = content.body,
formattedCaption = null,
mediaSource = content.source, mediaSource = content.source,
thumbnailSource = content.info.thumbnailSource, thumbnailSource = content.info.thumbnailSource,
mimeType = content.info.mimetype ?: MimeTypes.OctetStream, mimeType = content.info.mimetype ?: MimeTypes.OctetStream,
@ -42,7 +44,7 @@ class TimelineItemContentStickerFactory @Inject constructor(
height = content.info.height?.toInt(), height = content.info.height?.toInt(),
aspectRatio = aspectRatio, aspectRatio = aspectRatio,
formattedFileSize = fileSizeFormatter.format(content.info.size ?: 0), formattedFileSize = fileSizeFormatter.format(content.info.size ?: 0),
fileExtension = fileExtensionExtractor.extractFromName(content.body) fileExtension = fileExtensionExtractor.extractFromName(content.filename)
) )
} }
} }

View file

@ -85,9 +85,9 @@ class TimelineItemEventFactory @AssistedInject constructor(
localSendState = currentTimelineItem.event.localSendState, localSendState = currentTimelineItem.event.localSendState,
inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser), inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser),
isThreaded = currentTimelineItem.event.isThreaded(), isThreaded = currentTimelineItem.event.isThreaded(),
debugInfoProvider = currentTimelineItem.event.debugInfoProvider,
origin = currentTimelineItem.event.origin, origin = currentTimelineItem.event.origin,
messageShield = currentTimelineItem.event.messageShieldProvider.getShield(strict = false) timelineItemDebugInfoProvider = currentTimelineItem.event.timelineItemDebugInfoProvider,
messageShieldProvider = currentTimelineItem.event.messageShieldProvider,
) )
} }

View file

@ -17,10 +17,13 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.EventDebugInfoProvider import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShieldProvider
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemDebugInfoProvider
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
@ -74,9 +77,9 @@ sealed interface TimelineItem {
val localSendState: LocalEventSendState?, val localSendState: LocalEventSendState?,
val inReplyTo: InReplyToDetails?, val inReplyTo: InReplyToDetails?,
val isThreaded: Boolean, val isThreaded: Boolean,
val debugInfoProvider: EventDebugInfoProvider,
val origin: TimelineItemEventOrigin?, val origin: TimelineItemEventOrigin?,
val messageShield: MessageShield?, val timelineItemDebugInfoProvider: TimelineItemDebugInfoProvider,
val messageShieldProvider: MessageShieldProvider,
) : TimelineItem { ) : TimelineItem {
val showSenderInformation = groupPosition.isNew() && !isMine val showSenderInformation = groupPosition.isNew() && !isMine
@ -90,7 +93,14 @@ sealed interface TimelineItem {
val isRemote = eventId != null val isRemote = eventId != null
val debugInfo = debugInfoProvider.get() val eventOrTransactionId: EventOrTransactionId
get() = EventOrTransactionId.from(eventId = eventId, transactionId = transactionId)
// No need to be lazy here?
val messageShield: MessageShield? = messageShieldProvider(strict = false)
val debugInfo: TimelineItemDebugInfo
get() = timelineItemDebugInfoProvider()
} }
@Immutable @Immutable

View file

@ -8,17 +8,20 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
import kotlin.time.Duration import kotlin.time.Duration
data class TimelineItemAudioContent( data class TimelineItemAudioContent(
val body: String, override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val duration: Duration, val duration: Duration,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val mimeType: String, val mimeType: String,
val formattedFileSize: String, val formattedFileSize: String,
val fileExtension: String, val fileExtension: String,
) : TimelineItemEventContent { ) : TimelineItemEventContentWithAttachment {
val fileExtensionAndSize = val fileExtensionAndSize =
formatFileExtensionAndSize( formatFileExtensionAndSize(
fileExtension, fileExtension,

View file

@ -22,8 +22,10 @@ open class TimelineItemAudioContentProvider : PreviewParameterProvider<TimelineI
} }
fun aTimelineItemAudioContent(fileName: String = "A sound.mp3") = TimelineItemAudioContent( fun aTimelineItemAudioContent(fileName: String = "A sound.mp3") = TimelineItemAudioContent(
body = fileName, filename = fileName,
mimeType = MimeTypes.Pdf, caption = null,
formattedCaption = null,
mimeType = MimeTypes.Mp3,
formattedFileSize = "100kB", formattedFileSize = "100kB",
fileExtension = "mp3", fileExtension = "mp3",
duration = 100.milliseconds, duration = 100.milliseconds,

View file

@ -8,12 +8,23 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
@Immutable @Immutable
sealed interface TimelineItemEventContent { sealed interface TimelineItemEventContent {
val type: String val type: String
} }
@Immutable
sealed interface TimelineItemEventContentWithAttachment : TimelineItemEventContent {
val filename: String
val caption: String?
val formattedCaption: FormattedBody?
val bestDescription: String
get() = caption ?: filename
}
/** /**
* Only text based content can be copied. * Only text based content can be copied.
*/ */

View file

@ -8,16 +8,19 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
data class TimelineItemFileContent( data class TimelineItemFileContent(
val body: String, override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val fileSource: MediaSource, val fileSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val formattedFileSize: String, val formattedFileSize: String,
val fileExtension: String, val fileExtension: String,
val mimeType: String, val mimeType: String,
) : TimelineItemEventContent { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemFileContent" override val type: String = "TimelineItemFileContent"
val fileExtensionAndSize = formatFileExtensionAndSize(fileExtension, formattedFileSize) val fileExtensionAndSize = formatFileExtensionAndSize(fileExtension, formattedFileSize)

View file

@ -20,8 +20,12 @@ open class TimelineItemFileContentProvider : PreviewParameterProvider<TimelineIt
) )
} }
fun aTimelineItemFileContent(fileName: String = "A file.pdf") = TimelineItemFileContent( fun aTimelineItemFileContent(
body = fileName, fileName: String = "A file.pdf",
) = TimelineItemFileContent(
filename = fileName,
caption = null,
formattedCaption = null,
thumbnailSource = null, thumbnailSource = null,
fileSource = MediaSource(url = ""), fileSource = MediaSource(url = ""),
mimeType = MimeTypes.Pdf, mimeType = MimeTypes.Pdf,

View file

@ -12,9 +12,9 @@ import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
data class TimelineItemImageContent( data class TimelineItemImageContent(
val body: String, override val filename: String,
val formatted: FormattedBody?, override val caption: String?,
val filename: String?, override val formattedCaption: FormattedBody?,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val formattedFileSize: String, val formattedFileSize: String,
@ -24,11 +24,10 @@ data class TimelineItemImageContent(
val width: Int?, val width: Int?,
val height: Int?, val height: Int?,
val aspectRatio: Float? val aspectRatio: Float?
) : TimelineItemEventContent { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemImageContent" override val type: String = "TimelineItemImageContent"
val showCaption = filename != null && filename != body val showCaption = caption != null
val caption = if (showCaption) body else ""
val preferredMediaSource = if (mimeType == MimeTypes.Gif) { val preferredMediaSource = if (mimeType == MimeTypes.Gif) {
mediaSource mediaSource

View file

@ -23,12 +23,14 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineI
} }
fun aTimelineItemImageContent( fun aTimelineItemImageContent(
aspectRatio: Float = 0.5f, aspectRatio: Float? = 0.5f,
blurhash: String? = A_BLUR_HASH, blurhash: String? = A_BLUR_HASH,
filename: String = "A picture.jpg",
caption: String? = null,
) = TimelineItemImageContent( ) = TimelineItemImageContent(
body = "a body", filename = filename,
formatted = null, caption = caption,
filename = null, formattedCaption = null,
mediaSource = MediaSource(""), mediaSource = MediaSource(""),
thumbnailSource = null, thumbnailSource = null,
mimeType = MimeTypes.IMAGE_JPEG, mimeType = MimeTypes.IMAGE_JPEG,

View file

@ -8,9 +8,12 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
data class TimelineItemStickerContent( data class TimelineItemStickerContent(
val body: String, override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val formattedFileSize: String, val formattedFileSize: String,
@ -20,7 +23,7 @@ data class TimelineItemStickerContent(
val width: Int?, val width: Int?,
val height: Int?, val height: Int?,
val aspectRatio: Float? val aspectRatio: Float?
) : TimelineItemEventContent { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemStickerContent" override val type: String = "TimelineItemStickerContent"
/* Stickers are supposed to be small images so /* Stickers are supposed to be small images so

View file

@ -26,7 +26,9 @@ fun aTimelineItemStickerContent(
aspectRatio: Float = 0.5f, aspectRatio: Float = 0.5f,
blurhash: String? = A_BLUR_HASH, blurhash: String? = A_BLUR_HASH,
) = TimelineItemStickerContent( ) = TimelineItemStickerContent(
body = "a body", filename = "a sticker.gif",
caption = "a body",
formattedCaption = null,
mediaSource = MediaSource(""), mediaSource = MediaSource(""),
thumbnailSource = null, thumbnailSource = null,
mimeType = MimeTypes.IMAGE_JPEG, mimeType = MimeTypes.IMAGE_JPEG,

View file

@ -12,9 +12,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import kotlin.time.Duration import kotlin.time.Duration
data class TimelineItemVideoContent( data class TimelineItemVideoContent(
val body: String, override val filename: String,
val formatted: FormattedBody?, override val caption: String?,
val filename: String?, override val formattedCaption: FormattedBody?,
val duration: Duration, val duration: Duration,
val videoSource: MediaSource, val videoSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
@ -25,9 +25,8 @@ data class TimelineItemVideoContent(
val mimeType: String, val mimeType: String,
val formattedFileSize: String, val formattedFileSize: String,
val fileExtension: String, val fileExtension: String,
) : TimelineItemEventContent { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemImageContent" override val type: String = "TimelineItemImageContent"
val showCaption = filename != null && filename != body val showCaption = caption != null
val caption = if (showCaption) body else ""
} }

View file

@ -27,9 +27,9 @@ fun aTimelineItemVideoContent(
aspectRatio: Float = 0.5f, aspectRatio: Float = 0.5f,
blurhash: String? = A_BLUR_HASH, blurhash: String? = A_BLUR_HASH,
) = TimelineItemVideoContent( ) = TimelineItemVideoContent(
body = "Video.mp4", filename = "Video.mp4",
formatted = null, caption = null,
filename = null, formattedCaption = null,
thumbnailSource = null, thumbnailSource = null,
blurHash = blurhash, blurHash = blurhash,
aspectRatio = aspectRatio, aspectRatio = aspectRatio,

View file

@ -9,16 +9,19 @@ package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlin.time.Duration import kotlin.time.Duration
data class TimelineItemVoiceContent( data class TimelineItemVoiceContent(
val eventId: EventId?, val eventId: EventId?,
val body: String, override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val duration: Duration, val duration: Duration,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val mimeType: String, val mimeType: String,
val waveform: ImmutableList<Float>, val waveform: ImmutableList<Float>,
) : TimelineItemEventContent { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemAudioContent" override val type: String = "TimelineItemAudioContent"
} }

View file

@ -35,17 +35,21 @@ open class TimelineItemVoiceContentProvider : PreviewParameterProvider<TimelineI
} }
fun aTimelineItemVoiceContent( fun aTimelineItemVoiceContent(
eventId: String? = "\$anEventId", eventId: EventId? = EventId("\$anEventId"),
body: String = "body doesn't really matter for a voice message", filename: String = "filename doesn't really matter for a voice message",
caption: String? = "body doesn't really matter for a voice message",
duration: Duration = 61_000.milliseconds, duration: Duration = 61_000.milliseconds,
contentUri: String = "mxc://matrix.org/1234567890abcdefg", contentUri: String = "mxc://matrix.org/1234567890abcdefg",
mimeType: String = MimeTypes.Ogg, mimeType: String = MimeTypes.Ogg,
mediaSource: MediaSource = MediaSource(contentUri),
waveform: List<Float> = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f), waveform: List<Float> = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f),
) = TimelineItemVoiceContent( ) = TimelineItemVoiceContent(
eventId = eventId?.let { EventId(it) }, eventId = eventId,
body = body, filename = filename,
caption = caption,
formattedCaption = null,
duration = duration, duration = duration,
mediaSource = MediaSource(contentUri), mediaSource = mediaSource,
mimeType = mimeType, mimeType = mimeType,
waveform = waveform.toPersistentList(), waveform = waveform.toPersistentList(),
) )

View file

@ -35,12 +35,12 @@ interface VoiceMessageMediaRepo {
* *
* @param mediaSource the media source of the voice message. * @param mediaSource the media source of the voice message.
* @param mimeType the mime type of the voice message. * @param mimeType the mime type of the voice message.
* @param body the body of the voice message. * @param filename the filename of the voice message.
*/ */
fun create( fun create(
mediaSource: MediaSource, mediaSource: MediaSource,
mimeType: String?, mimeType: String?,
body: String?, filename: String?,
): VoiceMessageMediaRepo ): VoiceMessageMediaRepo
} }
@ -61,7 +61,7 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
private val matrixMediaLoader: MatrixMediaLoader, private val matrixMediaLoader: MatrixMediaLoader,
@Assisted private val mediaSource: MediaSource, @Assisted private val mediaSource: MediaSource,
@Assisted("mimeType") private val mimeType: String?, @Assisted("mimeType") private val mimeType: String?,
@Assisted("body") private val body: String?, @Assisted("filename") private val filename: String?,
) : VoiceMessageMediaRepo { ) : VoiceMessageMediaRepo {
@ContributesBinding(RoomScope::class) @ContributesBinding(RoomScope::class)
@AssistedFactory @AssistedFactory
@ -69,7 +69,7 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
override fun create( override fun create(
mediaSource: MediaSource, mediaSource: MediaSource,
@Assisted("mimeType") mimeType: String?, @Assisted("mimeType") mimeType: String?,
@Assisted("body") body: String?, @Assisted("filename") filename: String?,
): DefaultVoiceMessageMediaRepo ): DefaultVoiceMessageMediaRepo
} }
@ -79,7 +79,7 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
else -> matrixMediaLoader.downloadMediaFile( else -> matrixMediaLoader.downloadMediaFile(
source = mediaSource, source = mediaSource,
mimeType = mimeType, mimeType = mimeType,
body = body, filename = filename,
).mapCatching { ).mapCatching {
it.use { mediaFile -> it.use { mediaFile ->
val dest = cachedFile.apply { parentFile?.mkdirs() } val dest = cachedFile.apply { parentFile?.mkdirs() }

View file

@ -37,13 +37,13 @@ interface VoiceMessagePlayer {
* @param eventId The eventId of the voice message event. * @param eventId The eventId of the voice message event.
* @param mediaSource The media source of the voice message. * @param mediaSource The media source of the voice message.
* @param mimeType The mime type of the voice message. * @param mimeType The mime type of the voice message.
* @param body The body of the voice message. * @param filename The filename of the voice message.
*/ */
fun create( fun create(
eventId: EventId?, eventId: EventId?,
mediaSource: MediaSource, mediaSource: MediaSource,
mimeType: String?, mimeType: String?,
body: String?, filename: String?,
): VoiceMessagePlayer ): VoiceMessagePlayer
} }
@ -113,7 +113,7 @@ class DefaultVoiceMessagePlayer(
private val eventId: EventId?, private val eventId: EventId?,
mediaSource: MediaSource, mediaSource: MediaSource,
mimeType: String?, mimeType: String?,
body: String?, filename: String?,
) : VoiceMessagePlayer { ) : VoiceMessagePlayer {
@ContributesBinding(RoomScope::class) // Scoped types can't use @AssistedInject. @ContributesBinding(RoomScope::class) // Scoped types can't use @AssistedInject.
class Factory @Inject constructor( class Factory @Inject constructor(
@ -124,21 +124,21 @@ class DefaultVoiceMessagePlayer(
eventId: EventId?, eventId: EventId?,
mediaSource: MediaSource, mediaSource: MediaSource,
mimeType: String?, mimeType: String?,
body: String?, filename: String?,
): DefaultVoiceMessagePlayer = DefaultVoiceMessagePlayer( ): DefaultVoiceMessagePlayer = DefaultVoiceMessagePlayer(
mediaPlayer = mediaPlayer, mediaPlayer = mediaPlayer,
voiceMessageMediaRepoFactory = voiceMessageMediaRepoFactory, voiceMessageMediaRepoFactory = voiceMessageMediaRepoFactory,
eventId = eventId, eventId = eventId,
mediaSource = mediaSource, mediaSource = mediaSource,
mimeType = mimeType, mimeType = mimeType,
body = body, filename = filename,
) )
} }
private val repo = voiceMessageMediaRepoFactory.create( private val repo = voiceMessageMediaRepoFactory.create(
mediaSource = mediaSource, mediaSource = mediaSource,
mimeType = mimeType, mimeType = mimeType,
body = body filename = filename,
) )
private var internalState = MutableStateFlow( private var internalState = MutableStateFlow(

View file

@ -59,7 +59,7 @@ class VoiceMessagePresenter @AssistedInject constructor(
eventId = content.eventId, eventId = content.eventId,
mediaSource = content.mediaSource, mediaSource = content.mediaSource,
mimeType = content.mimeType, mimeType = content.mimeType,
body = content.body, filename = content.filename,
) )
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)

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