Merge branch 'develop' into renovate/kotlin
This commit is contained in:
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
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'
|
||||
with:
|
||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Αποσύνδεση &amp; Αναβάθμιση"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Αποσύνδεση & Αναβάθμιση"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή."</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_force_logout_title">"Ваш homeserver больше не поддерживает старый протокол. Пожалуйста, выйдите из системы и войдите снова, чтобы продолжить использование приложения."</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ваш домашний сервер больше не поддерживает старый протокол. Пожалуйста, выйдите и войдите в свою учётную запись снова, чтобы продолжить использование приложения."</string>
|
||||
</resources>
|
||||
|
||||
@@ -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:
|
||||
• public & private channels
|
||||
• room moderation & access conUpdatetrol
|
||||
• room moderation & access control
|
||||
• replies, reactions, polls, read receipts, pinned messages, etc.
|
||||
• simultaneous chat & calls (picture in picture)
|
||||
• decentralized & federated communication across organizations
|
||||
@@ -32,4 +32,8 @@ Enjoy the freedom of the Matrix open standard! You have native interoperability
|
||||
Enjoy your right to private conversations - free from data mining, ads and all the rest of it - and stay secure. Only the people in your conversation can read your messages.
|
||||
|
||||
<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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_content_link">"здесь"</string>
|
||||
<string name="screen_analytics_settings_share_data">"Делитесь данными аналитики"</string>
|
||||
<string name="screen_analytics_settings_share_data">"Отправлять аналитические данные"</string>
|
||||
</resources>
|
||||
|
||||
@@ -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_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_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_title">"Ajude a melhorar a %1$s"</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<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_content_link">"здесь"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Вы можете отключить эту функцию в любое время"</string>
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
<!-- Permissions for call foreground services -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
|
||||
<application>
|
||||
@@ -80,7 +80,7 @@
|
||||
android:name=".services.CallForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="phoneCall" />
|
||||
android:foregroundServiceType="microphone" />
|
||||
|
||||
<receiver
|
||||
android:name=".receivers.DeclineCallBroadcastReceiver"
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
|
||||
package io.element.android.features.call.impl.services
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
@@ -33,8 +35,12 @@ import timber.log.Timber
|
||||
class CallForegroundService : Service() {
|
||||
companion object {
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, CallForegroundService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
|
||||
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) {
|
||||
@@ -67,8 +73,8 @@ class CallForegroundService : Service() {
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.ONGOING_CALL)
|
||||
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
||||
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
@@ -180,6 +180,7 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||
urlState = urlState.value,
|
||||
webViewError = webViewError,
|
||||
userAgent = userAgent,
|
||||
isCallActive = isJoinedCall,
|
||||
isInWidgetMode = isInWidgetMode,
|
||||
eventSink = { handleEvents(it) },
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ data class CallScreenState(
|
||||
val urlState: AsyncData<String>,
|
||||
val webViewError: String?,
|
||||
val userAgent: String,
|
||||
val isCallActive: Boolean,
|
||||
val isInWidgetMode: Boolean,
|
||||
val eventSink: (CallScreenEvents) -> Unit,
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ internal fun aCallScreenState(
|
||||
urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
|
||||
webViewError: String? = null,
|
||||
userAgent: String = "",
|
||||
isCallActive: Boolean = true,
|
||||
isInWidgetMode: Boolean = false,
|
||||
eventSink: (CallScreenEvents) -> Unit = {},
|
||||
): CallScreenState {
|
||||
@@ -31,6 +32,7 @@ internal fun aCallScreenState(
|
||||
urlState = urlState,
|
||||
webViewError = webViewError,
|
||||
userAgent = userAgent,
|
||||
isCallActive = isCallActive,
|
||||
isInWidgetMode = isInWidgetMode,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
@@ -95,7 +96,6 @@ class ElementCallActivity :
|
||||
pictureInPicturePresenter.setPipView(this)
|
||||
|
||||
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
requestAudioFocus()
|
||||
|
||||
setContent {
|
||||
val pipState = pictureInPicturePresenter.present()
|
||||
@@ -103,6 +103,12 @@ class ElementCallActivity :
|
||||
ElementThemeApp(appPreferencesStore) {
|
||||
val state = presenter.present()
|
||||
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(
|
||||
state = state,
|
||||
pipState = pipState,
|
||||
@@ -115,6 +121,11 @@ class ElementCallActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCallIsActive() {
|
||||
requestAudioFocus()
|
||||
CallForegroundService.start(this)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListenToAndroidEvents(pipState: PictureInPictureState) {
|
||||
val pipEventSink by rememberUpdatedState(pipState.eventSink)
|
||||
@@ -156,18 +167,6 @@ class ElementCallActivity :
|
||||
setCallType(intent)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
CallForegroundService.stop(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (!isFinishing && !isChangingConfigurations) {
|
||||
CallForegroundService.start(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
releaseAudioFocus()
|
||||
@@ -231,10 +230,10 @@ class ElementCallActivity :
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun requestAudioFocus() {
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||
.build()
|
||||
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)
|
||||
.setAudioAttributes(audioAttributes)
|
||||
.build()
|
||||
@@ -247,7 +246,6 @@ class ElementCallActivity :
|
||||
AudioManager.STREAM_VOICE_CALL,
|
||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,
|
||||
)
|
||||
|
||||
audioFocusChangeListener = listener
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ class CallScreenPresenterTest {
|
||||
assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
|
||||
assertThat(initialState.webViewError).isNull()
|
||||
assertThat(initialState.isInWidgetMode).isFalse()
|
||||
assertThat(initialState.isCallActive).isFalse()
|
||||
analyticsLambda.assertions().isNeverCalled()
|
||||
joinedCallLambda.assertions().isCalledOnce()
|
||||
}
|
||||
@@ -106,6 +107,7 @@ class CallScreenPresenterTest {
|
||||
joinedCallLambda.assertions().isCalledOnce()
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(initialState.isCallActive).isFalse()
|
||||
assertThat(initialState.isInWidgetMode).isTrue()
|
||||
assertThat(widgetProvider.getWidgetCalled).isTrue()
|
||||
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
|
||||
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
|
||||
val navigator = FakeCallScreenNavigator()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_error_creating_room">"Произошла ошибка при создании комнаты"</string>
|
||||
<string name="screen_create_room_private_option_description">"Сообщения в этой комнате зашифрованы. Отключить шифрование позже будет невозможно."</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_title">"Публичная комната (любой)"</string>
|
||||
<string name="screen_create_room_private_option_description">"Сообщения в этой комнате будут зашифрованы. Отключить шифрование позже будет невозможно."</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_title">"Общедоступная комната (для всех)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Название комнаты"</string>
|
||||
<string name="screen_create_room_title">"Создать комнату"</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>
|
||||
|
||||
@@ -51,7 +51,7 @@ class AccountDeactivationPresenter @Inject constructor(
|
||||
action
|
||||
)
|
||||
} else {
|
||||
action.value = AsyncAction.Confirming
|
||||
action.value = AsyncAction.ConfirmingNoParams
|
||||
}
|
||||
AccountDeactivationEvents.CloseDialogs -> {
|
||||
action.value = AsyncAction.Uninitialized
|
||||
|
||||
@@ -20,7 +20,7 @@ open class AccountDeactivationStateProvider : PreviewParameterProvider<AccountDe
|
||||
),
|
||||
anAccountDeactivationState(
|
||||
deactivateFormState = filledForm,
|
||||
accountDeactivationAction = AsyncAction.Confirming,
|
||||
accountDeactivationAction = AsyncAction.ConfirmingNoParams,
|
||||
),
|
||||
anAccountDeactivationState(
|
||||
deactivateFormState = filledForm,
|
||||
|
||||
@@ -24,7 +24,7 @@ fun AccountDeactivationActionDialog(
|
||||
when (state) {
|
||||
AsyncAction.Uninitialized ->
|
||||
Unit
|
||||
AsyncAction.Confirming ->
|
||||
is AsyncAction.Confirming ->
|
||||
AccountDeactivationConfirmationDialog(
|
||||
onSubmitClick = onConfirmClick,
|
||||
onDismiss = onDismissDialog
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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>
|
||||
</resources>
|
||||
|
||||
@@ -74,7 +74,7 @@ class AccountDeactivationPresenterTest {
|
||||
skipItems(1)
|
||||
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
||||
val updatedState = awaitItem()
|
||||
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.Confirming)
|
||||
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
||||
val updatedState2 = awaitItem()
|
||||
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
||||
@@ -102,7 +102,7 @@ class AccountDeactivationPresenterTest {
|
||||
skipItems(2)
|
||||
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
||||
val updatedState = awaitItem()
|
||||
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.Confirming)
|
||||
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
||||
val updatedState2 = awaitItem()
|
||||
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
||||
@@ -135,7 +135,7 @@ class AccountDeactivationPresenterTest {
|
||||
skipItems(2)
|
||||
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
||||
val updatedState = awaitItem()
|
||||
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.Confirming)
|
||||
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
||||
val updatedState2 = awaitItem()
|
||||
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
||||
|
||||
@@ -71,7 +71,7 @@ class AccountDeactivationViewTest {
|
||||
deactivateFormState = aDeactivateFormState(
|
||||
password = A_PASSWORD,
|
||||
),
|
||||
accountDeactivationAction = AsyncAction.Confirming,
|
||||
accountDeactivationAction = AsyncAction.ConfirmingNoParams,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<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_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
||||
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
||||
|
||||
@@ -9,10 +9,8 @@ package io.element.android.features.invite.api.response
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import java.util.Optional
|
||||
|
||||
data class AcceptDeclineInviteState(
|
||||
val invite: Optional<InviteData>,
|
||||
val acceptAction: AsyncAction<RoomId>,
|
||||
val declineAction: AsyncAction<RoomId>,
|
||||
val eventSink: (AcceptDeclineInviteEvents) -> Unit,
|
||||
|
||||
@@ -10,23 +10,20 @@ package io.element.android.features.invite.api.response
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import java.util.Optional
|
||||
|
||||
open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> {
|
||||
override val values: Sequence<AcceptDeclineInviteState>
|
||||
get() = sequenceOf(
|
||||
anAcceptDeclineInviteState(),
|
||||
anAcceptDeclineInviteState(
|
||||
invite = Optional.of(
|
||||
InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice"),
|
||||
declineAction = ConfirmingDeclineInvite(
|
||||
InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice")
|
||||
),
|
||||
declineAction = AsyncAction.Confirming,
|
||||
),
|
||||
anAcceptDeclineInviteState(
|
||||
invite = Optional.of(
|
||||
InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room"),
|
||||
declineAction = ConfirmingDeclineInvite(
|
||||
InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room")
|
||||
),
|
||||
declineAction = AsyncAction.Confirming,
|
||||
),
|
||||
anAcceptDeclineInviteState(
|
||||
acceptAction = AsyncAction.Failure(Throwable("Whoops")),
|
||||
@@ -38,12 +35,10 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDec
|
||||
}
|
||||
|
||||
fun anAcceptDeclineInviteState(
|
||||
invite: Optional<InviteData> = Optional.empty(),
|
||||
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||
eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
|
||||
) = AcceptDeclineInviteState(
|
||||
invite = invite,
|
||||
acceptAction = acceptAction,
|
||||
declineAction = declineAction,
|
||||
eventSink = eventSink,
|
||||
|
||||
@@ -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
|
||||
@@ -9,15 +9,13 @@ package io.element.android.features.invite.impl.response
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
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.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.Presenter
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Optional
|
||||
import javax.inject.Inject
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
class AcceptDeclineInvitePresenter @Inject constructor(
|
||||
private val client: MatrixClient,
|
||||
@@ -43,35 +39,22 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val acceptedAction: 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) {
|
||||
when (event) {
|
||||
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)
|
||||
}
|
||||
|
||||
is AcceptDeclineInviteEvents.DeclineInvite -> {
|
||||
currentInvite = Optional.of(event.invite)
|
||||
declinedAction.value = AsyncAction.Confirming
|
||||
declinedAction.value = ConfirmingDeclineInvite(event.invite)
|
||||
}
|
||||
|
||||
is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
|
||||
declinedAction.value = AsyncAction.Uninitialized
|
||||
currentInvite.getOrNull()?.let {
|
||||
localCoroutineScope.declineInvite(it.roomId, declinedAction)
|
||||
}
|
||||
currentInvite = Optional.empty()
|
||||
localCoroutineScope.declineInvite(event.roomId, declinedAction)
|
||||
}
|
||||
|
||||
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
|
||||
currentInvite = Optional.empty()
|
||||
declinedAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
|
||||
@@ -86,7 +69,6 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||
}
|
||||
|
||||
return AcceptDeclineInviteState(
|
||||
invite = currentInvite,
|
||||
acceptAction = acceptedAction.value,
|
||||
declineAction = declinedAction.value,
|
||||
eventSink = ::handleEvents
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
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.ConfirmingDeclineInvite
|
||||
import io.element.android.features.invite.api.response.InviteData
|
||||
import io.element.android.features.invite.impl.R
|
||||
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.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@Composable
|
||||
fun AcceptDeclineInviteView(
|
||||
@@ -45,13 +45,13 @@ fun AcceptDeclineInviteView(
|
||||
onErrorDismiss = {
|
||||
state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
|
||||
},
|
||||
confirmationDialog = {
|
||||
val invite = state.invite.getOrNull()
|
||||
if (invite != null) {
|
||||
confirmationDialog = { confirming ->
|
||||
// Note: confirming will always be of type ConfirmingDeclineInvite.
|
||||
if (confirming is ConfirmingDeclineInvite) {
|
||||
DeclineConfirmationDialog(
|
||||
invite = invite,
|
||||
invite = confirming.inviteData,
|
||||
onConfirmClick = {
|
||||
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite)
|
||||
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(confirming.inviteData.roomId))
|
||||
},
|
||||
onDismissClick = {
|
||||
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
package io.element.android.features.invite.impl.response
|
||||
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
|
||||
data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents
|
||||
data class ConfirmDeclineInvite(val roomId: RoomId) : InternalAcceptDeclineInviteEvents
|
||||
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
|
||||
data object DismissAcceptError : InternalAcceptDeclineInviteEvents
|
||||
data object DismissDeclineError : InternalAcceptDeclineInviteEvents
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_title">"Rejeitar conite"</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_chat_message">"Tens a certeza que queres rejeitar o convite para entra em %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Rejeitar convite"</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_empty_list">"Sem convites"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</string>
|
||||
|
||||
@@ -10,6 +10,7 @@ package io.element.android.features.invite.impl.response
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.ConfirmingDeclineInvite
|
||||
import io.element.android.features.invite.api.response.InviteData
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
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 org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.Optional
|
||||
|
||||
class AcceptDeclineInvitePresenterTest {
|
||||
@get:Rule
|
||||
@@ -46,7 +46,6 @@ class AcceptDeclineInvitePresenterTest {
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.acceptAction).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)
|
||||
)
|
||||
}
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.invite).isEqualTo(Optional.of(inviteData))
|
||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Confirming::class.java)
|
||||
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
|
||||
state.eventSink(
|
||||
InternalAcceptDeclineInviteEvents.CancelDeclineInvite
|
||||
)
|
||||
}
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
|
||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
}
|
||||
}
|
||||
@@ -93,22 +88,20 @@ class AcceptDeclineInvitePresenterTest {
|
||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
||||
)
|
||||
}
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
|
||||
state.eventSink(
|
||||
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
|
||||
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId)
|
||||
)
|
||||
}
|
||||
skipItems(2)
|
||||
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
state.eventSink(
|
||||
InternalAcceptDeclineInviteEvents.DismissDeclineError
|
||||
)
|
||||
}
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
|
||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
}
|
||||
cancelAndConsumeRemainingEvents()
|
||||
@@ -141,13 +134,13 @@ class AcceptDeclineInvitePresenterTest {
|
||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
||||
)
|
||||
}
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
|
||||
state.eventSink(
|
||||
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
|
||||
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId)
|
||||
)
|
||||
}
|
||||
skipItems(2)
|
||||
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
}
|
||||
@@ -173,7 +166,6 @@ class AcceptDeclineInvitePresenterTest {
|
||||
)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
|
||||
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
@@ -183,7 +175,6 @@ class AcceptDeclineInvitePresenterTest {
|
||||
)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
|
||||
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
}
|
||||
cancelAndConsumeRemainingEvents()
|
||||
@@ -220,7 +211,6 @@ class AcceptDeclineInvitePresenterTest {
|
||||
)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
|
||||
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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_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. You’ll be able to join the conversation once approved."</string>
|
||||
|
||||
@@ -9,9 +9,12 @@ package io.element.android.features.joinroom.impl
|
||||
|
||||
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeKnockRoom(
|
||||
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
|
||||
) : KnockRoom {
|
||||
override suspend fun invoke(roomId: RoomId) = lambda(roomId)
|
||||
override suspend fun invoke(roomId: RoomId) = simulateLongTask {
|
||||
lambda(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_private_subtitle">"Вы уверены, что хотите покинуть эту комнату? Эта комната не является публичной, и Вы не сможете присоединиться к ней без приглашения."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Вы уверены, что хотите покинуть эту комнату? Эта комната не является общедоступной, и Вы не сможете присоединиться к ней без приглашения."</string>
|
||||
<string name="leave_room_alert_subtitle">"Вы уверены, что хотите покинуть комнату?"</string>
|
||||
</resources>
|
||||
|
||||
@@ -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.libraries.matrix.api.room.MatrixRoom
|
||||
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.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
@@ -397,8 +399,7 @@ class SendLocationPresenterTest {
|
||||
)
|
||||
fakeMessageComposerContext.apply {
|
||||
composerMode = MessageComposerMode.Edit(
|
||||
eventId = null,
|
||||
transactionId = null,
|
||||
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
||||
content = ""
|
||||
)
|
||||
}
|
||||
@@ -446,8 +447,7 @@ class SendLocationPresenterTest {
|
||||
)
|
||||
fakeMessageComposerContext.apply {
|
||||
composerMode = MessageComposerMode.Edit(
|
||||
eventId = null,
|
||||
transactionId = null,
|
||||
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
||||
content = ""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ fun PinUnlockView(
|
||||
latestOnSuccessLogout(state.signOutAction.data)
|
||||
}
|
||||
}
|
||||
AsyncAction.Confirming,
|
||||
is AsyncAction.Confirming,
|
||||
is AsyncAction.Failure,
|
||||
AsyncAction.Uninitialized -> Unit
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<string name="screen_app_lock_biometric_unlock">"биометрическая разблокировка"</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_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_remove_pin">"Удалить 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_title">"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">
|
||||
<item quantity="one">"Вы попытались разблокировать %1$d раз"</item>
|
||||
<item quantity="few">"Вы попытались разблокировать %1$d раз"</item>
|
||||
<item quantity="many">"Вы попытались разблокировать много раз"</item>
|
||||
<item quantity="one">"У вас осталась %1$d попытка на разблокировку"</item>
|
||||
<item quantity="few">"У вас остались %1$d попытки на разблокировку"</item>
|
||||
<item quantity="many">"У вас осталось %1$d попыток на разблокировку"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||
<item quantity="one">"Неверный PIN-код. У вас остался %1$d шанс"</item>
|
||||
<item quantity="few">"Неверный PIN-код. У вас остался %1$d шансов"</item>
|
||||
<item quantity="many">"Неверный PIN-код. У вас остался %1$d шанса"</item>
|
||||
<item quantity="one">"Неверный PIN-код. У вас осталась %1$d попытка"</item>
|
||||
<item quantity="few">"Неверный PIN-код. У вас остались %1$d попытки"</item>
|
||||
<item quantity="many">"Неверный PIN-код. У вас осталось %1$d попыток"</item>
|
||||
</plurals>
|
||||
<string name="screen_app_lock_use_biometric_android">"Использовать биометрию"</string>
|
||||
<string name="screen_app_lock_use_pin_android">"Использовать PIN-код"</string>
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
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.OidcActionFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -92,7 +93,8 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||
val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!!
|
||||
if (matrixHomeServerDetails.supportsOidcLogin) {
|
||||
// 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) {
|
||||
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)
|
||||
LoginFlow.AccountCreationFlow(url)
|
||||
|
||||
@@ -9,7 +9,7 @@ package io.element.android.features.login.impl.screens.qrcode.confirmation
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
class QrCodeConfirmationStepPreviewProvider : PreviewParameterProvider<QrCodeConfirmationStep> {
|
||||
class QrCodeConfirmationStepProvider : PreviewParameterProvider<QrCodeConfirmationStep> {
|
||||
override val values: Sequence<QrCodeConfirmationStep>
|
||||
get() = sequenceOf(
|
||||
QrCodeConfirmationStep.DisplayCheckCode("12"),
|
||||
@@ -148,7 +148,7 @@ private fun Buttons(
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun QrCodeConfirmationViewPreview(@PreviewParameter(QrCodeConfirmationStepPreviewProvider::class) step: QrCodeConfirmationStep) {
|
||||
internal fun QrCodeConfirmationViewPreview(@PreviewParameter(QrCodeConfirmationStepProvider::class) step: QrCodeConfirmationStep) {
|
||||
ElementPreview {
|
||||
QrCodeConfirmationView(
|
||||
step = step,
|
||||
|
||||
@@ -189,7 +189,7 @@ private fun ColumnScope.Buttons(
|
||||
}
|
||||
}
|
||||
AsyncAction.Uninitialized,
|
||||
AsyncAction.Confirming -> Unit
|
||||
is AsyncAction.Confirming -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_action">"“Звязаць новую прыладу”"</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_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
|
||||
|
||||
@@ -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_action">"«Σύνδεση νέας συσκευής»"</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_invalid_scan_state_description">"Χρησιμοποίησε τον κωδικό QR που εμφανίζεται στην άλλη συσκευή."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Προσπάθησε ξανά"</string>
|
||||
|
||||
@@ -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_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_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_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>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<string name="screen_account_provider_form_notice">"Введите поисковый запрос или адрес домена."</string>
|
||||
<string name="screen_account_provider_form_subtitle">"Поиск компании, сообщества или частного сервера."</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_signup_subtitle">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"Здесь будут храниться ваши разговоры — точно так же, как если бы вы использовали почтового провайдера для хранения своих писем."</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_other">"Другое"</string>
|
||||
@@ -27,14 +27,14 @@
|
||||
<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_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_title">"Рады видеть вас снова!"</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_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_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_3">"Если это не помогло, войдите вручную"</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_title">"%1$s не поддерживается"</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_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_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_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</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_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_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_register">"Вы собираетесь создать учетную запись на %1$s"</string>
|
||||
</resources>
|
||||
|
||||
@@ -14,7 +14,7 @@ open class DirectLogoutStateProvider : PreviewParameterProvider<DirectLogoutStat
|
||||
override val values: Sequence<DirectLogoutState>
|
||||
get() = sequenceOf(
|
||||
aDirectLogoutState(),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.Confirming),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.ConfirmingNoParams),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.Loading),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.Failure(Exception("Error"))),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.Success("success")),
|
||||
|
||||
@@ -64,7 +64,7 @@ class LogoutPresenter @Inject constructor(
|
||||
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
|
||||
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
|
||||
} else {
|
||||
logoutAction.value = AsyncAction.Confirming
|
||||
logoutAction.value = AsyncAction.ConfirmingNoParams
|
||||
}
|
||||
}
|
||||
LogoutEvents.CloseDialogs -> {
|
||||
|
||||
@@ -21,7 +21,7 @@ open class LogoutStateProvider : PreviewParameterProvider<LogoutState> {
|
||||
aLogoutState(isLastDevice = true),
|
||||
aLogoutState(isLastDevice = false, backupUploadState = BackupUploadState.Uploading(66, 200)),
|
||||
aLogoutState(isLastDevice = true, backupUploadState = BackupUploadState.Done),
|
||||
aLogoutState(logoutAction = AsyncAction.Confirming),
|
||||
aLogoutState(logoutAction = AsyncAction.ConfirmingNoParams),
|
||||
aLogoutState(logoutAction = AsyncAction.Loading),
|
||||
aLogoutState(logoutAction = AsyncAction.Failure(Exception("Failed to logout"))),
|
||||
aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))),
|
||||
|
||||
@@ -52,7 +52,7 @@ class DirectLogoutPresenter @Inject constructor(
|
||||
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
|
||||
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
|
||||
} else {
|
||||
logoutAction.value = AsyncAction.Confirming
|
||||
logoutAction.value = AsyncAction.ConfirmingNoParams
|
||||
}
|
||||
}
|
||||
DirectLogoutEvents.CloseDialogs -> {
|
||||
|
||||
@@ -29,7 +29,7 @@ fun LogoutActionDialog(
|
||||
when (state) {
|
||||
AsyncAction.Uninitialized ->
|
||||
Unit
|
||||
AsyncAction.Confirming ->
|
||||
is AsyncAction.Confirming ->
|
||||
LogoutConfirmationDialog(
|
||||
onSubmitClick = onConfirmClick,
|
||||
onDismiss = onDismissDialog
|
||||
|
||||
@@ -107,7 +107,7 @@ class LogoutPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
initialState.eventSink.invoke(LogoutEvents.CloseDialogs)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
@@ -123,7 +123,7 @@ class LogoutPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
@@ -148,7 +148,7 @@ class LogoutPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
@@ -180,7 +180,7 @@ class LogoutPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
|
||||
@@ -48,7 +48,7 @@ class LogoutViewTest {
|
||||
val eventsRecorder = EventsRecorder<LogoutEvents>()
|
||||
rule.setLogoutView(
|
||||
aLogoutState(
|
||||
logoutAction = AsyncAction.Confirming,
|
||||
logoutAction = AsyncAction.ConfirmingNoParams,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ class DefaultDirectLogoutViewTest {
|
||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
||||
rule.setDefaultDirectLogoutView(
|
||||
state = aDirectLogoutState(
|
||||
logoutAction = AsyncAction.Confirming,
|
||||
logoutAction = AsyncAction.ConfirmingNoParams,
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
@@ -49,7 +49,7 @@ class DefaultDirectLogoutViewTest {
|
||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
||||
rule.setDefaultDirectLogoutView(
|
||||
state = aDirectLogoutState(
|
||||
logoutAction = AsyncAction.Confirming,
|
||||
logoutAction = AsyncAction.ConfirmingNoParams,
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
@@ -63,7 +63,7 @@ class DefaultDirectLogoutViewTest {
|
||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
||||
rule.setDefaultDirectLogoutView(
|
||||
state = aDirectLogoutState(
|
||||
logoutAction = AsyncAction.Confirming,
|
||||
logoutAction = AsyncAction.ConfirmingNoParams,
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ class DirectLogoutPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
initialState.eventSink.invoke(DirectLogoutEvents.CloseDialogs)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
@@ -104,7 +104,7 @@ class DirectLogoutPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
@@ -129,7 +129,7 @@ class DirectLogoutPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
@@ -161,7 +161,7 @@ class DirectLogoutPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
|
||||
@@ -9,11 +9,12 @@ package io.element.android.features.logout.test
|
||||
|
||||
import io.element.android.features.logout.api.LogoutUseCase
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeLogoutUseCase(
|
||||
var logoutLambda: (Boolean) -> String? = { lambdaError() }
|
||||
) : LogoutUseCase {
|
||||
override suspend fun logout(ignoreSdkError: Boolean): String? {
|
||||
return logoutLambda(ignoreSdkError)
|
||||
override suspend fun logout(ignoreSdkError: Boolean): String? = simulateLongTask {
|
||||
logoutLambda(ignoreSdkError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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 {
|
||||
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 object Dismiss : MessagesEvents
|
||||
}
|
||||
|
||||
@@ -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.BaseFlowNode
|
||||
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.operation.show
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
@@ -324,7 +323,8 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
is TimelineItemImageContent -> {
|
||||
val navTarget = NavTarget.MediaViewer(
|
||||
mediaInfo = MediaInfo(
|
||||
name = event.content.filename ?: event.content.body,
|
||||
filename = event.content.filename,
|
||||
caption = event.content.caption,
|
||||
mimeType = event.content.mimeType,
|
||||
formattedFileSize = event.content.formattedFileSize,
|
||||
fileExtension = event.content.fileExtension
|
||||
@@ -341,7 +341,8 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
if (event.content.preferredMediaSource != null) {
|
||||
val navTarget = NavTarget.MediaViewer(
|
||||
mediaInfo = MediaInfo(
|
||||
name = event.content.body,
|
||||
filename = event.content.filename,
|
||||
caption = event.content.caption,
|
||||
mimeType = event.content.mimeType,
|
||||
formattedFileSize = event.content.formattedFileSize,
|
||||
fileExtension = event.content.fileExtension
|
||||
@@ -358,7 +359,8 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
is TimelineItemVideoContent -> {
|
||||
val navTarget = NavTarget.MediaViewer(
|
||||
mediaInfo = MediaInfo(
|
||||
name = event.content.filename ?: event.content.body,
|
||||
filename = event.content.filename,
|
||||
caption = event.content.caption,
|
||||
mimeType = event.content.mimeType,
|
||||
formattedFileSize = event.content.formattedFileSize,
|
||||
fileExtension = event.content.fileExtension
|
||||
@@ -372,7 +374,8 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
is TimelineItemFileContent -> {
|
||||
val navTarget = NavTarget.MediaViewer(
|
||||
mediaInfo = MediaInfo(
|
||||
name = event.content.body,
|
||||
filename = event.content.filename,
|
||||
caption = event.content.caption,
|
||||
mimeType = event.content.mimeType,
|
||||
formattedFileSize = event.content.formattedFileSize,
|
||||
fileExtension = event.content.fileExtension
|
||||
@@ -386,7 +389,8 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
is TimelineItemAudioContent -> {
|
||||
val navTarget = NavTarget.MediaViewer(
|
||||
mediaInfo = MediaInfo(
|
||||
name = event.content.body,
|
||||
filename = event.content.filename,
|
||||
caption = event.content.caption,
|
||||
mimeType = event.content.mimeType,
|
||||
formattedFileSize = event.content.formattedFileSize,
|
||||
fileExtension = event.content.fileExtension
|
||||
|
||||
@@ -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.featureflag.api.FeatureFlagService
|
||||
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.room.MatrixRoom
|
||||
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.canRedactOwn
|
||||
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.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.room.canCall
|
||||
@@ -191,7 +191,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
is MessagesEvents.ToggleReaction -> {
|
||||
localCoroutineScope.toggleReaction(event.emoji, event.uniqueId)
|
||||
localCoroutineScope.toggleReaction(event.emoji, event.eventOrTransactionId)
|
||||
}
|
||||
is MessagesEvents.InviteDialogDismissed -> {
|
||||
hasDismissedInviteDialog = true
|
||||
@@ -327,10 +327,10 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
|
||||
private fun CoroutineScope.toggleReaction(
|
||||
emoji: String,
|
||||
uniqueId: UniqueId,
|
||||
eventOrTransactionId: EventOrTransactionId,
|
||||
) = launch(dispatchers.io) {
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
toggleReaction(emoji, uniqueId)
|
||||
toggleReaction(emoji, eventOrTransactionId)
|
||||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
}
|
||||
@@ -360,7 +360,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
|
||||
private suspend fun handleActionRedact(event: TimelineItem.Event) {
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
redactEvent(eventId = event.eventId, transactionId = event.transactionId, reason = null)
|
||||
redactEvent(eventOrTransactionId = event.eventOrTransactionId, reason = null)
|
||||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
}
|
||||
@@ -377,8 +377,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
}
|
||||
else -> {
|
||||
val composerMode = MessageComposerMode.Edit(
|
||||
targetEvent.eventId,
|
||||
targetEvent.transactionId,
|
||||
targetEvent.eventOrTransactionId,
|
||||
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
|
||||
if (enableTextFormatting) {
|
||||
it.htmlBody ?: it.body
|
||||
|
||||
@@ -168,7 +168,7 @@ fun MessagesView(
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -31,103 +31,109 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
return sequenceOf(
|
||||
anActionListState(),
|
||||
anActionListState().copy(target = ActionListState.Target.Loading(aTimelineItemEvent())),
|
||||
anActionListState().copy(
|
||||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent().copy(
|
||||
reactionsState = reactionsState
|
||||
event = aTimelineItemEvent(
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
),
|
||||
anActionListState().copy(
|
||||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(
|
||||
content = aTimelineItemImageContent(),
|
||||
displayNameAmbiguous = true,
|
||||
).copy(
|
||||
reactionsState = reactionsState,
|
||||
timelineItemReactions = reactionsState,
|
||||
),
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
),
|
||||
anActionListState().copy(
|
||||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(content = aTimelineItemVideoContent()).copy(
|
||||
reactionsState = reactionsState
|
||||
event = aTimelineItemEvent(
|
||||
content = aTimelineItemVideoContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
),
|
||||
anActionListState().copy(
|
||||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(content = aTimelineItemFileContent()).copy(
|
||||
reactionsState = reactionsState
|
||||
event = aTimelineItemEvent(
|
||||
content = aTimelineItemFileContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
),
|
||||
anActionListState().copy(
|
||||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(content = aTimelineItemAudioContent()).copy(
|
||||
reactionsState = reactionsState
|
||||
event = aTimelineItemEvent(
|
||||
content = aTimelineItemAudioContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
),
|
||||
anActionListState().copy(
|
||||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(content = aTimelineItemVoiceContent()).copy(
|
||||
reactionsState = reactionsState
|
||||
event = aTimelineItemEvent(
|
||||
content = aTimelineItemVoiceContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
),
|
||||
anActionListState().copy(
|
||||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(content = aTimelineItemLocationContent()).copy(
|
||||
reactionsState = reactionsState
|
||||
event = aTimelineItemEvent(
|
||||
content = aTimelineItemLocationContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
),
|
||||
anActionListState().copy(
|
||||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(content = aTimelineItemLocationContent()).copy(
|
||||
reactionsState = reactionsState
|
||||
event = aTimelineItemEvent(
|
||||
content = aTimelineItemLocationContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
displayEmojiReactions = false,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
),
|
||||
),
|
||||
anActionListState().copy(
|
||||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(content = aTimelineItemPollContent()).copy(
|
||||
reactionsState = reactionsState
|
||||
event = aTimelineItemEvent(
|
||||
content = aTimelineItemPollContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
displayEmojiReactions = false,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemPollActionList(),
|
||||
),
|
||||
),
|
||||
anActionListState().copy(
|
||||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent().copy(
|
||||
reactionsState = reactionsState,
|
||||
event = aTimelineItemEvent(
|
||||
timelineItemReactions = reactionsState,
|
||||
messageShield = MessageShield.UnknownDevice(isCritical = true)
|
||||
),
|
||||
displayEmojiReactions = true,
|
||||
@@ -135,7 +141,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
),
|
||||
anActionListState().copy(
|
||||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(),
|
||||
displayEmojiReactions = true,
|
||||
|
||||
@@ -146,7 +146,7 @@ fun ActionListView(
|
||||
onDismissRequest = ::onDismiss,
|
||||
modifier = modifier,
|
||||
) {
|
||||
SheetContent(
|
||||
ActionListViewContent(
|
||||
state = state,
|
||||
onActionClick = ::onItemActionClick,
|
||||
onEmojiReactionClick = ::onEmojiReactionClick,
|
||||
@@ -161,7 +161,7 @@ fun ActionListView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SheetContent(
|
||||
private fun ActionListViewContent(
|
||||
state: ActionListState,
|
||||
onActionClick: (TimelineItemAction) -> Unit,
|
||||
onEmojiReactionClick: (String) -> Unit,
|
||||
@@ -269,19 +269,19 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||
content = { ContentForBody(stringResource(CommonStrings.common_shared_location)) }
|
||||
}
|
||||
is TimelineItemImageContent -> {
|
||||
content = { ContentForBody(event.content.body) }
|
||||
content = { ContentForBody(event.content.bestDescription) }
|
||||
}
|
||||
is TimelineItemStickerContent -> {
|
||||
content = { ContentForBody(event.content.body) }
|
||||
content = { ContentForBody(event.content.bestDescription) }
|
||||
}
|
||||
is TimelineItemVideoContent -> {
|
||||
content = { ContentForBody(event.content.body) }
|
||||
content = { ContentForBody(event.content.bestDescription) }
|
||||
}
|
||||
is TimelineItemFileContent -> {
|
||||
content = { ContentForBody(event.content.body) }
|
||||
content = { ContentForBody(event.content.bestDescription) }
|
||||
}
|
||||
is TimelineItemAudioContent -> {
|
||||
content = { ContentForBody(event.content.body) }
|
||||
content = { ContentForBody(event.content.bestDescription) }
|
||||
}
|
||||
is TimelineItemVoiceContent -> {
|
||||
content = { ContentForBody(textContent) }
|
||||
@@ -442,10 +442,10 @@ private fun EmojiButton(
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SheetContentPreview(
|
||||
internal fun ActionListViewContentPreview(
|
||||
@PreviewParameter(ActionListStateProvider::class) state: ActionListState
|
||||
) = ElementPreview {
|
||||
SheetContent(
|
||||
ActionListViewContent(
|
||||
state = state,
|
||||
onActionClick = {},
|
||||
onEmojiReactionClick = {},
|
||||
|
||||
@@ -105,13 +105,13 @@ class IdentityChangeStatePresenter @Inject constructor(
|
||||
|
||||
private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember(
|
||||
userId = userId,
|
||||
disambiguatedDisplayName = disambiguatedDisplayName,
|
||||
displayNameOrDefault = displayNameOrDefault,
|
||||
avatarData = getAvatarData(AvatarSize.ComposerAlert),
|
||||
)
|
||||
|
||||
private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember(
|
||||
userId = userId,
|
||||
disambiguatedDisplayName = userId.value,
|
||||
displayNameOrDefault = userId.extractedDisplayName,
|
||||
avatarData = AvatarData(
|
||||
id = userId.value,
|
||||
name = null,
|
||||
|
||||
@@ -20,8 +20,16 @@ class IdentityChangeStateProvider : PreviewParameterProvider<IdentityChangeState
|
||||
anIdentityChangeState(),
|
||||
anIdentityChangeState(
|
||||
roomMemberIdentityStateChanges = listOf(
|
||||
RoomMemberIdentityStateChange(
|
||||
identityRoomMember = anIdentityRoomMember(disambiguatedDisplayName = "Alice"),
|
||||
aRoomMemberIdentityStateChange(
|
||||
identityRoomMember = anIdentityRoomMember(),
|
||||
identityState = IdentityState.PinViolation,
|
||||
),
|
||||
),
|
||||
),
|
||||
anIdentityChangeState(
|
||||
roomMemberIdentityStateChanges = listOf(
|
||||
aRoomMemberIdentityStateChange(
|
||||
identityRoomMember = anIdentityRoomMember(displayNameOrDefault = "Alice"),
|
||||
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(
|
||||
roomMemberIdentityStateChanges: List<RoomMemberIdentityStateChange> = emptyList(),
|
||||
) = IdentityChangeState(
|
||||
@@ -38,7 +54,7 @@ internal fun anIdentityChangeState(
|
||||
|
||||
internal fun anIdentityRoomMember(
|
||||
userId: UserId = UserId("@alice:example.com"),
|
||||
disambiguatedDisplayName: String = userId.value,
|
||||
displayNameOrDefault: String = userId.extractedDisplayName,
|
||||
avatarData: AvatarData = AvatarData(
|
||||
id = userId.value,
|
||||
name = null,
|
||||
@@ -47,6 +63,6 @@ internal fun anIdentityRoomMember(
|
||||
),
|
||||
) = IdentityRoomMember(
|
||||
userId = userId,
|
||||
disambiguatedDisplayName = disambiguatedDisplayName,
|
||||
displayNameOrDefault = displayNameOrDefault,
|
||||
avatarData = avatarData,
|
||||
)
|
||||
|
||||
@@ -40,13 +40,27 @@ fun IdentityChangeStateView(
|
||||
avatar = pinViolationIdentityChange.identityRoomMember.avatarData,
|
||||
content = buildAnnotatedString {
|
||||
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(
|
||||
id = CommonStrings.crypto_identity_change_pin_violation,
|
||||
pinViolationIdentityChange.identityRoomMember.disambiguatedDisplayName,
|
||||
id = CommonStrings.crypto_identity_change_pin_violation_new,
|
||||
displayName,
|
||||
userIdStr,
|
||||
learnMoreStr,
|
||||
)
|
||||
val learnMoreStartIndex = fullText.indexOf(learnMoreStr)
|
||||
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(
|
||||
style = SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
|
||||
@@ -12,6 +12,6 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
data class IdentityRoomMember(
|
||||
val userId: UserId,
|
||||
val disambiguatedDisplayName: String,
|
||||
val displayNameOrDefault: String,
|
||||
val avatarData: AvatarData,
|
||||
)
|
||||
|
||||
@@ -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.isDm
|
||||
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.reply.InReplyToDetails
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||
@@ -442,12 +443,11 @@ class MessageComposerPresenter @Inject constructor(
|
||||
intentionalMentions = message.intentionalMentions
|
||||
)
|
||||
is MessageComposerMode.Edit -> {
|
||||
val eventId = capturedMode.eventId
|
||||
val transactionId = capturedMode.transactionId
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
// 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 ->
|
||||
val eventId = capturedMode.eventOrTransactionId.eventId
|
||||
if (cause is TimelineException.EventNotFound && eventId != null) {
|
||||
// if the event is not found in the timeline, try to edit the message directly
|
||||
room.editMessage(eventId, message.markdown, message.html, message.intentionalMentions)
|
||||
@@ -581,8 +581,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||
when (val draftType = draft.draftType) {
|
||||
ComposerDraftType.NewMessage -> messageComposerContext.composerMode = MessageComposerMode.Normal
|
||||
is ComposerDraftType.Edit -> messageComposerContext.composerMode = MessageComposerMode.Edit(
|
||||
eventId = draftType.eventId,
|
||||
transactionId = null,
|
||||
eventOrTransactionId = draftType.eventId.toEventOrTransactionId(),
|
||||
content = htmlText ?: markdownText
|
||||
)
|
||||
is ComposerDraftType.Reply -> {
|
||||
@@ -611,7 +610,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||
val draftType = when (val mode = messageComposerContext.composerMode) {
|
||||
is MessageComposerMode.Normal -> ComposerDraftType.NewMessage
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -164,10 +164,10 @@ internal fun aTimelineItemEvent(
|
||||
groupPosition = groupPosition,
|
||||
localSendState = sendState,
|
||||
inReplyTo = inReplyTo,
|
||||
debugInfoProvider = { debugInfo },
|
||||
isThreaded = isThreaded,
|
||||
origin = null,
|
||||
messageShield = messageShield,
|
||||
timelineItemDebugInfoProvider = { debugInfo },
|
||||
messageShieldProvider = { messageShield },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
val messageShield = aCriticalShield()
|
||||
val items = listOf(
|
||||
(timelineItems.first() as TimelineItem.Event).copy(messageShield = messageShield)
|
||||
(timelineItems.first() as TimelineItem.Event).copy(
|
||||
messageShieldProvider = { messageShield },
|
||||
)
|
||||
) + timelineItems.drop(1)
|
||||
CompositionLocalProvider(
|
||||
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
|
||||
|
||||
@@ -29,7 +29,9 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
@@ -101,6 +103,7 @@ fun MessageEventBubble(
|
||||
val bubbleShape = bubbleShape()
|
||||
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
||||
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.graphicsLayer {
|
||||
@@ -112,7 +115,7 @@ fun MessageEventBubble(
|
||||
drawCircle(
|
||||
color = Color.Black,
|
||||
center = Offset(
|
||||
x = 0f,
|
||||
x = if (isRtl) size.width else 0f,
|
||||
y = yOffsetPx,
|
||||
),
|
||||
radius = radiusPx,
|
||||
@@ -129,7 +132,9 @@ fun MessageEventBubble(
|
||||
.testTag(TestTags.messageBubble)
|
||||
.widthIn(
|
||||
min = MIN_BUBBLE_WIDTH,
|
||||
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO).toInt().toDp()
|
||||
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO)
|
||||
.toInt()
|
||||
.toDp()
|
||||
)
|
||||
.clip(bubbleShape)
|
||||
.combinedClickable(
|
||||
|
||||
@@ -27,10 +27,10 @@ class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider<Timel
|
||||
localSendState = LocalEventSendState.Failed.Unknown("AN_ERROR"),
|
||||
content = aTimelineItemTextContent().copy(isEdited = true),
|
||||
),
|
||||
aTimelineItemEvent().copy(
|
||||
aTimelineItemEvent(
|
||||
messageShield = MessageShield.AuthenticityNotGuaranteed(isCritical = false),
|
||||
),
|
||||
aTimelineItemEvent().copy(
|
||||
aTimelineItemEvent(
|
||||
messageShield = MessageShield.UnknownDevice(isCritical = true),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -301,6 +301,8 @@ private fun TimelineItemEventRowContent(
|
||||
Modifier
|
||||
.constrainAs(sender) {
|
||||
top.linkTo(parent.top)
|
||||
// Required for correct RTL layout
|
||||
start.linkTo(parent.start)
|
||||
}
|
||||
.padding(horizontal = 16.dp)
|
||||
.zIndex(1f)
|
||||
@@ -629,7 +631,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = isMine,
|
||||
content = aTimelineItemImageContent().copy(
|
||||
content = aTimelineItemImageContent(
|
||||
aspectRatio = 2.5f
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
|
||||
@@ -38,7 +38,7 @@ internal fun TimelineItemEventRowForDirectRoomPreview() = ElementPreview {
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = it,
|
||||
content = aTimelineItemImageContent().copy(
|
||||
content = aTimelineItemImageContent(
|
||||
aspectRatio = 5f
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
|
||||
@@ -45,7 +45,7 @@ internal fun TimelineItemEventRowShieldPreview() = ElementPreview {
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = aTimelineItemImageContent().copy(
|
||||
content = aTimelineItemImageContent(
|
||||
aspectRatio = 2.5f
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
@@ -54,7 +54,7 @@ internal fun TimelineItemEventRowShieldPreview() = ElementPreview {
|
||||
)
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
content = aTimelineItemImageContent().copy(
|
||||
content = aTimelineItemImageContent(
|
||||
aspectRatio = 2.5f
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
|
||||
@@ -49,7 +49,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = it,
|
||||
timelineItemReactions = aTimelineItemReactions(count = 0),
|
||||
content = aTimelineItemImageContent().copy(
|
||||
content = aTimelineItemImageContent(
|
||||
aspectRatio = 2.5f
|
||||
),
|
||||
inReplyTo = inReplyToDetails,
|
||||
|
||||
@@ -16,13 +16,13 @@ import androidx.compose.ui.Modifier
|
||||
import io.element.android.emojibasebindings.Emoji
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
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)
|
||||
@Composable
|
||||
fun CustomReactionBottomSheet(
|
||||
state: CustomReactionState,
|
||||
onSelectEmoji: (UniqueId, Emoji) -> Unit,
|
||||
onSelectEmoji: (EventOrTransactionId, Emoji) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
@@ -37,7 +37,7 @@ fun CustomReactionBottomSheet(
|
||||
if (target?.event == null) return
|
||||
sheetState.hide(coroutineScope) {
|
||||
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
onSelectEmoji(target.event.id, emoji)
|
||||
onSelectEmoji(target.event.eventOrTransactionId, emoji)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ fun TimelineItemAudioView(
|
||||
Spacer(Modifier.width(spacing))
|
||||
Column {
|
||||
Text(
|
||||
text = content.body,
|
||||
text = content.bestDescription,
|
||||
color = ElementTheme.materialColors.primary,
|
||||
maxLines = 2,
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
|
||||
@@ -64,7 +64,7 @@ fun TimelineItemFileView(
|
||||
Spacer(Modifier.width(spacing))
|
||||
Column {
|
||||
Text(
|
||||
text = content.body,
|
||||
text = content.bestDescription,
|
||||
color = ElementTheme.materialColors.primary,
|
||||
maxLines = 2,
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
|
||||
@@ -91,7 +91,7 @@ fun TimelineItemImageView(
|
||||
model = MediaRequestData(
|
||||
source = content.preferredMediaSource,
|
||||
kind = MediaRequestData.Kind.File(
|
||||
body = content.filename ?: content.body,
|
||||
fileName = content.filename,
|
||||
mimeType = content.mimeType,
|
||||
),
|
||||
),
|
||||
@@ -108,7 +108,9 @@ fun TimelineItemImageView(
|
||||
val caption = if (LocalInspectionMode.current) {
|
||||
SpannedString(content.caption)
|
||||
} 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(
|
||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||
@@ -158,9 +160,9 @@ internal fun TimelineImageWithCaptionRowPreview() = ElementPreview {
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = isMine,
|
||||
content = aTimelineItemImageContent().copy(
|
||||
content = aTimelineItemImageContent(
|
||||
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,
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
@@ -170,9 +172,9 @@ internal fun TimelineImageWithCaptionRowPreview() = ElementPreview {
|
||||
ATimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = false,
|
||||
content = aTimelineItemImageContent().copy(
|
||||
content = aTimelineItemImageContent(
|
||||
filename = "image.jpg",
|
||||
body = "Image with null aspectRatio",
|
||||
caption = "Image with null aspectRatio",
|
||||
aspectRatio = null,
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
|
||||
@@ -43,7 +43,7 @@ fun TimelineItemStickerView(
|
||||
onShowClick: () -> Unit,
|
||||
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(
|
||||
modifier = modifier.semantics { contentDescription = description },
|
||||
) {
|
||||
@@ -65,7 +65,7 @@ fun TimelineItemStickerView(
|
||||
model = MediaRequestData(
|
||||
source = content.preferredMediaSource,
|
||||
kind = MediaRequestData.Kind.File(
|
||||
body = content.body,
|
||||
fileName = content.filename,
|
||||
mimeType = content.mimeType,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -76,8 +76,8 @@ fun TimelineItemVideoView(
|
||||
) {
|
||||
val containerModifier = if (content.showCaption) {
|
||||
Modifier
|
||||
.padding(top = 6.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.padding(top = 6.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
@@ -93,12 +93,12 @@ fun TimelineItemVideoView(
|
||||
var isLoaded by remember { mutableStateOf(false) }
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
||||
.fillMaxWidth()
|
||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
||||
model = MediaRequestData(
|
||||
source = content.thumbnailSource,
|
||||
kind = MediaRequestData.Kind.File(
|
||||
body = content.filename ?: content.body,
|
||||
fileName = content.filename,
|
||||
mimeType = content.mimeType
|
||||
)
|
||||
),
|
||||
@@ -126,7 +126,9 @@ fun TimelineItemVideoView(
|
||||
val caption = if (LocalInspectionMode.current) {
|
||||
SpannedString(content.caption)
|
||||
} 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(
|
||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||
@@ -178,7 +180,7 @@ internal fun TimelineVideoWithCaptionRowPreview() = ElementPreview {
|
||||
isMine = isMine,
|
||||
content = aTimelineItemVideoContent().copy(
|
||||
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,
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
@@ -190,7 +192,7 @@ internal fun TimelineVideoWithCaptionRowPreview() = ElementPreview {
|
||||
isMine = false,
|
||||
content = aTimelineItemVideoContent().copy(
|
||||
filename = "video.mp4",
|
||||
body = "Video with null aspect ratio",
|
||||
caption = "Video with null aspect ratio",
|
||||
aspectRatio = null,
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
|
||||
@@ -89,14 +89,14 @@ fun ReactionSummaryView(
|
||||
sheetState = sheetState,
|
||||
modifier = modifier
|
||||
) {
|
||||
SheetContent(summary = state.target)
|
||||
ReactionSummaryViewContent(summary = state.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun SheetContent(
|
||||
private fun ReactionSummaryViewContent(
|
||||
summary: ReactionSummaryState.Summary,
|
||||
) {
|
||||
val animationScope = rememberCoroutineScope()
|
||||
@@ -274,8 +274,8 @@ private fun SenderRow(
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SheetContentPreview(
|
||||
internal fun ReactionSummaryViewContentPreview(
|
||||
@PreviewParameter(ReactionSummaryStateProvider::class) state: ReactionSummaryState
|
||||
) = ElementPreview {
|
||||
SheetContent(summary = state.target as ReactionSummaryState.Summary)
|
||||
ReactionSummaryViewContent(summary = state.target as ReactionSummaryState.Summary)
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
is ImageMessageType -> {
|
||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||
TimelineItemImageContent(
|
||||
body = messageType.body.trimEnd(),
|
||||
formatted = messageType.formatted,
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
mediaSource = messageType.source,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -95,13 +95,15 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
height = messageType.info?.height?.toInt(),
|
||||
aspectRatio = aspectRatio,
|
||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||
fileExtension = messageType.filename?.let { fileExtensionExtractor.extractFromName(it) }.orEmpty()
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
|
||||
)
|
||||
}
|
||||
is StickerMessageType -> {
|
||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||
TimelineItemStickerContent(
|
||||
body = messageType.body.trimEnd(),
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
mediaSource = messageType.source,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -110,7 +112,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
height = messageType.info?.height?.toInt(),
|
||||
aspectRatio = aspectRatio,
|
||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
|
||||
)
|
||||
}
|
||||
is LocationMessageType -> {
|
||||
@@ -136,9 +138,9 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
is VideoMessageType -> {
|
||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||
TimelineItemVideoContent(
|
||||
body = messageType.body.trimEnd(),
|
||||
formatted = messageType.formatted,
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
videoSource = messageType.source,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -148,17 +150,19 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
blurHash = messageType.info?.blurhash,
|
||||
aspectRatio = aspectRatio,
|
||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||
fileExtension = messageType.filename?.let { fileExtensionExtractor.extractFromName(it) }.orEmpty(),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.filename),
|
||||
)
|
||||
}
|
||||
is AudioMessageType -> {
|
||||
TimelineItemAudioContent(
|
||||
body = messageType.body.trimEnd(),
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
mediaSource = messageType.source,
|
||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.filename),
|
||||
)
|
||||
}
|
||||
is VoiceMessageType -> {
|
||||
@@ -166,7 +170,9 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
true -> {
|
||||
TimelineItemVoiceContent(
|
||||
eventId = eventId,
|
||||
body = messageType.body.trimEnd(),
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
mediaSource = messageType.source,
|
||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -175,20 +181,24 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
}
|
||||
false -> {
|
||||
TimelineItemAudioContent(
|
||||
body = messageType.body.trimEnd(),
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
mediaSource = messageType.source,
|
||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.filename),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is FileMessageType -> {
|
||||
val fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
|
||||
val fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
|
||||
TimelineItemFileContent(
|
||||
body = messageType.body.trimEnd(),
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
fileSource = messageType.source,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
|
||||
|
||||
@@ -33,7 +33,9 @@ class TimelineItemContentStickerFactory @Inject constructor(
|
||||
val aspectRatio = aspectRatioOf(content.info.width, content.info.height)
|
||||
|
||||
return TimelineItemStickerContent(
|
||||
body = content.body,
|
||||
filename = content.filename,
|
||||
caption = content.body,
|
||||
formattedCaption = null,
|
||||
mediaSource = content.source,
|
||||
thumbnailSource = content.info.thumbnailSource,
|
||||
mimeType = content.info.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -42,7 +44,7 @@ class TimelineItemContentStickerFactory @Inject constructor(
|
||||
height = content.info.height?.toInt(),
|
||||
aspectRatio = aspectRatio,
|
||||
formattedFileSize = fileSizeFormatter.format(content.info.size ?: 0),
|
||||
fileExtension = fileExtensionExtractor.extractFromName(content.body)
|
||||
fileExtension = fileExtensionExtractor.extractFromName(content.filename)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,9 +85,9 @@ class TimelineItemEventFactory @AssistedInject constructor(
|
||||
localSendState = currentTimelineItem.event.localSendState,
|
||||
inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser),
|
||||
isThreaded = currentTimelineItem.event.isThreaded(),
|
||||
debugInfoProvider = currentTimelineItem.event.debugInfoProvider,
|
||||
origin = currentTimelineItem.event.origin,
|
||||
messageShield = currentTimelineItem.event.messageShieldProvider.getShield(strict = false)
|
||||
timelineItemDebugInfoProvider = currentTimelineItem.event.timelineItemDebugInfoProvider,
|
||||
messageShieldProvider = currentTimelineItem.event.messageShieldProvider,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.UniqueId
|
||||
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.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.TimelineItemDebugInfoProvider
|
||||
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.ui.messages.reply.InReplyToDetails
|
||||
@@ -74,9 +77,9 @@ sealed interface TimelineItem {
|
||||
val localSendState: LocalEventSendState?,
|
||||
val inReplyTo: InReplyToDetails?,
|
||||
val isThreaded: Boolean,
|
||||
val debugInfoProvider: EventDebugInfoProvider,
|
||||
val origin: TimelineItemEventOrigin?,
|
||||
val messageShield: MessageShield?,
|
||||
val timelineItemDebugInfoProvider: TimelineItemDebugInfoProvider,
|
||||
val messageShieldProvider: MessageShieldProvider,
|
||||
) : TimelineItem {
|
||||
val showSenderInformation = groupPosition.isNew() && !isMine
|
||||
|
||||
@@ -90,7 +93,14 @@ sealed interface TimelineItem {
|
||||
|
||||
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
|
||||
|
||||
@@ -8,17 +8,20 @@
|
||||
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.timeline.item.event.FormattedBody
|
||||
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class TimelineItemAudioContent(
|
||||
val body: String,
|
||||
override val filename: String,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: FormattedBody?,
|
||||
val duration: Duration,
|
||||
val mediaSource: MediaSource,
|
||||
val mimeType: String,
|
||||
val formattedFileSize: String,
|
||||
val fileExtension: String,
|
||||
) : TimelineItemEventContent {
|
||||
) : TimelineItemEventContentWithAttachment {
|
||||
val fileExtensionAndSize =
|
||||
formatFileExtensionAndSize(
|
||||
fileExtension,
|
||||
|
||||
@@ -22,8 +22,10 @@ open class TimelineItemAudioContentProvider : PreviewParameterProvider<TimelineI
|
||||
}
|
||||
|
||||
fun aTimelineItemAudioContent(fileName: String = "A sound.mp3") = TimelineItemAudioContent(
|
||||
body = fileName,
|
||||
mimeType = MimeTypes.Pdf,
|
||||
filename = fileName,
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
mimeType = MimeTypes.Mp3,
|
||||
formattedFileSize = "100kB",
|
||||
fileExtension = "mp3",
|
||||
duration = 100.milliseconds,
|
||||
|
||||
@@ -8,12 +8,23 @@
|
||||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
|
||||
@Immutable
|
||||
sealed interface TimelineItemEventContent {
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -8,16 +8,19 @@
|
||||
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.timeline.item.event.FormattedBody
|
||||
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
|
||||
|
||||
data class TimelineItemFileContent(
|
||||
val body: String,
|
||||
override val filename: String,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: FormattedBody?,
|
||||
val fileSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val formattedFileSize: String,
|
||||
val fileExtension: String,
|
||||
val mimeType: String,
|
||||
) : TimelineItemEventContent {
|
||||
) : TimelineItemEventContentWithAttachment {
|
||||
override val type: String = "TimelineItemFileContent"
|
||||
|
||||
val fileExtensionAndSize = formatFileExtensionAndSize(fileExtension, formattedFileSize)
|
||||
|
||||
@@ -20,8 +20,12 @@ open class TimelineItemFileContentProvider : PreviewParameterProvider<TimelineIt
|
||||
)
|
||||
}
|
||||
|
||||
fun aTimelineItemFileContent(fileName: String = "A file.pdf") = TimelineItemFileContent(
|
||||
body = fileName,
|
||||
fun aTimelineItemFileContent(
|
||||
fileName: String = "A file.pdf",
|
||||
) = TimelineItemFileContent(
|
||||
filename = fileName,
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
thumbnailSource = null,
|
||||
fileSource = MediaSource(url = ""),
|
||||
mimeType = MimeTypes.Pdf,
|
||||
|
||||
@@ -12,9 +12,9 @@ import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
|
||||
data class TimelineItemImageContent(
|
||||
val body: String,
|
||||
val formatted: FormattedBody?,
|
||||
val filename: String?,
|
||||
override val filename: String,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: FormattedBody?,
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val formattedFileSize: String,
|
||||
@@ -24,11 +24,10 @@ data class TimelineItemImageContent(
|
||||
val width: Int?,
|
||||
val height: Int?,
|
||||
val aspectRatio: Float?
|
||||
) : TimelineItemEventContent {
|
||||
) : TimelineItemEventContentWithAttachment {
|
||||
override val type: String = "TimelineItemImageContent"
|
||||
|
||||
val showCaption = filename != null && filename != body
|
||||
val caption = if (showCaption) body else ""
|
||||
val showCaption = caption != null
|
||||
|
||||
val preferredMediaSource = if (mimeType == MimeTypes.Gif) {
|
||||
mediaSource
|
||||
|
||||
@@ -23,12 +23,14 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineI
|
||||
}
|
||||
|
||||
fun aTimelineItemImageContent(
|
||||
aspectRatio: Float = 0.5f,
|
||||
aspectRatio: Float? = 0.5f,
|
||||
blurhash: String? = A_BLUR_HASH,
|
||||
filename: String = "A picture.jpg",
|
||||
caption: String? = null,
|
||||
) = TimelineItemImageContent(
|
||||
body = "a body",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
filename = filename,
|
||||
caption = caption,
|
||||
formattedCaption = null,
|
||||
mediaSource = MediaSource(""),
|
||||
thumbnailSource = null,
|
||||
mimeType = MimeTypes.IMAGE_JPEG,
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
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.timeline.item.event.FormattedBody
|
||||
|
||||
data class TimelineItemStickerContent(
|
||||
val body: String,
|
||||
override val filename: String,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: FormattedBody?,
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val formattedFileSize: String,
|
||||
@@ -20,7 +23,7 @@ data class TimelineItemStickerContent(
|
||||
val width: Int?,
|
||||
val height: Int?,
|
||||
val aspectRatio: Float?
|
||||
) : TimelineItemEventContent {
|
||||
) : TimelineItemEventContentWithAttachment {
|
||||
override val type: String = "TimelineItemStickerContent"
|
||||
|
||||
/* Stickers are supposed to be small images so
|
||||
|
||||
@@ -26,7 +26,9 @@ fun aTimelineItemStickerContent(
|
||||
aspectRatio: Float = 0.5f,
|
||||
blurhash: String? = A_BLUR_HASH,
|
||||
) = TimelineItemStickerContent(
|
||||
body = "a body",
|
||||
filename = "a sticker.gif",
|
||||
caption = "a body",
|
||||
formattedCaption = null,
|
||||
mediaSource = MediaSource(""),
|
||||
thumbnailSource = null,
|
||||
mimeType = MimeTypes.IMAGE_JPEG,
|
||||
|
||||
@@ -12,9 +12,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class TimelineItemVideoContent(
|
||||
val body: String,
|
||||
val formatted: FormattedBody?,
|
||||
val filename: String?,
|
||||
override val filename: String,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: FormattedBody?,
|
||||
val duration: Duration,
|
||||
val videoSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
@@ -25,9 +25,8 @@ data class TimelineItemVideoContent(
|
||||
val mimeType: String,
|
||||
val formattedFileSize: String,
|
||||
val fileExtension: String,
|
||||
) : TimelineItemEventContent {
|
||||
) : TimelineItemEventContentWithAttachment {
|
||||
override val type: String = "TimelineItemImageContent"
|
||||
|
||||
val showCaption = filename != null && filename != body
|
||||
val caption = if (showCaption) body else ""
|
||||
val showCaption = caption != null
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ fun aTimelineItemVideoContent(
|
||||
aspectRatio: Float = 0.5f,
|
||||
blurhash: String? = A_BLUR_HASH,
|
||||
) = TimelineItemVideoContent(
|
||||
body = "Video.mp4",
|
||||
formatted = null,
|
||||
filename = null,
|
||||
filename = "Video.mp4",
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
thumbnailSource = null,
|
||||
blurHash = blurhash,
|
||||
aspectRatio = aspectRatio,
|
||||
|
||||
@@ -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.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class TimelineItemVoiceContent(
|
||||
val eventId: EventId?,
|
||||
val body: String,
|
||||
override val filename: String,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: FormattedBody?,
|
||||
val duration: Duration,
|
||||
val mediaSource: MediaSource,
|
||||
val mimeType: String,
|
||||
val waveform: ImmutableList<Float>,
|
||||
) : TimelineItemEventContent {
|
||||
) : TimelineItemEventContentWithAttachment {
|
||||
override val type: String = "TimelineItemAudioContent"
|
||||
}
|
||||
|
||||
@@ -35,17 +35,21 @@ open class TimelineItemVoiceContentProvider : PreviewParameterProvider<TimelineI
|
||||
}
|
||||
|
||||
fun aTimelineItemVoiceContent(
|
||||
eventId: String? = "\$anEventId",
|
||||
body: String = "body doesn't really matter for a voice message",
|
||||
eventId: EventId? = EventId("\$anEventId"),
|
||||
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,
|
||||
contentUri: String = "mxc://matrix.org/1234567890abcdefg",
|
||||
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),
|
||||
) = TimelineItemVoiceContent(
|
||||
eventId = eventId?.let { EventId(it) },
|
||||
body = body,
|
||||
eventId = eventId,
|
||||
filename = filename,
|
||||
caption = caption,
|
||||
formattedCaption = null,
|
||||
duration = duration,
|
||||
mediaSource = MediaSource(contentUri),
|
||||
mediaSource = mediaSource,
|
||||
mimeType = mimeType,
|
||||
waveform = waveform.toPersistentList(),
|
||||
)
|
||||
|
||||
@@ -35,12 +35,12 @@ interface VoiceMessageMediaRepo {
|
||||
*
|
||||
* @param mediaSource the media source 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(
|
||||
mediaSource: MediaSource,
|
||||
mimeType: String?,
|
||||
body: String?,
|
||||
filename: String?,
|
||||
): VoiceMessageMediaRepo
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
|
||||
private val matrixMediaLoader: MatrixMediaLoader,
|
||||
@Assisted private val mediaSource: MediaSource,
|
||||
@Assisted("mimeType") private val mimeType: String?,
|
||||
@Assisted("body") private val body: String?,
|
||||
@Assisted("filename") private val filename: String?,
|
||||
) : VoiceMessageMediaRepo {
|
||||
@ContributesBinding(RoomScope::class)
|
||||
@AssistedFactory
|
||||
@@ -69,7 +69,7 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
|
||||
override fun create(
|
||||
mediaSource: MediaSource,
|
||||
@Assisted("mimeType") mimeType: String?,
|
||||
@Assisted("body") body: String?,
|
||||
@Assisted("filename") filename: String?,
|
||||
): DefaultVoiceMessageMediaRepo
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
|
||||
else -> matrixMediaLoader.downloadMediaFile(
|
||||
source = mediaSource,
|
||||
mimeType = mimeType,
|
||||
body = body,
|
||||
filename = filename,
|
||||
).mapCatching {
|
||||
it.use { mediaFile ->
|
||||
val dest = cachedFile.apply { parentFile?.mkdirs() }
|
||||
|
||||
@@ -37,13 +37,13 @@ interface VoiceMessagePlayer {
|
||||
* @param eventId The eventId of the voice message event.
|
||||
* @param mediaSource The media source 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(
|
||||
eventId: EventId?,
|
||||
mediaSource: MediaSource,
|
||||
mimeType: String?,
|
||||
body: String?,
|
||||
filename: String?,
|
||||
): VoiceMessagePlayer
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ class DefaultVoiceMessagePlayer(
|
||||
private val eventId: EventId?,
|
||||
mediaSource: MediaSource,
|
||||
mimeType: String?,
|
||||
body: String?,
|
||||
filename: String?,
|
||||
) : VoiceMessagePlayer {
|
||||
@ContributesBinding(RoomScope::class) // Scoped types can't use @AssistedInject.
|
||||
class Factory @Inject constructor(
|
||||
@@ -124,21 +124,21 @@ class DefaultVoiceMessagePlayer(
|
||||
eventId: EventId?,
|
||||
mediaSource: MediaSource,
|
||||
mimeType: String?,
|
||||
body: String?,
|
||||
filename: String?,
|
||||
): DefaultVoiceMessagePlayer = DefaultVoiceMessagePlayer(
|
||||
mediaPlayer = mediaPlayer,
|
||||
voiceMessageMediaRepoFactory = voiceMessageMediaRepoFactory,
|
||||
eventId = eventId,
|
||||
mediaSource = mediaSource,
|
||||
mimeType = mimeType,
|
||||
body = body,
|
||||
filename = filename,
|
||||
)
|
||||
}
|
||||
|
||||
private val repo = voiceMessageMediaRepoFactory.create(
|
||||
mediaSource = mediaSource,
|
||||
mimeType = mimeType,
|
||||
body = body
|
||||
filename = filename,
|
||||
)
|
||||
|
||||
private var internalState = MutableStateFlow(
|
||||
|
||||
@@ -59,7 +59,7 @@ class VoiceMessagePresenter @AssistedInject constructor(
|
||||
eventId = content.eventId,
|
||||
mediaSource = content.mediaSource,
|
||||
mimeType = content.mimeType,
|
||||
body = content.body,
|
||||
filename = content.filename,
|
||||
)
|
||||
|
||||
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user