Merge branch 'develop' into renovate/kotlin
This commit is contained in:
commit
431467af5b
363 changed files with 3030 additions and 2267 deletions
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
|
|
@ -79,7 +79,7 @@ jobs:
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: elementx-apk-maestro
|
name: elementx-apk-maestro
|
||||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.9.1
|
- uses: mobile-dev-inc/action-maestro-cloud@v1.9.2
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
with:
|
with:
|
||||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="banner_migrate_to_native_sliding_sync_action">"Αποσύνδεση &amp; Αναβάθμιση"</string>
|
<string name="banner_migrate_to_native_sliding_sync_action">"Αποσύνδεση & Αναβάθμιση"</string>
|
||||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή."</string>
|
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="banner_migrate_to_native_sliding_sync_action">"Выйти и обновить"</string>
|
<string name="banner_migrate_to_native_sliding_sync_action">"Выйти и обновить"</string>
|
||||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ваш homeserver больше не поддерживает старый протокол. Пожалуйста, выйдите из системы и войдите снова, чтобы продолжить использование приложения."</string>
|
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ваш домашний сервер больше не поддерживает старый протокол. Пожалуйста, выйдите и войдите в свою учётную запись снова, чтобы продолжить использование приложения."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ Element X brings you both sovereign & seamless collaboration built on Matrix.
|
||||||
|
|
||||||
The collaboration capabilities include chat & video calls with the modern set of features such as:
|
The collaboration capabilities include chat & video calls with the modern set of features such as:
|
||||||
• public & private channels
|
• public & private channels
|
||||||
• room moderation & access conUpdatetrol
|
• room moderation & access control
|
||||||
• replies, reactions, polls, read receipts, pinned messages, etc.
|
• replies, reactions, polls, read receipts, pinned messages, etc.
|
||||||
• simultaneous chat & calls (picture in picture)
|
• simultaneous chat & calls (picture in picture)
|
||||||
• decentralized & federated communication across organizations
|
• decentralized & federated communication across organizations
|
||||||
|
|
@ -33,3 +33,7 @@ Enjoy your right to private conversations - free from data mining, ads and all t
|
||||||
|
|
||||||
<b>Chat across multiple devices</b>
|
<b>Chat across multiple devices</b>
|
||||||
Stay in touch wherever you are with fully synchronized message history across all your devices, even those running Element legacy app, and on the web at https://app.element.io
|
Stay in touch wherever you are with fully synchronized message history across all your devices, even those running Element legacy app, and on the web at https://app.element.io
|
||||||
|
|
||||||
|
The application requires the android.permission.REQUEST_INSTALL_PACKAGES permission to enable the installation of applications received as attachments, ensuring seamless and convenient access to new software within the app.
|
||||||
|
|
||||||
|
The application requires the USE_FULL_SCREEN_INTENT permission to ensure our users can effectively receive call notifications even when their devices are locked.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_settings_help_us_improve">"Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы."</string>
|
<string name="screen_analytics_settings_help_us_improve">"Предоставьте разработчикам анонимные данные об использовании, чтобы помочь им выявлять проблемы эффективнее."</string>
|
||||||
<string name="screen_analytics_settings_read_terms">"Вы можете ознакомиться со всеми нашими условиями %1$s."</string>
|
<string name="screen_analytics_settings_read_terms">"Вы можете ознакомиться со всеми нашими условиями %1$s."</string>
|
||||||
<string name="screen_analytics_settings_read_terms_content_link">"здесь"</string>
|
<string name="screen_analytics_settings_read_terms_content_link">"здесь"</string>
|
||||||
<string name="screen_analytics_settings_share_data">"Делитесь данными аналитики"</string>
|
<string name="screen_analytics_settings_share_data">"Отправлять аналитические данные"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<string name="screen_analytics_prompt_help_us_improve">"Partilhe dados de utilização anónimos para nos ajudar a identificar problemas."</string>
|
<string name="screen_analytics_prompt_help_us_improve">"Partilhe dados de utilização anónimos para nos ajudar a identificar problemas."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms">"Podes ler todos os nossos termos %1$s."</string>
|
<string name="screen_analytics_prompt_read_terms">"Podes ler todos os nossos termos %1$s."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms_content_link">"aqui"</string>
|
<string name="screen_analytics_prompt_read_terms_content_link">"aqui"</string>
|
||||||
<string name="screen_analytics_prompt_settings">"Podes desligar qualquer momento"</string>
|
<string name="screen_analytics_prompt_settings">"Pode desactivar a qualquer momento"</string>
|
||||||
<string name="screen_analytics_prompt_third_party_sharing">"Não partilharemos os teus dados com terceiros"</string>
|
<string name="screen_analytics_prompt_third_party_sharing">"Não partilharemos os teus dados com terceiros"</string>
|
||||||
<string name="screen_analytics_prompt_title">"Ajude a melhorar a %1$s"</string>
|
<string name="screen_analytics_prompt_title">"Ajude a melhorar a %1$s"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_prompt_data_usage">"Мы не будем записывать или профилировать какие-либо персональные данные"</string>
|
<string name="screen_analytics_prompt_data_usage">"Мы не будем записывать или профилировать какие-либо персональные данные"</string>
|
||||||
<string name="screen_analytics_prompt_help_us_improve">"Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы."</string>
|
<string name="screen_analytics_prompt_help_us_improve">"Предоставьте разработчикам анонимные данные об использовании, чтобы помочь им выявлять проблемы эффективнее."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms">"Вы можете ознакомиться со всеми нашими условиями %1$s."</string>
|
<string name="screen_analytics_prompt_read_terms">"Вы можете ознакомиться со всеми нашими условиями %1$s."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms_content_link">"здесь"</string>
|
<string name="screen_analytics_prompt_read_terms_content_link">"здесь"</string>
|
||||||
<string name="screen_analytics_prompt_settings">"Вы можете отключить эту функцию в любое время"</string>
|
<string name="screen_analytics_prompt_settings">"Вы можете отключить эту функцию в любое время"</string>
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@
|
||||||
|
|
||||||
<!-- Permissions for call foreground services -->
|
<!-- Permissions for call foreground services -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
|
||||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
android:name=".services.CallForegroundService"
|
android:name=".services.CallForegroundService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="phoneCall" />
|
android:foregroundServiceType="microphone" />
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".receivers.DeclineCallBroadcastReceiver"
|
android:name=".receivers.DeclineCallBroadcastReceiver"
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@
|
||||||
|
|
||||||
package io.element.android.features.call.impl.services
|
package io.element.android.features.call.impl.services
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
|
@ -33,8 +35,12 @@ import timber.log.Timber
|
||||||
class CallForegroundService : Service() {
|
class CallForegroundService : Service() {
|
||||||
companion object {
|
companion object {
|
||||||
fun start(context: Context) {
|
fun start(context: Context) {
|
||||||
val intent = Intent(context, CallForegroundService::class.java)
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
|
||||||
ContextCompat.startForegroundService(context, intent)
|
val intent = Intent(context, CallForegroundService::class.java)
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
} else {
|
||||||
|
Timber.w("Microphone permission is not granted, cannot start the call foreground service")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
|
|
@ -67,8 +73,8 @@ class CallForegroundService : Service() {
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.build()
|
.build()
|
||||||
val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.ONGOING_CALL)
|
val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.ONGOING_CALL)
|
||||||
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,7 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
urlState = urlState.value,
|
urlState = urlState.value,
|
||||||
webViewError = webViewError,
|
webViewError = webViewError,
|
||||||
userAgent = userAgent,
|
userAgent = userAgent,
|
||||||
|
isCallActive = isJoinedCall,
|
||||||
isInWidgetMode = isInWidgetMode,
|
isInWidgetMode = isInWidgetMode,
|
||||||
eventSink = { handleEvents(it) },
|
eventSink = { handleEvents(it) },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ data class CallScreenState(
|
||||||
val urlState: AsyncData<String>,
|
val urlState: AsyncData<String>,
|
||||||
val webViewError: String?,
|
val webViewError: String?,
|
||||||
val userAgent: String,
|
val userAgent: String,
|
||||||
|
val isCallActive: Boolean,
|
||||||
val isInWidgetMode: Boolean,
|
val isInWidgetMode: Boolean,
|
||||||
val eventSink: (CallScreenEvents) -> Unit,
|
val eventSink: (CallScreenEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ internal fun aCallScreenState(
|
||||||
urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
|
urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
|
||||||
webViewError: String? = null,
|
webViewError: String? = null,
|
||||||
userAgent: String = "",
|
userAgent: String = "",
|
||||||
|
isCallActive: Boolean = true,
|
||||||
isInWidgetMode: Boolean = false,
|
isInWidgetMode: Boolean = false,
|
||||||
eventSink: (CallScreenEvents) -> Unit = {},
|
eventSink: (CallScreenEvents) -> Unit = {},
|
||||||
): CallScreenState {
|
): CallScreenState {
|
||||||
|
|
@ -31,6 +32,7 @@ internal fun aCallScreenState(
|
||||||
urlState = urlState,
|
urlState = urlState,
|
||||||
webViewError = webViewError,
|
webViewError = webViewError,
|
||||||
userAgent = userAgent,
|
userAgent = userAgent,
|
||||||
|
isCallActive = isCallActive,
|
||||||
isInWidgetMode = isInWidgetMode,
|
isInWidgetMode = isInWidgetMode,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
|
@ -95,7 +96,6 @@ class ElementCallActivity :
|
||||||
pictureInPicturePresenter.setPipView(this)
|
pictureInPicturePresenter.setPipView(this)
|
||||||
|
|
||||||
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
||||||
requestAudioFocus()
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val pipState = pictureInPicturePresenter.present()
|
val pipState = pictureInPicturePresenter.present()
|
||||||
|
|
@ -103,6 +103,12 @@ class ElementCallActivity :
|
||||||
ElementThemeApp(appPreferencesStore) {
|
ElementThemeApp(appPreferencesStore) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
eventSink = state.eventSink
|
eventSink = state.eventSink
|
||||||
|
LaunchedEffect(state.isCallActive, state.isInWidgetMode) {
|
||||||
|
// Note when not in WidgetMode, isCallActive will never be true, so consider the call is active
|
||||||
|
if (state.isCallActive || !state.isInWidgetMode) {
|
||||||
|
setCallIsActive()
|
||||||
|
}
|
||||||
|
}
|
||||||
CallScreenView(
|
CallScreenView(
|
||||||
state = state,
|
state = state,
|
||||||
pipState = pipState,
|
pipState = pipState,
|
||||||
|
|
@ -115,6 +121,11 @@ class ElementCallActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setCallIsActive() {
|
||||||
|
requestAudioFocus()
|
||||||
|
CallForegroundService.start(this)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ListenToAndroidEvents(pipState: PictureInPictureState) {
|
private fun ListenToAndroidEvents(pipState: PictureInPictureState) {
|
||||||
val pipEventSink by rememberUpdatedState(pipState.eventSink)
|
val pipEventSink by rememberUpdatedState(pipState.eventSink)
|
||||||
|
|
@ -156,18 +167,6 @@ class ElementCallActivity :
|
||||||
setCallType(intent)
|
setCallType(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
CallForegroundService.stop(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
if (!isFinishing && !isChangingConfigurations) {
|
|
||||||
CallForegroundService.start(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
releaseAudioFocus()
|
releaseAudioFocus()
|
||||||
|
|
@ -231,10 +230,10 @@ class ElementCallActivity :
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private fun requestAudioFocus() {
|
private fun requestAudioFocus() {
|
||||||
val audioAttributes = AudioAttributes.Builder()
|
|
||||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
|
||||||
.build()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val audioAttributes = AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||||
|
.build()
|
||||||
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||||
.setAudioAttributes(audioAttributes)
|
.setAudioAttributes(audioAttributes)
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -247,7 +246,6 @@ class ElementCallActivity :
|
||||||
AudioManager.STREAM_VOICE_CALL,
|
AudioManager.STREAM_VOICE_CALL,
|
||||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,
|
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,
|
||||||
)
|
)
|
||||||
|
|
||||||
audioFocusChangeListener = listener
|
audioFocusChangeListener = listener
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ class CallScreenPresenterTest {
|
||||||
assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
|
assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
|
||||||
assertThat(initialState.webViewError).isNull()
|
assertThat(initialState.webViewError).isNull()
|
||||||
assertThat(initialState.isInWidgetMode).isFalse()
|
assertThat(initialState.isInWidgetMode).isFalse()
|
||||||
|
assertThat(initialState.isCallActive).isFalse()
|
||||||
analyticsLambda.assertions().isNeverCalled()
|
analyticsLambda.assertions().isNeverCalled()
|
||||||
joinedCallLambda.assertions().isCalledOnce()
|
joinedCallLambda.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +107,7 @@ class CallScreenPresenterTest {
|
||||||
joinedCallLambda.assertions().isCalledOnce()
|
joinedCallLambda.assertions().isCalledOnce()
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java)
|
||||||
|
assertThat(initialState.isCallActive).isFalse()
|
||||||
assertThat(initialState.isInWidgetMode).isTrue()
|
assertThat(initialState.isInWidgetMode).isTrue()
|
||||||
assertThat(widgetProvider.getWidgetCalled).isTrue()
|
assertThat(widgetProvider.getWidgetCalled).isTrue()
|
||||||
assertThat(widgetDriver.runCalledCount).isEqualTo(1)
|
assertThat(widgetDriver.runCalledCount).isEqualTo(1)
|
||||||
|
|
@ -203,6 +205,44 @@ class CallScreenPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - a received room member message makes the call to be active`() = runTest {
|
||||||
|
val navigator = FakeCallScreenNavigator()
|
||||||
|
val widgetDriver = FakeMatrixWidgetDriver()
|
||||||
|
val presenter = createCallScreenPresenter(
|
||||||
|
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||||
|
widgetDriver = widgetDriver,
|
||||||
|
navigator = navigator,
|
||||||
|
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||||
|
screenTracker = FakeScreenTracker {},
|
||||||
|
)
|
||||||
|
val messageInterceptor = FakeWidgetMessageInterceptor()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.isCallActive).isFalse()
|
||||||
|
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
|
||||||
|
messageInterceptor.givenInterceptedMessage(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"action":"send_event",
|
||||||
|
"api":"fromWidget",
|
||||||
|
"widgetId":"1",
|
||||||
|
"requestId":"1",
|
||||||
|
"data":{
|
||||||
|
"type":"org.matrix.msc3401.call.member"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
skipItems(1)
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.isCallActive).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
|
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
|
||||||
val navigator = FakeCallScreenNavigator()
|
val navigator = FakeCallScreenNavigator()
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_create_room_action_create_room">"Новая комната"</string>
|
<string name="screen_create_room_action_create_room">"Создать новую комнату"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Пригласить в комнату"</string>
|
<string name="screen_create_room_add_people_title">"Пригласить в комнату"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string>
|
<string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Сообщения в этой комнате зашифрованы. Отключить шифрование позже будет невозможно."</string>
|
<string name="screen_create_room_private_option_description">"Сообщения в этой комнате будут зашифрованы. Отключить шифрование позже будет невозможно."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Приватная комната (только по приглашению)"</string>
|
<string name="screen_create_room_private_option_title">"Частная комната (только по приглашениям)"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Сообщения не зашифрованы, каждый может их прочитать. Вы можете включить шифрование позже."</string>
|
<string name="screen_create_room_public_option_description">"Сообщения не будут зашифрованы и каждый сможет их прочитать. Шифрование можно будет включить позже."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Публичная комната (любой)"</string>
|
<string name="screen_create_room_public_option_title">"Общедоступная комната (для всех)"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Название комнаты"</string>
|
<string name="screen_create_room_room_name_label">"Название комнаты"</string>
|
||||||
<string name="screen_create_room_title">"Создать комнату"</string>
|
<string name="screen_create_room_title">"Создать комнату"</string>
|
||||||
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
|
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при попытке открытия комнаты"</string>
|
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при запуске чата"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ class AccountDeactivationPresenter @Inject constructor(
|
||||||
action
|
action
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
action.value = AsyncAction.Confirming
|
action.value = AsyncAction.ConfirmingNoParams
|
||||||
}
|
}
|
||||||
AccountDeactivationEvents.CloseDialogs -> {
|
AccountDeactivationEvents.CloseDialogs -> {
|
||||||
action.value = AsyncAction.Uninitialized
|
action.value = AsyncAction.Uninitialized
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ open class AccountDeactivationStateProvider : PreviewParameterProvider<AccountDe
|
||||||
),
|
),
|
||||||
anAccountDeactivationState(
|
anAccountDeactivationState(
|
||||||
deactivateFormState = filledForm,
|
deactivateFormState = filledForm,
|
||||||
accountDeactivationAction = AsyncAction.Confirming,
|
accountDeactivationAction = AsyncAction.ConfirmingNoParams,
|
||||||
),
|
),
|
||||||
anAccountDeactivationState(
|
anAccountDeactivationState(
|
||||||
deactivateFormState = filledForm,
|
deactivateFormState = filledForm,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ fun AccountDeactivationActionDialog(
|
||||||
when (state) {
|
when (state) {
|
||||||
AsyncAction.Uninitialized ->
|
AsyncAction.Uninitialized ->
|
||||||
Unit
|
Unit
|
||||||
AsyncAction.Confirming ->
|
is AsyncAction.Confirming ->
|
||||||
AccountDeactivationConfirmationDialog(
|
AccountDeactivationConfirmationDialog(
|
||||||
onSubmitClick = onConfirmClick,
|
onSubmitClick = onConfirmClick,
|
||||||
onDismiss = onDismissDialog
|
onDismiss = onDismissDialog
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,11 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_deactivate_account_confirmation_dialog_content">"Калі ласка, пацвердзіце, што вы хочаце дэактываваць свой уліковы запіс. Гэта дзеянне нельга адмяніць."</string>
|
||||||
|
<string name="screen_deactivate_account_delete_all_messages">"Выдаліць усе мае паведамленні"</string>
|
||||||
|
<string name="screen_deactivate_account_delete_all_messages_notice">"Увага: будучыя карыстальнікі могуць бачыць няпоўныя размовы."</string>
|
||||||
|
<string name="screen_deactivate_account_description_bold_part">"незваротны"</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_1_bold_part">"Назаўсёды адключыць"</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_2">"Выдаліць вас з усіх чатаў."</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_3">"Выдаліце інфармацыю аб сваім уліковым запісе з нашага сервера ідэнтыфікацыі."</string>
|
||||||
<string name="screen_deactivate_account_title">"Дэактываваць уліковы запіс"</string>
|
<string name="screen_deactivate_account_title">"Дэактываваць уліковы запіс"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ class AccountDeactivationPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
||||||
val updatedState = awaitItem()
|
val updatedState = awaitItem()
|
||||||
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.Confirming)
|
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
||||||
val updatedState2 = awaitItem()
|
val updatedState2 = awaitItem()
|
||||||
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
|
@ -102,7 +102,7 @@ class AccountDeactivationPresenterTest {
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
||||||
val updatedState = awaitItem()
|
val updatedState = awaitItem()
|
||||||
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.Confirming)
|
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
||||||
val updatedState2 = awaitItem()
|
val updatedState2 = awaitItem()
|
||||||
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
|
@ -135,7 +135,7 @@ class AccountDeactivationPresenterTest {
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
||||||
val updatedState = awaitItem()
|
val updatedState = awaitItem()
|
||||||
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.Confirming)
|
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
||||||
val updatedState2 = awaitItem()
|
val updatedState2 = awaitItem()
|
||||||
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ class AccountDeactivationViewTest {
|
||||||
deactivateFormState = aDeactivateFormState(
|
deactivateFormState = aDeactivateFormState(
|
||||||
password = A_PASSWORD,
|
password = A_PASSWORD,
|
||||||
),
|
),
|
||||||
accountDeactivationAction = AsyncAction.Confirming,
|
accountDeactivationAction = AsyncAction.ConfirmingNoParams,
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
|
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
|
||||||
<string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string>
|
<string name="screen_notification_optin_title">"Разрешите отправку уведомлений и ни одно сообщение не будет пропущено"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
||||||
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,8 @@ package io.element.android.features.invite.api.response
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import java.util.Optional
|
|
||||||
|
|
||||||
data class AcceptDeclineInviteState(
|
data class AcceptDeclineInviteState(
|
||||||
val invite: Optional<InviteData>,
|
|
||||||
val acceptAction: AsyncAction<RoomId>,
|
val acceptAction: AsyncAction<RoomId>,
|
||||||
val declineAction: AsyncAction<RoomId>,
|
val declineAction: AsyncAction<RoomId>,
|
||||||
val eventSink: (AcceptDeclineInviteEvents) -> Unit,
|
val eventSink: (AcceptDeclineInviteEvents) -> Unit,
|
||||||
|
|
|
||||||
|
|
@ -10,23 +10,20 @@ package io.element.android.features.invite.api.response
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import java.util.Optional
|
|
||||||
|
|
||||||
open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> {
|
open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> {
|
||||||
override val values: Sequence<AcceptDeclineInviteState>
|
override val values: Sequence<AcceptDeclineInviteState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
anAcceptDeclineInviteState(),
|
anAcceptDeclineInviteState(),
|
||||||
anAcceptDeclineInviteState(
|
anAcceptDeclineInviteState(
|
||||||
invite = Optional.of(
|
declineAction = ConfirmingDeclineInvite(
|
||||||
InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice"),
|
InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice")
|
||||||
),
|
),
|
||||||
declineAction = AsyncAction.Confirming,
|
|
||||||
),
|
),
|
||||||
anAcceptDeclineInviteState(
|
anAcceptDeclineInviteState(
|
||||||
invite = Optional.of(
|
declineAction = ConfirmingDeclineInvite(
|
||||||
InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room"),
|
InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room")
|
||||||
),
|
),
|
||||||
declineAction = AsyncAction.Confirming,
|
|
||||||
),
|
),
|
||||||
anAcceptDeclineInviteState(
|
anAcceptDeclineInviteState(
|
||||||
acceptAction = AsyncAction.Failure(Throwable("Whoops")),
|
acceptAction = AsyncAction.Failure(Throwable("Whoops")),
|
||||||
|
|
@ -38,12 +35,10 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDec
|
||||||
}
|
}
|
||||||
|
|
||||||
fun anAcceptDeclineInviteState(
|
fun anAcceptDeclineInviteState(
|
||||||
invite: Optional<InviteData> = Optional.empty(),
|
|
||||||
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
|
eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
|
||||||
) = AcceptDeclineInviteState(
|
) = AcceptDeclineInviteState(
|
||||||
invite = invite,
|
|
||||||
acceptAction = acceptAction,
|
acceptAction = acceptAction,
|
||||||
declineAction = declineAction,
|
declineAction = declineAction,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
|
|
|
||||||
|
|
@ -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.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.InviteData
|
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
|
@ -29,9 +27,7 @@ import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Optional
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.jvm.optionals.getOrNull
|
|
||||||
|
|
||||||
class AcceptDeclineInvitePresenter @Inject constructor(
|
class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
|
|
@ -43,35 +39,22 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val acceptedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val acceptedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
val declinedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val declinedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
var currentInvite by remember {
|
|
||||||
mutableStateOf<Optional<InviteData>>(Optional.empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleEvents(event: AcceptDeclineInviteEvents) {
|
fun handleEvents(event: AcceptDeclineInviteEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is AcceptDeclineInviteEvents.AcceptInvite -> {
|
is AcceptDeclineInviteEvents.AcceptInvite -> {
|
||||||
// currentInvite is used to render the decline confirmation dialog
|
|
||||||
// and to reuse the roomId when the user confirm the rejection of the invitation.
|
|
||||||
// Just set it to empty here.
|
|
||||||
currentInvite = Optional.empty()
|
|
||||||
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
|
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AcceptDeclineInviteEvents.DeclineInvite -> {
|
is AcceptDeclineInviteEvents.DeclineInvite -> {
|
||||||
currentInvite = Optional.of(event.invite)
|
declinedAction.value = ConfirmingDeclineInvite(event.invite)
|
||||||
declinedAction.value = AsyncAction.Confirming
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
|
is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
|
||||||
declinedAction.value = AsyncAction.Uninitialized
|
localCoroutineScope.declineInvite(event.roomId, declinedAction)
|
||||||
currentInvite.getOrNull()?.let {
|
|
||||||
localCoroutineScope.declineInvite(it.roomId, declinedAction)
|
|
||||||
}
|
|
||||||
currentInvite = Optional.empty()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
|
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
|
||||||
currentInvite = Optional.empty()
|
|
||||||
declinedAction.value = AsyncAction.Uninitialized
|
declinedAction.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,7 +69,6 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
return AcceptDeclineInviteState(
|
return AcceptDeclineInviteState(
|
||||||
invite = currentInvite,
|
|
||||||
acceptAction = acceptedAction.value,
|
acceptAction = acceptedAction.value,
|
||||||
declineAction = declinedAction.value,
|
declineAction = declinedAction.value,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
||||||
|
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
|
||||||
import io.element.android.features.invite.api.response.InviteData
|
import io.element.android.features.invite.api.response.InviteData
|
||||||
import io.element.android.features.invite.impl.R
|
import io.element.android.features.invite.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
|
|
@ -22,7 +23,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlin.jvm.optionals.getOrNull
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AcceptDeclineInviteView(
|
fun AcceptDeclineInviteView(
|
||||||
|
|
@ -45,13 +45,13 @@ fun AcceptDeclineInviteView(
|
||||||
onErrorDismiss = {
|
onErrorDismiss = {
|
||||||
state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
|
state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
|
||||||
},
|
},
|
||||||
confirmationDialog = {
|
confirmationDialog = { confirming ->
|
||||||
val invite = state.invite.getOrNull()
|
// Note: confirming will always be of type ConfirmingDeclineInvite.
|
||||||
if (invite != null) {
|
if (confirming is ConfirmingDeclineInvite) {
|
||||||
DeclineConfirmationDialog(
|
DeclineConfirmationDialog(
|
||||||
invite = invite,
|
invite = confirming.inviteData,
|
||||||
onConfirmClick = {
|
onConfirmClick = {
|
||||||
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite)
|
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(confirming.inviteData.roomId))
|
||||||
},
|
},
|
||||||
onDismissClick = {
|
onDismissClick = {
|
||||||
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
|
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,10 @@
|
||||||
package io.element.android.features.invite.impl.response
|
package io.element.android.features.invite.impl.response
|
||||||
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
|
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
|
||||||
data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents
|
data class ConfirmDeclineInvite(val roomId: RoomId) : InternalAcceptDeclineInviteEvents
|
||||||
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
|
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
|
||||||
data object DismissAcceptError : InternalAcceptDeclineInviteEvents
|
data object DismissAcceptError : InternalAcceptDeclineInviteEvents
|
||||||
data object DismissDeclineError : InternalAcceptDeclineInviteEvents
|
data object DismissDeclineError : InternalAcceptDeclineInviteEvents
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_invites_decline_chat_message">"Tens a certeza que queres rejeitar o convite para %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Tens a certeza que queres rejeitar o convite para entra em %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Rejeitar conite"</string>
|
<string name="screen_invites_decline_chat_title">"Rejeitar convite"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Tens a certeza que queres rejeitar esta conversa privada com %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Tem a certeza que queres rejeitar esta conversa privada com %1$s?"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_title">"Rejeitar conversa"</string>
|
<string name="screen_invites_decline_direct_chat_title">"Rejeitar conversa"</string>
|
||||||
<string name="screen_invites_empty_list">"Sem convites"</string>
|
<string name="screen_invites_empty_list">"Sem convites"</string>
|
||||||
<string name="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</string>
|
<string name="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</string>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.features.invite.impl.response
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
|
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
|
||||||
import io.element.android.features.invite.api.response.InviteData
|
import io.element.android.features.invite.api.response.InviteData
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
@ -33,7 +34,6 @@ import io.element.android.tests.testutils.test
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.Optional
|
|
||||||
|
|
||||||
class AcceptDeclineInvitePresenterTest {
|
class AcceptDeclineInvitePresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
|
@ -46,7 +46,6 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,17 +60,13 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
skipItems(1)
|
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.invite).isEqualTo(Optional.of(inviteData))
|
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
|
||||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Confirming::class.java)
|
|
||||||
state.eventSink(
|
state.eventSink(
|
||||||
InternalAcceptDeclineInviteEvents.CancelDeclineInvite
|
InternalAcceptDeclineInviteEvents.CancelDeclineInvite
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
skipItems(1)
|
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
|
|
||||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -93,22 +88,20 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
skipItems(1)
|
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
|
||||||
state.eventSink(
|
state.eventSink(
|
||||||
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
|
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
skipItems(2)
|
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
state.eventSink(
|
state.eventSink(
|
||||||
InternalAcceptDeclineInviteEvents.DismissDeclineError
|
InternalAcceptDeclineInviteEvents.DismissDeclineError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
skipItems(1)
|
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
|
|
||||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
}
|
}
|
||||||
cancelAndConsumeRemainingEvents()
|
cancelAndConsumeRemainingEvents()
|
||||||
|
|
@ -141,13 +134,13 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
skipItems(1)
|
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
|
||||||
state.eventSink(
|
state.eventSink(
|
||||||
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
|
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
skipItems(2)
|
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +166,6 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
|
|
||||||
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
|
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
|
||||||
}
|
}
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
|
|
@ -183,7 +175,6 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
|
|
||||||
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
}
|
}
|
||||||
cancelAndConsumeRemainingEvents()
|
cancelAndConsumeRemainingEvents()
|
||||||
|
|
@ -220,7 +211,6 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
|
|
||||||
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
|
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
|
||||||
}
|
}
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_join_room_join_action">"Join room"</string>
|
<string name="screen_join_room_join_action">"Join room"</string>
|
||||||
<string name="screen_join_room_knock_action">"Knock to join"</string>
|
<string name="screen_join_room_knock_action">"Send request to join"</string>
|
||||||
<string name="screen_join_room_space_not_supported_description">"%1$s does not support spaces yet. You can access spaces on web."</string>
|
<string name="screen_join_room_space_not_supported_description">"%1$s does not support spaces yet. You can access spaces on web."</string>
|
||||||
<string name="screen_join_room_space_not_supported_title">"Spaces are not supported yet"</string>
|
<string name="screen_join_room_space_not_supported_title">"Spaces are not supported yet"</string>
|
||||||
<string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."</string>
|
<string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. 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.features.joinroom.impl.di.KnockRoom
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
class FakeKnockRoom(
|
class FakeKnockRoom(
|
||||||
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
|
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
|
||||||
) : KnockRoom {
|
) : KnockRoom {
|
||||||
override suspend fun invoke(roomId: RoomId) = lambda(roomId)
|
override suspend fun invoke(roomId: RoomId) = simulateLongTask {
|
||||||
|
lambda(roomId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="leave_conversation_alert_subtitle">"Вы уверены, что хотите покинуть беседу?"</string>
|
<string name="leave_conversation_alert_subtitle">"Вы уверены, что хотите покинуть беседу? Эта беседа не является общедоступной, и Вы не сможете присоединиться к ней без приглашения."</string>
|
||||||
<string name="leave_room_alert_empty_subtitle">"Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас."</string>
|
<string name="leave_room_alert_empty_subtitle">"Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас."</string>
|
||||||
<string name="leave_room_alert_private_subtitle">"Вы уверены, что хотите покинуть эту комнату? Эта комната не является публичной, и Вы не сможете присоединиться к ней без приглашения."</string>
|
<string name="leave_room_alert_private_subtitle">"Вы уверены, что хотите покинуть эту комнату? Эта комната не является общедоступной, и Вы не сможете присоединиться к ней без приглашения."</string>
|
||||||
<string name="leave_room_alert_subtitle">"Вы уверены, что хотите покинуть комнату?"</string>
|
<string name="leave_room_alert_subtitle">"Вы уверены, что хотите покинуть комнату?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS
|
||||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
|
|
@ -397,8 +399,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
fakeMessageComposerContext.apply {
|
fakeMessageComposerContext.apply {
|
||||||
composerMode = MessageComposerMode.Edit(
|
composerMode = MessageComposerMode.Edit(
|
||||||
eventId = null,
|
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
||||||
transactionId = null,
|
|
||||||
content = ""
|
content = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -446,8 +447,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
fakeMessageComposerContext.apply {
|
fakeMessageComposerContext.apply {
|
||||||
composerMode = MessageComposerMode.Edit(
|
composerMode = MessageComposerMode.Edit(
|
||||||
eventId = null,
|
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
||||||
transactionId = null,
|
|
||||||
content = ""
|
content = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ fun PinUnlockView(
|
||||||
latestOnSuccessLogout(state.signOutAction.data)
|
latestOnSuccessLogout(state.signOutAction.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AsyncAction.Confirming,
|
is AsyncAction.Confirming,
|
||||||
is AsyncAction.Failure,
|
is AsyncAction.Failure,
|
||||||
AsyncAction.Uninitialized -> Unit
|
AsyncAction.Uninitialized -> Unit
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<string name="screen_app_lock_biometric_unlock">"биометрическая разблокировка"</string>
|
<string name="screen_app_lock_biometric_unlock">"биометрическая разблокировка"</string>
|
||||||
<string name="screen_app_lock_biometric_unlock_title_android">"Разблокировать с помощью биометрии"</string>
|
<string name="screen_app_lock_biometric_unlock_title_android">"Разблокировать с помощью биометрии"</string>
|
||||||
<string name="screen_app_lock_forgot_pin">"Забыли PIN-код?"</string>
|
<string name="screen_app_lock_forgot_pin">"Забыли PIN-код?"</string>
|
||||||
<string name="screen_app_lock_settings_change_pin">"Измените PIN-код"</string>
|
<string name="screen_app_lock_settings_change_pin">"Изменить PIN-код"</string>
|
||||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Разрешить биометрическую разблокировку"</string>
|
<string name="screen_app_lock_settings_enable_biometric_unlock">"Разрешить биометрическую разблокировку"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin">"Удалить PIN-код"</string>
|
<string name="screen_app_lock_settings_remove_pin">"Удалить PIN-код"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin_alert_message">"Вы действительно хотите удалить PIN-код?"</string>
|
<string name="screen_app_lock_settings_remove_pin_alert_message">"Вы действительно хотите удалить PIN-код?"</string>
|
||||||
|
|
@ -22,16 +22,16 @@
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Повторите PIN-код"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Повторите PIN-код"</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-коды не совпадают"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-коды не совпадают"</string>
|
||||||
<string name="screen_app_lock_signout_alert_message">"Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код"</string>
|
<string name="screen_app_lock_signout_alert_message">"Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код"</string>
|
||||||
<string name="screen_app_lock_signout_alert_title">"Вы выходите из системы"</string>
|
<string name="screen_app_lock_signout_alert_title">"Выполняется выход из системы"</string>
|
||||||
<plurals name="screen_app_lock_subtitle">
|
<plurals name="screen_app_lock_subtitle">
|
||||||
<item quantity="one">"Вы попытались разблокировать %1$d раз"</item>
|
<item quantity="one">"У вас осталась %1$d попытка на разблокировку"</item>
|
||||||
<item quantity="few">"Вы попытались разблокировать %1$d раз"</item>
|
<item quantity="few">"У вас остались %1$d попытки на разблокировку"</item>
|
||||||
<item quantity="many">"Вы попытались разблокировать много раз"</item>
|
<item quantity="many">"У вас осталось %1$d попыток на разблокировку"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||||
<item quantity="one">"Неверный PIN-код. У вас остался %1$d шанс"</item>
|
<item quantity="one">"Неверный PIN-код. У вас осталась %1$d попытка"</item>
|
||||||
<item quantity="few">"Неверный PIN-код. У вас остался %1$d шансов"</item>
|
<item quantity="few">"Неверный PIN-код. У вас остались %1$d попытки"</item>
|
||||||
<item quantity="many">"Неверный PIN-код. У вас остался %1$d шанса"</item>
|
<item quantity="many">"Неверный PIN-код. У вас осталось %1$d попыток"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_app_lock_use_biometric_android">"Использовать биометрию"</string>
|
<string name="screen_app_lock_use_biometric_android">"Использовать биометрию"</string>
|
||||||
<string name="screen_app_lock_use_pin_android">"Использовать PIN-код"</string>
|
<string name="screen_app_lock_use_pin_android">"Использовать PIN-код"</string>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
import io.element.android.libraries.matrix.api.auth.OidcPrompt
|
||||||
import io.element.android.libraries.oidc.api.OidcAction
|
import io.element.android.libraries.oidc.api.OidcAction
|
||||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -92,7 +93,8 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||||
val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!!
|
val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!!
|
||||||
if (matrixHomeServerDetails.supportsOidcLogin) {
|
if (matrixHomeServerDetails.supportsOidcLogin) {
|
||||||
// Retrieve the details right now
|
// Retrieve the details right now
|
||||||
LoginFlow.OidcFlow(authenticationService.getOidcUrl().getOrThrow())
|
val oidcPrompt = if (params.isAccountCreation) OidcPrompt.Create else OidcPrompt.Consent
|
||||||
|
LoginFlow.OidcFlow(authenticationService.getOidcUrl(oidcPrompt).getOrThrow())
|
||||||
} else if (params.isAccountCreation) {
|
} else if (params.isAccountCreation) {
|
||||||
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)
|
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)
|
||||||
LoginFlow.AccountCreationFlow(url)
|
LoginFlow.AccountCreationFlow(url)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ package io.element.android.features.login.impl.screens.qrcode.confirmation
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
class QrCodeConfirmationStepPreviewProvider : PreviewParameterProvider<QrCodeConfirmationStep> {
|
class QrCodeConfirmationStepProvider : PreviewParameterProvider<QrCodeConfirmationStep> {
|
||||||
override val values: Sequence<QrCodeConfirmationStep>
|
override val values: Sequence<QrCodeConfirmationStep>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
QrCodeConfirmationStep.DisplayCheckCode("12"),
|
QrCodeConfirmationStep.DisplayCheckCode("12"),
|
||||||
|
|
@ -148,7 +148,7 @@ private fun Buttons(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun QrCodeConfirmationViewPreview(@PreviewParameter(QrCodeConfirmationStepPreviewProvider::class) step: QrCodeConfirmationStep) {
|
internal fun QrCodeConfirmationViewPreview(@PreviewParameter(QrCodeConfirmationStepProvider::class) step: QrCodeConfirmationStep) {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
QrCodeConfirmationView(
|
QrCodeConfirmationView(
|
||||||
step = step,
|
step = step,
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ private fun ColumnScope.Buttons(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AsyncAction.Uninitialized,
|
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">"Выберыце %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Адсканіруйце QR-код з дапамогай гэтай прылады"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Адсканіруйце QR-код з дапамогай гэтай прылады"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_subtitle">"Даступна толькі ў тым выпадку, калі ваш правайдар уліковага запісу гэта падтрымлівае."</string>
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Επιλογή %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Επιλογή %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"«Σύνδεση νέας συσκευής»"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"«Σύνδεση νέας συσκευής»"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Σάρωσε τον κωδικό QR με αυτήν τη συσκευή"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Σάρωσε τον κωδικό QR με αυτήν τη συσκευή"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_subtitle">"Διατίθεται μόνο εάν ο πάροχος του λογαριασμού σου το υποστηρίζει."</string>
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Άνοιγμα %1$s σε άλλη συσκευή για να λήψη κωδικού QR"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Άνοιγμα %1$s σε άλλη συσκευή για να λήψη κωδικού QR"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Χρησιμοποίησε τον κωδικό QR που εμφανίζεται στην άλλη συσκευή."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Χρησιμοποίησε τον κωδικό QR που εμφανίζεται στην άλλη συσκευή."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Προσπάθησε ξανά"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Προσπάθησε ξανά"</string>
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ Tenta iniciar a sessão manualmente ou digitaliza o código QR com outro disposi
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Seleciona %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Seleciona %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Ligar novo dispositivo”"</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"“Ligar novo dispositivo”"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Lê o código QR com este dispositivo"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Lê o código QR com este dispositivo"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_subtitle">"Disponível apenas se o seu fornecedor de conta o suportar."</string>
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Abre a %1$s noutro dispositivo para obteres o código QR"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Abre a %1$s noutro dispositivo para obteres o código QR"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Lê o código QR apresentado no outro dispositivo."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Lê o código QR apresentado no outro dispositivo."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Tentar novamente"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Tentar novamente"</string>
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
<string name="screen_account_provider_form_notice">"Введите поисковый запрос или адрес домена."</string>
|
<string name="screen_account_provider_form_notice">"Введите поисковый запрос или адрес домена."</string>
|
||||||
<string name="screen_account_provider_form_subtitle">"Поиск компании, сообщества или частного сервера."</string>
|
<string name="screen_account_provider_form_subtitle">"Поиск компании, сообщества или частного сервера."</string>
|
||||||
<string name="screen_account_provider_form_title">"Поиск сервера учетной записи"</string>
|
<string name="screen_account_provider_form_title">"Поиск сервера учетной записи"</string>
|
||||||
<string name="screen_account_provider_signin_subtitle">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string>
|
<string name="screen_account_provider_signin_subtitle">"Здесь будут храниться ваши разговоры — точно так же, как если бы вы использовали почтового провайдера для хранения своих писем."</string>
|
||||||
<string name="screen_account_provider_signin_title">"Вы собираетесь войти в %s"</string>
|
<string name="screen_account_provider_signin_title">"Вы собираетесь войти в %s"</string>
|
||||||
<string name="screen_account_provider_signup_subtitle">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string>
|
<string name="screen_account_provider_signup_subtitle">"Здесь будут храниться ваши разговоры — точно так же, как если бы вы использовали почтового провайдера для хранения своих писем."</string>
|
||||||
<string name="screen_account_provider_signup_title">"Вы собираетесь создать учетную запись на %s"</string>
|
<string name="screen_account_provider_signup_title">"Вы собираетесь создать учетную запись на %s"</string>
|
||||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org — это большой бесплатный сервер в общедоступной сети Matrix для безопасной децентрализованной связи, управляемый Matrix.org Foundation."</string>
|
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org — это большой бесплатный сервер в общедоступной сети Matrix для безопасной децентрализованной связи, управляемый Matrix.org Foundation."</string>
|
||||||
<string name="screen_change_account_provider_other">"Другое"</string>
|
<string name="screen_change_account_provider_other">"Другое"</string>
|
||||||
|
|
@ -27,14 +27,14 @@
|
||||||
<string name="screen_login_error_invalid_user_id">"Это не корректный идентификатор пользователя. Ожидаемый формат: \'@user:homeserver.org\'"</string>
|
<string name="screen_login_error_invalid_user_id">"Это не корректный идентификатор пользователя. Ожидаемый формат: \'@user:homeserver.org\'"</string>
|
||||||
<string name="screen_login_error_refresh_tokens">"Этот сервер настроен на использование токенов обновления. Они не поддерживаются при использовании входа на основе пароля."</string>
|
<string name="screen_login_error_refresh_tokens">"Этот сервер настроен на использование токенов обновления. Они не поддерживаются при использовании входа на основе пароля."</string>
|
||||||
<string name="screen_login_error_unsupported_authentication">"Выбранный домашний сервер не поддерживает пароль или логин OIDC. Пожалуйста, свяжитесь с администратором или выберите другой домашний сервер."</string>
|
<string name="screen_login_error_unsupported_authentication">"Выбранный домашний сервер не поддерживает пароль или логин OIDC. Пожалуйста, свяжитесь с администратором или выберите другой домашний сервер."</string>
|
||||||
<string name="screen_login_form_header">"Введите сведения о себе"</string>
|
<string name="screen_login_form_header">"Введите свои данные"</string>
|
||||||
<string name="screen_login_subtitle">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
|
<string name="screen_login_subtitle">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
|
||||||
<string name="screen_login_title">"Рады видеть вас снова!"</string>
|
<string name="screen_login_title">"Рады видеть вас снова!"</string>
|
||||||
<string name="screen_login_title_with_homeserver">"Войти в %1$s"</string>
|
<string name="screen_login_title_with_homeserver">"Войти в %1$s"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Установление безопасного соединения"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Установление безопасного соединения"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Что теперь?"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Что теперь?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Попробуйте снова войти в систему с помощью QR-кода, если это была сетевая проблема"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Попробуйте снова войти в систему с помощью QR-кода, если это была проблема с соединением"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
|
||||||
|
|
@ -55,11 +55,12 @@
|
||||||
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Поставщик учетной записи не поддерживает %1$s."</string>
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Поставщик учетной записи не поддерживает %1$s."</string>
|
||||||
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s не поддерживается"</string>
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s не поддерживается"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_button_title">"Готово к сканированию"</string>
|
<string name="screen_qr_code_login_initial_state_button_title">"Готово к сканированию"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на компьютере"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Выберите %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Отсканируйте QR-код с помощью этого устройства"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Отсканируйте QR-код с помощью этого устройства"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_subtitle">"Доступно только в том случае, если ваш поставщик учетной записи поддерживает это."</string>
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>
|
||||||
|
|
@ -76,7 +77,7 @@
|
||||||
<string name="screen_server_confirmation_change_server">"Сменить поставщика учетной записи"</string>
|
<string name="screen_server_confirmation_change_server">"Сменить поставщика учетной записи"</string>
|
||||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Частный сервер для сотрудников Element."</string>
|
<string name="screen_server_confirmation_message_login_element_dot_io">"Частный сервер для сотрудников Element."</string>
|
||||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
|
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
|
||||||
<string name="screen_server_confirmation_message_register">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string>
|
<string name="screen_server_confirmation_message_register">"Здесь будут храниться ваши разговоры — точно так же, как если бы вы использовали почтового провайдера для хранения своих писем."</string>
|
||||||
<string name="screen_server_confirmation_title_login">"Вы собираетесь войти в %1$s"</string>
|
<string name="screen_server_confirmation_title_login">"Вы собираетесь войти в %1$s"</string>
|
||||||
<string name="screen_server_confirmation_title_register">"Вы собираетесь создать учетную запись на %1$s"</string>
|
<string name="screen_server_confirmation_title_register">"Вы собираетесь создать учетную запись на %1$s"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ open class DirectLogoutStateProvider : PreviewParameterProvider<DirectLogoutStat
|
||||||
override val values: Sequence<DirectLogoutState>
|
override val values: Sequence<DirectLogoutState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aDirectLogoutState(),
|
aDirectLogoutState(),
|
||||||
aDirectLogoutState(logoutAction = AsyncAction.Confirming),
|
aDirectLogoutState(logoutAction = AsyncAction.ConfirmingNoParams),
|
||||||
aDirectLogoutState(logoutAction = AsyncAction.Loading),
|
aDirectLogoutState(logoutAction = AsyncAction.Loading),
|
||||||
aDirectLogoutState(logoutAction = AsyncAction.Failure(Exception("Error"))),
|
aDirectLogoutState(logoutAction = AsyncAction.Failure(Exception("Error"))),
|
||||||
aDirectLogoutState(logoutAction = AsyncAction.Success("success")),
|
aDirectLogoutState(logoutAction = AsyncAction.Success("success")),
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class LogoutPresenter @Inject constructor(
|
||||||
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
|
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
|
||||||
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
|
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
|
||||||
} else {
|
} else {
|
||||||
logoutAction.value = AsyncAction.Confirming
|
logoutAction.value = AsyncAction.ConfirmingNoParams
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LogoutEvents.CloseDialogs -> {
|
LogoutEvents.CloseDialogs -> {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ open class LogoutStateProvider : PreviewParameterProvider<LogoutState> {
|
||||||
aLogoutState(isLastDevice = true),
|
aLogoutState(isLastDevice = true),
|
||||||
aLogoutState(isLastDevice = false, backupUploadState = BackupUploadState.Uploading(66, 200)),
|
aLogoutState(isLastDevice = false, backupUploadState = BackupUploadState.Uploading(66, 200)),
|
||||||
aLogoutState(isLastDevice = true, backupUploadState = BackupUploadState.Done),
|
aLogoutState(isLastDevice = true, backupUploadState = BackupUploadState.Done),
|
||||||
aLogoutState(logoutAction = AsyncAction.Confirming),
|
aLogoutState(logoutAction = AsyncAction.ConfirmingNoParams),
|
||||||
aLogoutState(logoutAction = AsyncAction.Loading),
|
aLogoutState(logoutAction = AsyncAction.Loading),
|
||||||
aLogoutState(logoutAction = AsyncAction.Failure(Exception("Failed to logout"))),
|
aLogoutState(logoutAction = AsyncAction.Failure(Exception("Failed to logout"))),
|
||||||
aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))),
|
aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))),
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class DirectLogoutPresenter @Inject constructor(
|
||||||
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
|
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
|
||||||
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
|
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
|
||||||
} else {
|
} else {
|
||||||
logoutAction.value = AsyncAction.Confirming
|
logoutAction.value = AsyncAction.ConfirmingNoParams
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DirectLogoutEvents.CloseDialogs -> {
|
DirectLogoutEvents.CloseDialogs -> {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ fun LogoutActionDialog(
|
||||||
when (state) {
|
when (state) {
|
||||||
AsyncAction.Uninitialized ->
|
AsyncAction.Uninitialized ->
|
||||||
Unit
|
Unit
|
||||||
AsyncAction.Confirming ->
|
is AsyncAction.Confirming ->
|
||||||
LogoutConfirmationDialog(
|
LogoutConfirmationDialog(
|
||||||
onSubmitClick = onConfirmClick,
|
onSubmitClick = onConfirmClick,
|
||||||
onDismiss = onDismissDialog
|
onDismiss = onDismissDialog
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ class LogoutPresenterTest {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val confirmationState = awaitItem()
|
val confirmationState = awaitItem()
|
||||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
initialState.eventSink.invoke(LogoutEvents.CloseDialogs)
|
initialState.eventSink.invoke(LogoutEvents.CloseDialogs)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
|
@ -123,7 +123,7 @@ class LogoutPresenterTest {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val confirmationState = awaitItem()
|
val confirmationState = awaitItem()
|
||||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
|
@ -148,7 +148,7 @@ class LogoutPresenterTest {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val confirmationState = awaitItem()
|
val confirmationState = awaitItem()
|
||||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
|
@ -180,7 +180,7 @@ class LogoutPresenterTest {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val confirmationState = awaitItem()
|
val confirmationState = awaitItem()
|
||||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class LogoutViewTest {
|
||||||
val eventsRecorder = EventsRecorder<LogoutEvents>()
|
val eventsRecorder = EventsRecorder<LogoutEvents>()
|
||||||
rule.setLogoutView(
|
rule.setLogoutView(
|
||||||
aLogoutState(
|
aLogoutState(
|
||||||
logoutAction = AsyncAction.Confirming,
|
logoutAction = AsyncAction.ConfirmingNoParams,
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ class DefaultDirectLogoutViewTest {
|
||||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
||||||
rule.setDefaultDirectLogoutView(
|
rule.setDefaultDirectLogoutView(
|
||||||
state = aDirectLogoutState(
|
state = aDirectLogoutState(
|
||||||
logoutAction = AsyncAction.Confirming,
|
logoutAction = AsyncAction.ConfirmingNoParams,
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -49,7 +49,7 @@ class DefaultDirectLogoutViewTest {
|
||||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
||||||
rule.setDefaultDirectLogoutView(
|
rule.setDefaultDirectLogoutView(
|
||||||
state = aDirectLogoutState(
|
state = aDirectLogoutState(
|
||||||
logoutAction = AsyncAction.Confirming,
|
logoutAction = AsyncAction.ConfirmingNoParams,
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -63,7 +63,7 @@ class DefaultDirectLogoutViewTest {
|
||||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
||||||
rule.setDefaultDirectLogoutView(
|
rule.setDefaultDirectLogoutView(
|
||||||
state = aDirectLogoutState(
|
state = aDirectLogoutState(
|
||||||
logoutAction = AsyncAction.Confirming,
|
logoutAction = AsyncAction.ConfirmingNoParams,
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ class DirectLogoutPresenterTest {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val confirmationState = awaitItem()
|
val confirmationState = awaitItem()
|
||||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
initialState.eventSink.invoke(DirectLogoutEvents.CloseDialogs)
|
initialState.eventSink.invoke(DirectLogoutEvents.CloseDialogs)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
|
@ -104,7 +104,7 @@ class DirectLogoutPresenterTest {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val confirmationState = awaitItem()
|
val confirmationState = awaitItem()
|
||||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
|
@ -129,7 +129,7 @@ class DirectLogoutPresenterTest {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val confirmationState = awaitItem()
|
val confirmationState = awaitItem()
|
||||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
|
@ -161,7 +161,7 @@ class DirectLogoutPresenterTest {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val confirmationState = awaitItem()
|
val confirmationState = awaitItem()
|
||||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,12 @@ package io.element.android.features.logout.test
|
||||||
|
|
||||||
import io.element.android.features.logout.api.LogoutUseCase
|
import io.element.android.features.logout.api.LogoutUseCase
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
class FakeLogoutUseCase(
|
class FakeLogoutUseCase(
|
||||||
var logoutLambda: (Boolean) -> String? = { lambdaError() }
|
var logoutLambda: (Boolean) -> String? = { lambdaError() }
|
||||||
) : LogoutUseCase {
|
) : LogoutUseCase {
|
||||||
override suspend fun logout(ignoreSdkError: Boolean): String? {
|
override suspend fun logout(ignoreSdkError: Boolean): String? = simulateLongTask {
|
||||||
return logoutLambda(ignoreSdkError)
|
logoutLambda(ignoreSdkError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ package io.element.android.features.messages.impl
|
||||||
|
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||||
|
|
||||||
sealed interface MessagesEvents {
|
sealed interface MessagesEvents {
|
||||||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
|
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
|
||||||
data class ToggleReaction(val emoji: String, val uniqueId: UniqueId) : MessagesEvents
|
data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvents
|
||||||
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
|
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
|
||||||
data object Dismiss : MessagesEvents
|
data object Dismiss : MessagesEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ import io.element.android.features.poll.api.create.CreatePollMode
|
||||||
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.inputs
|
|
||||||
import io.element.android.libraries.architecture.overlay.Overlay
|
import io.element.android.libraries.architecture.overlay.Overlay
|
||||||
import io.element.android.libraries.architecture.overlay.operation.show
|
import io.element.android.libraries.architecture.overlay.operation.show
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
|
@ -324,7 +323,8 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
is TimelineItemImageContent -> {
|
is TimelineItemImageContent -> {
|
||||||
val navTarget = NavTarget.MediaViewer(
|
val navTarget = NavTarget.MediaViewer(
|
||||||
mediaInfo = MediaInfo(
|
mediaInfo = MediaInfo(
|
||||||
name = event.content.filename ?: event.content.body,
|
filename = event.content.filename,
|
||||||
|
caption = event.content.caption,
|
||||||
mimeType = event.content.mimeType,
|
mimeType = event.content.mimeType,
|
||||||
formattedFileSize = event.content.formattedFileSize,
|
formattedFileSize = event.content.formattedFileSize,
|
||||||
fileExtension = event.content.fileExtension
|
fileExtension = event.content.fileExtension
|
||||||
|
|
@ -341,7 +341,8 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
if (event.content.preferredMediaSource != null) {
|
if (event.content.preferredMediaSource != null) {
|
||||||
val navTarget = NavTarget.MediaViewer(
|
val navTarget = NavTarget.MediaViewer(
|
||||||
mediaInfo = MediaInfo(
|
mediaInfo = MediaInfo(
|
||||||
name = event.content.body,
|
filename = event.content.filename,
|
||||||
|
caption = event.content.caption,
|
||||||
mimeType = event.content.mimeType,
|
mimeType = event.content.mimeType,
|
||||||
formattedFileSize = event.content.formattedFileSize,
|
formattedFileSize = event.content.formattedFileSize,
|
||||||
fileExtension = event.content.fileExtension
|
fileExtension = event.content.fileExtension
|
||||||
|
|
@ -358,7 +359,8 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
is TimelineItemVideoContent -> {
|
is TimelineItemVideoContent -> {
|
||||||
val navTarget = NavTarget.MediaViewer(
|
val navTarget = NavTarget.MediaViewer(
|
||||||
mediaInfo = MediaInfo(
|
mediaInfo = MediaInfo(
|
||||||
name = event.content.filename ?: event.content.body,
|
filename = event.content.filename,
|
||||||
|
caption = event.content.caption,
|
||||||
mimeType = event.content.mimeType,
|
mimeType = event.content.mimeType,
|
||||||
formattedFileSize = event.content.formattedFileSize,
|
formattedFileSize = event.content.formattedFileSize,
|
||||||
fileExtension = event.content.fileExtension
|
fileExtension = event.content.fileExtension
|
||||||
|
|
@ -372,7 +374,8 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
is TimelineItemFileContent -> {
|
is TimelineItemFileContent -> {
|
||||||
val navTarget = NavTarget.MediaViewer(
|
val navTarget = NavTarget.MediaViewer(
|
||||||
mediaInfo = MediaInfo(
|
mediaInfo = MediaInfo(
|
||||||
name = event.content.body,
|
filename = event.content.filename,
|
||||||
|
caption = event.content.caption,
|
||||||
mimeType = event.content.mimeType,
|
mimeType = event.content.mimeType,
|
||||||
formattedFileSize = event.content.formattedFileSize,
|
formattedFileSize = event.content.formattedFileSize,
|
||||||
fileExtension = event.content.fileExtension
|
fileExtension = event.content.fileExtension
|
||||||
|
|
@ -386,7 +389,8 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
is TimelineItemAudioContent -> {
|
is TimelineItemAudioContent -> {
|
||||||
val navTarget = NavTarget.MediaViewer(
|
val navTarget = NavTarget.MediaViewer(
|
||||||
mediaInfo = MediaInfo(
|
mediaInfo = MediaInfo(
|
||||||
name = event.content.body,
|
filename = event.content.filename,
|
||||||
|
caption = event.content.caption,
|
||||||
mimeType = event.content.mimeType,
|
mimeType = event.content.mimeType,
|
||||||
formattedFileSize = event.content.formattedFileSize,
|
formattedFileSize = event.content.formattedFileSize,
|
||||||
fileExtension = event.content.fileExtension
|
fileExtension = event.content.fileExtension
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
|
|
@ -73,6 +72,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
|
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.room.canCall
|
import io.element.android.libraries.matrix.ui.room.canCall
|
||||||
|
|
@ -191,7 +191,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is MessagesEvents.ToggleReaction -> {
|
is MessagesEvents.ToggleReaction -> {
|
||||||
localCoroutineScope.toggleReaction(event.emoji, event.uniqueId)
|
localCoroutineScope.toggleReaction(event.emoji, event.eventOrTransactionId)
|
||||||
}
|
}
|
||||||
is MessagesEvents.InviteDialogDismissed -> {
|
is MessagesEvents.InviteDialogDismissed -> {
|
||||||
hasDismissedInviteDialog = true
|
hasDismissedInviteDialog = true
|
||||||
|
|
@ -327,10 +327,10 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.toggleReaction(
|
private fun CoroutineScope.toggleReaction(
|
||||||
emoji: String,
|
emoji: String,
|
||||||
uniqueId: UniqueId,
|
eventOrTransactionId: EventOrTransactionId,
|
||||||
) = launch(dispatchers.io) {
|
) = launch(dispatchers.io) {
|
||||||
timelineController.invokeOnCurrentTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
toggleReaction(emoji, uniqueId)
|
toggleReaction(emoji, eventOrTransactionId)
|
||||||
.onFailure { Timber.e(it) }
|
.onFailure { Timber.e(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -360,7 +360,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private suspend fun handleActionRedact(event: TimelineItem.Event) {
|
private suspend fun handleActionRedact(event: TimelineItem.Event) {
|
||||||
timelineController.invokeOnCurrentTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
redactEvent(eventId = event.eventId, transactionId = event.transactionId, reason = null)
|
redactEvent(eventOrTransactionId = event.eventOrTransactionId, reason = null)
|
||||||
.onFailure { Timber.e(it) }
|
.onFailure { Timber.e(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -377,8 +377,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val composerMode = MessageComposerMode.Edit(
|
val composerMode = MessageComposerMode.Edit(
|
||||||
targetEvent.eventId,
|
targetEvent.eventOrTransactionId,
|
||||||
targetEvent.transactionId,
|
|
||||||
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
|
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
|
||||||
if (enableTextFormatting) {
|
if (enableTextFormatting) {
|
||||||
it.htmlBody ?: it.body
|
it.htmlBody ?: it.body
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ fun MessagesView(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEmojiReactionClick(emoji: String, event: TimelineItem.Event) {
|
fun onEmojiReactionClick(emoji: String, event: TimelineItem.Event) {
|
||||||
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.id))
|
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.eventOrTransactionId))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEmojiReactionLongClick(emoji: String, event: TimelineItem.Event) {
|
fun onEmojiReactionLongClick(emoji: String, event: TimelineItem.Event) {
|
||||||
|
|
|
||||||
|
|
@ -31,103 +31,109 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
return sequenceOf(
|
return sequenceOf(
|
||||||
anActionListState(),
|
anActionListState(),
|
||||||
anActionListState().copy(target = ActionListState.Target.Loading(aTimelineItemEvent())),
|
anActionListState().copy(target = ActionListState.Target.Loading(aTimelineItemEvent())),
|
||||||
anActionListState().copy(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent().copy(
|
event = aTimelineItemEvent(
|
||||||
reactionsState = reactionsState
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState().copy(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(
|
event = aTimelineItemEvent(
|
||||||
content = aTimelineItemImageContent(),
|
content = aTimelineItemImageContent(),
|
||||||
displayNameAmbiguous = true,
|
displayNameAmbiguous = true,
|
||||||
).copy(
|
timelineItemReactions = reactionsState,
|
||||||
reactionsState = reactionsState,
|
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState().copy(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(content = aTimelineItemVideoContent()).copy(
|
event = aTimelineItemEvent(
|
||||||
reactionsState = reactionsState
|
content = aTimelineItemVideoContent(),
|
||||||
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState().copy(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(content = aTimelineItemFileContent()).copy(
|
event = aTimelineItemEvent(
|
||||||
reactionsState = reactionsState
|
content = aTimelineItemFileContent(),
|
||||||
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState().copy(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(content = aTimelineItemAudioContent()).copy(
|
event = aTimelineItemEvent(
|
||||||
reactionsState = reactionsState
|
content = aTimelineItemAudioContent(),
|
||||||
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState().copy(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(content = aTimelineItemVoiceContent()).copy(
|
event = aTimelineItemEvent(
|
||||||
reactionsState = reactionsState
|
content = aTimelineItemVoiceContent(),
|
||||||
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState().copy(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(content = aTimelineItemLocationContent()).copy(
|
event = aTimelineItemEvent(
|
||||||
reactionsState = reactionsState
|
content = aTimelineItemLocationContent(),
|
||||||
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState().copy(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(content = aTimelineItemLocationContent()).copy(
|
event = aTimelineItemEvent(
|
||||||
reactionsState = reactionsState
|
content = aTimelineItemLocationContent(),
|
||||||
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
anActionListState().copy(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(content = aTimelineItemPollContent()).copy(
|
event = aTimelineItemEvent(
|
||||||
reactionsState = reactionsState
|
content = aTimelineItemPollContent(),
|
||||||
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemPollActionList(),
|
actions = aTimelineItemPollActionList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
anActionListState().copy(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent().copy(
|
event = aTimelineItemEvent(
|
||||||
reactionsState = reactionsState,
|
timelineItemReactions = reactionsState,
|
||||||
messageShield = MessageShield.UnknownDevice(isCritical = true)
|
messageShield = MessageShield.UnknownDevice(isCritical = true)
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
|
@ -135,7 +141,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState().copy(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(),
|
event = aTimelineItemEvent(),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ fun ActionListView(
|
||||||
onDismissRequest = ::onDismiss,
|
onDismissRequest = ::onDismiss,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
SheetContent(
|
ActionListViewContent(
|
||||||
state = state,
|
state = state,
|
||||||
onActionClick = ::onItemActionClick,
|
onActionClick = ::onItemActionClick,
|
||||||
onEmojiReactionClick = ::onEmojiReactionClick,
|
onEmojiReactionClick = ::onEmojiReactionClick,
|
||||||
|
|
@ -161,7 +161,7 @@ fun ActionListView(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SheetContent(
|
private fun ActionListViewContent(
|
||||||
state: ActionListState,
|
state: ActionListState,
|
||||||
onActionClick: (TimelineItemAction) -> Unit,
|
onActionClick: (TimelineItemAction) -> Unit,
|
||||||
onEmojiReactionClick: (String) -> Unit,
|
onEmojiReactionClick: (String) -> Unit,
|
||||||
|
|
@ -269,19 +269,19 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||||
content = { ContentForBody(stringResource(CommonStrings.common_shared_location)) }
|
content = { ContentForBody(stringResource(CommonStrings.common_shared_location)) }
|
||||||
}
|
}
|
||||||
is TimelineItemImageContent -> {
|
is TimelineItemImageContent -> {
|
||||||
content = { ContentForBody(event.content.body) }
|
content = { ContentForBody(event.content.bestDescription) }
|
||||||
}
|
}
|
||||||
is TimelineItemStickerContent -> {
|
is TimelineItemStickerContent -> {
|
||||||
content = { ContentForBody(event.content.body) }
|
content = { ContentForBody(event.content.bestDescription) }
|
||||||
}
|
}
|
||||||
is TimelineItemVideoContent -> {
|
is TimelineItemVideoContent -> {
|
||||||
content = { ContentForBody(event.content.body) }
|
content = { ContentForBody(event.content.bestDescription) }
|
||||||
}
|
}
|
||||||
is TimelineItemFileContent -> {
|
is TimelineItemFileContent -> {
|
||||||
content = { ContentForBody(event.content.body) }
|
content = { ContentForBody(event.content.bestDescription) }
|
||||||
}
|
}
|
||||||
is TimelineItemAudioContent -> {
|
is TimelineItemAudioContent -> {
|
||||||
content = { ContentForBody(event.content.body) }
|
content = { ContentForBody(event.content.bestDescription) }
|
||||||
}
|
}
|
||||||
is TimelineItemVoiceContent -> {
|
is TimelineItemVoiceContent -> {
|
||||||
content = { ContentForBody(textContent) }
|
content = { ContentForBody(textContent) }
|
||||||
|
|
@ -442,10 +442,10 @@ private fun EmojiButton(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SheetContentPreview(
|
internal fun ActionListViewContentPreview(
|
||||||
@PreviewParameter(ActionListStateProvider::class) state: ActionListState
|
@PreviewParameter(ActionListStateProvider::class) state: ActionListState
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
SheetContent(
|
ActionListViewContent(
|
||||||
state = state,
|
state = state,
|
||||||
onActionClick = {},
|
onActionClick = {},
|
||||||
onEmojiReactionClick = {},
|
onEmojiReactionClick = {},
|
||||||
|
|
|
||||||
|
|
@ -105,13 +105,13 @@ class IdentityChangeStatePresenter @Inject constructor(
|
||||||
|
|
||||||
private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember(
|
private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
disambiguatedDisplayName = disambiguatedDisplayName,
|
displayNameOrDefault = displayNameOrDefault,
|
||||||
avatarData = getAvatarData(AvatarSize.ComposerAlert),
|
avatarData = getAvatarData(AvatarSize.ComposerAlert),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember(
|
private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
disambiguatedDisplayName = userId.value,
|
displayNameOrDefault = userId.extractedDisplayName,
|
||||||
avatarData = AvatarData(
|
avatarData = AvatarData(
|
||||||
id = userId.value,
|
id = userId.value,
|
||||||
name = null,
|
name = null,
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,16 @@ class IdentityChangeStateProvider : PreviewParameterProvider<IdentityChangeState
|
||||||
anIdentityChangeState(),
|
anIdentityChangeState(),
|
||||||
anIdentityChangeState(
|
anIdentityChangeState(
|
||||||
roomMemberIdentityStateChanges = listOf(
|
roomMemberIdentityStateChanges = listOf(
|
||||||
RoomMemberIdentityStateChange(
|
aRoomMemberIdentityStateChange(
|
||||||
identityRoomMember = anIdentityRoomMember(disambiguatedDisplayName = "Alice"),
|
identityRoomMember = anIdentityRoomMember(),
|
||||||
|
identityState = IdentityState.PinViolation,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
anIdentityChangeState(
|
||||||
|
roomMemberIdentityStateChanges = listOf(
|
||||||
|
aRoomMemberIdentityStateChange(
|
||||||
|
identityRoomMember = anIdentityRoomMember(displayNameOrDefault = "Alice"),
|
||||||
identityState = IdentityState.PinViolation,
|
identityState = IdentityState.PinViolation,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -29,6 +37,14 @@ class IdentityChangeStateProvider : PreviewParameterProvider<IdentityChangeState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun aRoomMemberIdentityStateChange(
|
||||||
|
identityRoomMember: IdentityRoomMember = anIdentityRoomMember(),
|
||||||
|
identityState: IdentityState = IdentityState.PinViolation,
|
||||||
|
) = RoomMemberIdentityStateChange(
|
||||||
|
identityRoomMember = identityRoomMember,
|
||||||
|
identityState = identityState,
|
||||||
|
)
|
||||||
|
|
||||||
internal fun anIdentityChangeState(
|
internal fun anIdentityChangeState(
|
||||||
roomMemberIdentityStateChanges: List<RoomMemberIdentityStateChange> = emptyList(),
|
roomMemberIdentityStateChanges: List<RoomMemberIdentityStateChange> = emptyList(),
|
||||||
) = IdentityChangeState(
|
) = IdentityChangeState(
|
||||||
|
|
@ -38,7 +54,7 @@ internal fun anIdentityChangeState(
|
||||||
|
|
||||||
internal fun anIdentityRoomMember(
|
internal fun anIdentityRoomMember(
|
||||||
userId: UserId = UserId("@alice:example.com"),
|
userId: UserId = UserId("@alice:example.com"),
|
||||||
disambiguatedDisplayName: String = userId.value,
|
displayNameOrDefault: String = userId.extractedDisplayName,
|
||||||
avatarData: AvatarData = AvatarData(
|
avatarData: AvatarData = AvatarData(
|
||||||
id = userId.value,
|
id = userId.value,
|
||||||
name = null,
|
name = null,
|
||||||
|
|
@ -47,6 +63,6 @@ internal fun anIdentityRoomMember(
|
||||||
),
|
),
|
||||||
) = IdentityRoomMember(
|
) = IdentityRoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
disambiguatedDisplayName = disambiguatedDisplayName,
|
displayNameOrDefault = displayNameOrDefault,
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -40,13 +40,27 @@ fun IdentityChangeStateView(
|
||||||
avatar = pinViolationIdentityChange.identityRoomMember.avatarData,
|
avatar = pinViolationIdentityChange.identityRoomMember.avatarData,
|
||||||
content = buildAnnotatedString {
|
content = buildAnnotatedString {
|
||||||
val learnMoreStr = stringResource(CommonStrings.action_learn_more)
|
val learnMoreStr = stringResource(CommonStrings.action_learn_more)
|
||||||
|
val displayName = pinViolationIdentityChange.identityRoomMember.displayNameOrDefault
|
||||||
|
val userIdStr = stringResource(
|
||||||
|
CommonStrings.crypto_identity_change_pin_violation_new_user_id,
|
||||||
|
pinViolationIdentityChange.identityRoomMember.userId,
|
||||||
|
)
|
||||||
val fullText = stringResource(
|
val fullText = stringResource(
|
||||||
id = CommonStrings.crypto_identity_change_pin_violation,
|
id = CommonStrings.crypto_identity_change_pin_violation_new,
|
||||||
pinViolationIdentityChange.identityRoomMember.disambiguatedDisplayName,
|
displayName,
|
||||||
|
userIdStr,
|
||||||
learnMoreStr,
|
learnMoreStr,
|
||||||
)
|
)
|
||||||
val learnMoreStartIndex = fullText.indexOf(learnMoreStr)
|
|
||||||
append(fullText)
|
append(fullText)
|
||||||
|
val userIdStartIndex = fullText.indexOf(userIdStr)
|
||||||
|
addStyle(
|
||||||
|
style = SpanStyle(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
),
|
||||||
|
start = userIdStartIndex,
|
||||||
|
end = userIdStartIndex + userIdStr.length,
|
||||||
|
)
|
||||||
|
val learnMoreStartIndex = fullText.lastIndexOf(learnMoreStr)
|
||||||
addStyle(
|
addStyle(
|
||||||
style = SpanStyle(
|
style = SpanStyle(
|
||||||
textDecoration = TextDecoration.Underline,
|
textDecoration = TextDecoration.Underline,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,6 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
data class IdentityRoomMember(
|
data class IdentityRoomMember(
|
||||||
val userId: UserId,
|
val userId: UserId,
|
||||||
val disambiguatedDisplayName: String,
|
val displayNameOrDefault: String,
|
||||||
val avatarData: AvatarData,
|
val avatarData: AvatarData,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
|
||||||
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
|
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
|
||||||
import io.element.android.libraries.matrix.api.room.isDm
|
import io.element.android.libraries.matrix.api.room.isDm
|
||||||
import io.element.android.libraries.matrix.api.timeline.TimelineException
|
import io.element.android.libraries.matrix.api.timeline.TimelineException
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||||
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||||
|
|
@ -442,12 +443,11 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
intentionalMentions = message.intentionalMentions
|
intentionalMentions = message.intentionalMentions
|
||||||
)
|
)
|
||||||
is MessageComposerMode.Edit -> {
|
is MessageComposerMode.Edit -> {
|
||||||
val eventId = capturedMode.eventId
|
|
||||||
val transactionId = capturedMode.transactionId
|
|
||||||
timelineController.invokeOnCurrentTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
// First try to edit the message in the current timeline
|
// First try to edit the message in the current timeline
|
||||||
editMessage(eventId, transactionId, message.markdown, message.html, message.intentionalMentions)
|
editMessage(capturedMode.eventOrTransactionId, message.markdown, message.html, message.intentionalMentions)
|
||||||
.onFailure { cause ->
|
.onFailure { cause ->
|
||||||
|
val eventId = capturedMode.eventOrTransactionId.eventId
|
||||||
if (cause is TimelineException.EventNotFound && eventId != null) {
|
if (cause is TimelineException.EventNotFound && eventId != null) {
|
||||||
// if the event is not found in the timeline, try to edit the message directly
|
// if the event is not found in the timeline, try to edit the message directly
|
||||||
room.editMessage(eventId, message.markdown, message.html, message.intentionalMentions)
|
room.editMessage(eventId, message.markdown, message.html, message.intentionalMentions)
|
||||||
|
|
@ -581,8 +581,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
when (val draftType = draft.draftType) {
|
when (val draftType = draft.draftType) {
|
||||||
ComposerDraftType.NewMessage -> messageComposerContext.composerMode = MessageComposerMode.Normal
|
ComposerDraftType.NewMessage -> messageComposerContext.composerMode = MessageComposerMode.Normal
|
||||||
is ComposerDraftType.Edit -> messageComposerContext.composerMode = MessageComposerMode.Edit(
|
is ComposerDraftType.Edit -> messageComposerContext.composerMode = MessageComposerMode.Edit(
|
||||||
eventId = draftType.eventId,
|
eventOrTransactionId = draftType.eventId.toEventOrTransactionId(),
|
||||||
transactionId = null,
|
|
||||||
content = htmlText ?: markdownText
|
content = htmlText ?: markdownText
|
||||||
)
|
)
|
||||||
is ComposerDraftType.Reply -> {
|
is ComposerDraftType.Reply -> {
|
||||||
|
|
@ -611,7 +610,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
val draftType = when (val mode = messageComposerContext.composerMode) {
|
val draftType = when (val mode = messageComposerContext.composerMode) {
|
||||||
is MessageComposerMode.Normal -> ComposerDraftType.NewMessage
|
is MessageComposerMode.Normal -> ComposerDraftType.NewMessage
|
||||||
is MessageComposerMode.Edit -> {
|
is MessageComposerMode.Edit -> {
|
||||||
mode.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }
|
mode.eventOrTransactionId.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }
|
||||||
}
|
}
|
||||||
is MessageComposerMode.Reply -> ComposerDraftType.Reply(mode.eventId)
|
is MessageComposerMode.Reply -> ComposerDraftType.Reply(mode.eventId)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,10 +164,10 @@ internal fun aTimelineItemEvent(
|
||||||
groupPosition = groupPosition,
|
groupPosition = groupPosition,
|
||||||
localSendState = sendState,
|
localSendState = sendState,
|
||||||
inReplyTo = inReplyTo,
|
inReplyTo = inReplyTo,
|
||||||
debugInfoProvider = { debugInfo },
|
|
||||||
isThreaded = isThreaded,
|
isThreaded = isThreaded,
|
||||||
origin = null,
|
origin = null,
|
||||||
messageShield = messageShield,
|
timelineItemDebugInfoProvider = { debugInfo },
|
||||||
|
messageShieldProvider = { messageShield },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview {
|
||||||
// For consistency, ensure that there is a message in the timeline (the last one) with an error.
|
// For consistency, ensure that there is a message in the timeline (the last one) with an error.
|
||||||
val messageShield = aCriticalShield()
|
val messageShield = aCriticalShield()
|
||||||
val items = listOf(
|
val items = listOf(
|
||||||
(timelineItems.first() as TimelineItem.Event).copy(messageShield = messageShield)
|
(timelineItems.first() as TimelineItem.Event).copy(
|
||||||
|
messageShieldProvider = { messageShield },
|
||||||
|
)
|
||||||
) + timelineItems.drop(1)
|
) + timelineItems.drop(1)
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
|
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.CompositingStrategy
|
import androidx.compose.ui.graphics.CompositingStrategy
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||||
|
|
@ -101,6 +103,7 @@ fun MessageEventBubble(
|
||||||
val bubbleShape = bubbleShape()
|
val bubbleShape = bubbleShape()
|
||||||
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
||||||
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
||||||
|
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
|
|
@ -112,7 +115,7 @@ fun MessageEventBubble(
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
center = Offset(
|
center = Offset(
|
||||||
x = 0f,
|
x = if (isRtl) size.width else 0f,
|
||||||
y = yOffsetPx,
|
y = yOffsetPx,
|
||||||
),
|
),
|
||||||
radius = radiusPx,
|
radius = radiusPx,
|
||||||
|
|
@ -129,7 +132,9 @@ fun MessageEventBubble(
|
||||||
.testTag(TestTags.messageBubble)
|
.testTag(TestTags.messageBubble)
|
||||||
.widthIn(
|
.widthIn(
|
||||||
min = MIN_BUBBLE_WIDTH,
|
min = MIN_BUBBLE_WIDTH,
|
||||||
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO).toInt().toDp()
|
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO)
|
||||||
|
.toInt()
|
||||||
|
.toDp()
|
||||||
)
|
)
|
||||||
.clip(bubbleShape)
|
.clip(bubbleShape)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,10 @@ class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider<Timel
|
||||||
localSendState = LocalEventSendState.Failed.Unknown("AN_ERROR"),
|
localSendState = LocalEventSendState.Failed.Unknown("AN_ERROR"),
|
||||||
content = aTimelineItemTextContent().copy(isEdited = true),
|
content = aTimelineItemTextContent().copy(isEdited = true),
|
||||||
),
|
),
|
||||||
aTimelineItemEvent().copy(
|
aTimelineItemEvent(
|
||||||
messageShield = MessageShield.AuthenticityNotGuaranteed(isCritical = false),
|
messageShield = MessageShield.AuthenticityNotGuaranteed(isCritical = false),
|
||||||
),
|
),
|
||||||
aTimelineItemEvent().copy(
|
aTimelineItemEvent(
|
||||||
messageShield = MessageShield.UnknownDevice(isCritical = true),
|
messageShield = MessageShield.UnknownDevice(isCritical = true),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -301,6 +301,8 @@ private fun TimelineItemEventRowContent(
|
||||||
Modifier
|
Modifier
|
||||||
.constrainAs(sender) {
|
.constrainAs(sender) {
|
||||||
top.linkTo(parent.top)
|
top.linkTo(parent.top)
|
||||||
|
// Required for correct RTL layout
|
||||||
|
start.linkTo(parent.start)
|
||||||
}
|
}
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.zIndex(1f)
|
.zIndex(1f)
|
||||||
|
|
@ -629,7 +631,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
|
||||||
ATimelineItemEventRow(
|
ATimelineItemEventRow(
|
||||||
event = aTimelineItemEvent(
|
event = aTimelineItemEvent(
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
content = aTimelineItemImageContent().copy(
|
content = aTimelineItemImageContent(
|
||||||
aspectRatio = 2.5f
|
aspectRatio = 2.5f
|
||||||
),
|
),
|
||||||
groupPosition = TimelineItemGroupPosition.Last,
|
groupPosition = TimelineItemGroupPosition.Last,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ internal fun TimelineItemEventRowForDirectRoomPreview() = ElementPreview {
|
||||||
ATimelineItemEventRow(
|
ATimelineItemEventRow(
|
||||||
event = aTimelineItemEvent(
|
event = aTimelineItemEvent(
|
||||||
isMine = it,
|
isMine = it,
|
||||||
content = aTimelineItemImageContent().copy(
|
content = aTimelineItemImageContent(
|
||||||
aspectRatio = 5f
|
aspectRatio = 5f
|
||||||
),
|
),
|
||||||
groupPosition = TimelineItemGroupPosition.Last,
|
groupPosition = TimelineItemGroupPosition.Last,
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ internal fun TimelineItemEventRowShieldPreview() = ElementPreview {
|
||||||
ATimelineItemEventRow(
|
ATimelineItemEventRow(
|
||||||
event = aTimelineItemEvent(
|
event = aTimelineItemEvent(
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = aTimelineItemImageContent().copy(
|
content = aTimelineItemImageContent(
|
||||||
aspectRatio = 2.5f
|
aspectRatio = 2.5f
|
||||||
),
|
),
|
||||||
groupPosition = TimelineItemGroupPosition.Last,
|
groupPosition = TimelineItemGroupPosition.Last,
|
||||||
|
|
@ -54,7 +54,7 @@ internal fun TimelineItemEventRowShieldPreview() = ElementPreview {
|
||||||
)
|
)
|
||||||
ATimelineItemEventRow(
|
ATimelineItemEventRow(
|
||||||
event = aTimelineItemEvent(
|
event = aTimelineItemEvent(
|
||||||
content = aTimelineItemImageContent().copy(
|
content = aTimelineItemImageContent(
|
||||||
aspectRatio = 2.5f
|
aspectRatio = 2.5f
|
||||||
),
|
),
|
||||||
groupPosition = TimelineItemGroupPosition.Last,
|
groupPosition = TimelineItemGroupPosition.Last,
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(
|
||||||
event = aTimelineItemEvent(
|
event = aTimelineItemEvent(
|
||||||
isMine = it,
|
isMine = it,
|
||||||
timelineItemReactions = aTimelineItemReactions(count = 0),
|
timelineItemReactions = aTimelineItemReactions(count = 0),
|
||||||
content = aTimelineItemImageContent().copy(
|
content = aTimelineItemImageContent(
|
||||||
aspectRatio = 2.5f
|
aspectRatio = 2.5f
|
||||||
),
|
),
|
||||||
inReplyTo = inReplyToDetails,
|
inReplyTo = inReplyToDetails,
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@ import androidx.compose.ui.Modifier
|
||||||
import io.element.android.emojibasebindings.Emoji
|
import io.element.android.emojibasebindings.Emoji
|
||||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
import io.element.android.libraries.designsystem.theme.components.hide
|
import io.element.android.libraries.designsystem.theme.components.hide
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomReactionBottomSheet(
|
fun CustomReactionBottomSheet(
|
||||||
state: CustomReactionState,
|
state: CustomReactionState,
|
||||||
onSelectEmoji: (UniqueId, Emoji) -> Unit,
|
onSelectEmoji: (EventOrTransactionId, Emoji) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
|
|
@ -37,7 +37,7 @@ fun CustomReactionBottomSheet(
|
||||||
if (target?.event == null) return
|
if (target?.event == null) return
|
||||||
sheetState.hide(coroutineScope) {
|
sheetState.hide(coroutineScope) {
|
||||||
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||||
onSelectEmoji(target.event.id, emoji)
|
onSelectEmoji(target.event.eventOrTransactionId, emoji)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ fun TimelineItemAudioView(
|
||||||
Spacer(Modifier.width(spacing))
|
Spacer(Modifier.width(spacing))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = content.body,
|
text = content.bestDescription,
|
||||||
color = ElementTheme.materialColors.primary,
|
color = ElementTheme.materialColors.primary,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ fun TimelineItemFileView(
|
||||||
Spacer(Modifier.width(spacing))
|
Spacer(Modifier.width(spacing))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = content.body,
|
text = content.bestDescription,
|
||||||
color = ElementTheme.materialColors.primary,
|
color = ElementTheme.materialColors.primary,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ fun TimelineItemImageView(
|
||||||
model = MediaRequestData(
|
model = MediaRequestData(
|
||||||
source = content.preferredMediaSource,
|
source = content.preferredMediaSource,
|
||||||
kind = MediaRequestData.Kind.File(
|
kind = MediaRequestData.Kind.File(
|
||||||
body = content.filename ?: content.body,
|
fileName = content.filename,
|
||||||
mimeType = content.mimeType,
|
mimeType = content.mimeType,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -108,7 +108,9 @@ fun TimelineItemImageView(
|
||||||
val caption = if (LocalInspectionMode.current) {
|
val caption = if (LocalInspectionMode.current) {
|
||||||
SpannedString(content.caption)
|
SpannedString(content.caption)
|
||||||
} else {
|
} else {
|
||||||
content.formatted?.body?.takeIf { content.formatted.format == MessageFormat.HTML } ?: SpannedString(content.caption)
|
content.formattedCaption?.body
|
||||||
|
?.takeIf { content.formattedCaption.format == MessageFormat.HTML }
|
||||||
|
?: SpannedString(content.caption)
|
||||||
}
|
}
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||||
|
|
@ -158,9 +160,9 @@ internal fun TimelineImageWithCaptionRowPreview() = ElementPreview {
|
||||||
ATimelineItemEventRow(
|
ATimelineItemEventRow(
|
||||||
event = aTimelineItemEvent(
|
event = aTimelineItemEvent(
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
content = aTimelineItemImageContent().copy(
|
content = aTimelineItemImageContent(
|
||||||
filename = "image.jpg",
|
filename = "image.jpg",
|
||||||
body = "A long caption that may wrap into several lines",
|
caption = "A long caption that may wrap into several lines",
|
||||||
aspectRatio = 2.5f,
|
aspectRatio = 2.5f,
|
||||||
),
|
),
|
||||||
groupPosition = TimelineItemGroupPosition.Last,
|
groupPosition = TimelineItemGroupPosition.Last,
|
||||||
|
|
@ -170,9 +172,9 @@ internal fun TimelineImageWithCaptionRowPreview() = ElementPreview {
|
||||||
ATimelineItemEventRow(
|
ATimelineItemEventRow(
|
||||||
event = aTimelineItemEvent(
|
event = aTimelineItemEvent(
|
||||||
isMine = false,
|
isMine = false,
|
||||||
content = aTimelineItemImageContent().copy(
|
content = aTimelineItemImageContent(
|
||||||
filename = "image.jpg",
|
filename = "image.jpg",
|
||||||
body = "Image with null aspectRatio",
|
caption = "Image with null aspectRatio",
|
||||||
aspectRatio = null,
|
aspectRatio = null,
|
||||||
),
|
),
|
||||||
groupPosition = TimelineItemGroupPosition.Last,
|
groupPosition = TimelineItemGroupPosition.Last,
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ fun TimelineItemStickerView(
|
||||||
onShowClick: () -> Unit,
|
onShowClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val description = content.body.takeIf { it.isNotEmpty() } ?: stringResource(CommonStrings.common_image)
|
val description = content.bestDescription.takeIf { it.isNotEmpty() } ?: stringResource(CommonStrings.common_image)
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.semantics { contentDescription = description },
|
modifier = modifier.semantics { contentDescription = description },
|
||||||
) {
|
) {
|
||||||
|
|
@ -65,7 +65,7 @@ fun TimelineItemStickerView(
|
||||||
model = MediaRequestData(
|
model = MediaRequestData(
|
||||||
source = content.preferredMediaSource,
|
source = content.preferredMediaSource,
|
||||||
kind = MediaRequestData.Kind.File(
|
kind = MediaRequestData.Kind.File(
|
||||||
body = content.body,
|
fileName = content.filename,
|
||||||
mimeType = content.mimeType,
|
mimeType = content.mimeType,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,8 @@ fun TimelineItemVideoView(
|
||||||
) {
|
) {
|
||||||
val containerModifier = if (content.showCaption) {
|
val containerModifier = if (content.showCaption) {
|
||||||
Modifier
|
Modifier
|
||||||
.padding(top = 6.dp)
|
.padding(top = 6.dp)
|
||||||
.clip(RoundedCornerShape(6.dp))
|
.clip(RoundedCornerShape(6.dp))
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
|
|
@ -93,12 +93,12 @@ fun TimelineItemVideoView(
|
||||||
var isLoaded by remember { mutableStateOf(false) }
|
var isLoaded by remember { mutableStateOf(false) }
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
||||||
model = MediaRequestData(
|
model = MediaRequestData(
|
||||||
source = content.thumbnailSource,
|
source = content.thumbnailSource,
|
||||||
kind = MediaRequestData.Kind.File(
|
kind = MediaRequestData.Kind.File(
|
||||||
body = content.filename ?: content.body,
|
fileName = content.filename,
|
||||||
mimeType = content.mimeType
|
mimeType = content.mimeType
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -126,7 +126,9 @@ fun TimelineItemVideoView(
|
||||||
val caption = if (LocalInspectionMode.current) {
|
val caption = if (LocalInspectionMode.current) {
|
||||||
SpannedString(content.caption)
|
SpannedString(content.caption)
|
||||||
} else {
|
} else {
|
||||||
content.formatted?.body?.takeIf { content.formatted.format == MessageFormat.HTML } ?: SpannedString(content.caption)
|
content.formattedCaption?.body
|
||||||
|
?.takeIf { content.formattedCaption.format == MessageFormat.HTML }
|
||||||
|
?: SpannedString(content.caption)
|
||||||
}
|
}
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||||
|
|
@ -178,7 +180,7 @@ internal fun TimelineVideoWithCaptionRowPreview() = ElementPreview {
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
content = aTimelineItemVideoContent().copy(
|
content = aTimelineItemVideoContent().copy(
|
||||||
filename = "video.mp4",
|
filename = "video.mp4",
|
||||||
body = "A long caption that may wrap into several lines",
|
caption = "A long caption that may wrap into several lines",
|
||||||
aspectRatio = 2.5f,
|
aspectRatio = 2.5f,
|
||||||
),
|
),
|
||||||
groupPosition = TimelineItemGroupPosition.Last,
|
groupPosition = TimelineItemGroupPosition.Last,
|
||||||
|
|
@ -190,7 +192,7 @@ internal fun TimelineVideoWithCaptionRowPreview() = ElementPreview {
|
||||||
isMine = false,
|
isMine = false,
|
||||||
content = aTimelineItemVideoContent().copy(
|
content = aTimelineItemVideoContent().copy(
|
||||||
filename = "video.mp4",
|
filename = "video.mp4",
|
||||||
body = "Video with null aspect ratio",
|
caption = "Video with null aspect ratio",
|
||||||
aspectRatio = null,
|
aspectRatio = null,
|
||||||
),
|
),
|
||||||
groupPosition = TimelineItemGroupPosition.Last,
|
groupPosition = TimelineItemGroupPosition.Last,
|
||||||
|
|
|
||||||
|
|
@ -89,14 +89,14 @@ fun ReactionSummaryView(
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
SheetContent(summary = state.target)
|
ReactionSummaryViewContent(summary = state.target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SheetContent(
|
private fun ReactionSummaryViewContent(
|
||||||
summary: ReactionSummaryState.Summary,
|
summary: ReactionSummaryState.Summary,
|
||||||
) {
|
) {
|
||||||
val animationScope = rememberCoroutineScope()
|
val animationScope = rememberCoroutineScope()
|
||||||
|
|
@ -274,8 +274,8 @@ private fun SenderRow(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SheetContentPreview(
|
internal fun ReactionSummaryViewContentPreview(
|
||||||
@PreviewParameter(ReactionSummaryStateProvider::class) state: ReactionSummaryState
|
@PreviewParameter(ReactionSummaryStateProvider::class) state: ReactionSummaryState
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
SheetContent(summary = state.target as ReactionSummaryState.Summary)
|
ReactionSummaryViewContent(summary = state.target as ReactionSummaryState.Summary)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,9 +84,9 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
is ImageMessageType -> {
|
is ImageMessageType -> {
|
||||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||||
TimelineItemImageContent(
|
TimelineItemImageContent(
|
||||||
body = messageType.body.trimEnd(),
|
|
||||||
formatted = messageType.formatted,
|
|
||||||
filename = messageType.filename,
|
filename = messageType.filename,
|
||||||
|
caption = messageType.caption?.trimEnd(),
|
||||||
|
formattedCaption = messageType.formattedCaption,
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
thumbnailSource = messageType.info?.thumbnailSource,
|
thumbnailSource = messageType.info?.thumbnailSource,
|
||||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||||
|
|
@ -95,13 +95,15 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
height = messageType.info?.height?.toInt(),
|
height = messageType.info?.height?.toInt(),
|
||||||
aspectRatio = aspectRatio,
|
aspectRatio = aspectRatio,
|
||||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||||
fileExtension = messageType.filename?.let { fileExtensionExtractor.extractFromName(it) }.orEmpty()
|
fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is StickerMessageType -> {
|
is StickerMessageType -> {
|
||||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||||
TimelineItemStickerContent(
|
TimelineItemStickerContent(
|
||||||
body = messageType.body.trimEnd(),
|
filename = messageType.filename,
|
||||||
|
caption = messageType.caption?.trimEnd(),
|
||||||
|
formattedCaption = messageType.formattedCaption,
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
thumbnailSource = messageType.info?.thumbnailSource,
|
thumbnailSource = messageType.info?.thumbnailSource,
|
||||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||||
|
|
@ -110,7 +112,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
height = messageType.info?.height?.toInt(),
|
height = messageType.info?.height?.toInt(),
|
||||||
aspectRatio = aspectRatio,
|
aspectRatio = aspectRatio,
|
||||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
|
fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is LocationMessageType -> {
|
is LocationMessageType -> {
|
||||||
|
|
@ -136,9 +138,9 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
is VideoMessageType -> {
|
is VideoMessageType -> {
|
||||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||||
TimelineItemVideoContent(
|
TimelineItemVideoContent(
|
||||||
body = messageType.body.trimEnd(),
|
|
||||||
formatted = messageType.formatted,
|
|
||||||
filename = messageType.filename,
|
filename = messageType.filename,
|
||||||
|
caption = messageType.caption?.trimEnd(),
|
||||||
|
formattedCaption = messageType.formattedCaption,
|
||||||
thumbnailSource = messageType.info?.thumbnailSource,
|
thumbnailSource = messageType.info?.thumbnailSource,
|
||||||
videoSource = messageType.source,
|
videoSource = messageType.source,
|
||||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||||
|
|
@ -148,17 +150,19 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
blurHash = messageType.info?.blurhash,
|
blurHash = messageType.info?.blurhash,
|
||||||
aspectRatio = aspectRatio,
|
aspectRatio = aspectRatio,
|
||||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||||
fileExtension = messageType.filename?.let { fileExtensionExtractor.extractFromName(it) }.orEmpty(),
|
fileExtension = fileExtensionExtractor.extractFromName(messageType.filename),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AudioMessageType -> {
|
is AudioMessageType -> {
|
||||||
TimelineItemAudioContent(
|
TimelineItemAudioContent(
|
||||||
body = messageType.body.trimEnd(),
|
filename = messageType.filename,
|
||||||
|
caption = messageType.caption?.trimEnd(),
|
||||||
|
formattedCaption = messageType.formattedCaption,
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body),
|
fileExtension = fileExtensionExtractor.extractFromName(messageType.filename),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is VoiceMessageType -> {
|
is VoiceMessageType -> {
|
||||||
|
|
@ -166,7 +170,9 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
true -> {
|
true -> {
|
||||||
TimelineItemVoiceContent(
|
TimelineItemVoiceContent(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
body = messageType.body.trimEnd(),
|
filename = messageType.filename,
|
||||||
|
caption = messageType.caption?.trimEnd(),
|
||||||
|
formattedCaption = messageType.formattedCaption,
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||||
|
|
@ -175,20 +181,24 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
false -> {
|
false -> {
|
||||||
TimelineItemAudioContent(
|
TimelineItemAudioContent(
|
||||||
body = messageType.body.trimEnd(),
|
filename = messageType.filename,
|
||||||
|
caption = messageType.caption?.trimEnd(),
|
||||||
|
formattedCaption = messageType.formattedCaption,
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||||
fileExtension = fileExtensionExtractor.extractFromName(messageType.body),
|
fileExtension = fileExtensionExtractor.extractFromName(messageType.filename),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is FileMessageType -> {
|
is FileMessageType -> {
|
||||||
val fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
|
val fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
|
||||||
TimelineItemFileContent(
|
TimelineItemFileContent(
|
||||||
body = messageType.body.trimEnd(),
|
filename = messageType.filename,
|
||||||
|
caption = messageType.caption?.trimEnd(),
|
||||||
|
formattedCaption = messageType.formattedCaption,
|
||||||
thumbnailSource = messageType.info?.thumbnailSource,
|
thumbnailSource = messageType.info?.thumbnailSource,
|
||||||
fileSource = messageType.source,
|
fileSource = messageType.source,
|
||||||
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
|
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ class TimelineItemContentStickerFactory @Inject constructor(
|
||||||
val aspectRatio = aspectRatioOf(content.info.width, content.info.height)
|
val aspectRatio = aspectRatioOf(content.info.width, content.info.height)
|
||||||
|
|
||||||
return TimelineItemStickerContent(
|
return TimelineItemStickerContent(
|
||||||
body = content.body,
|
filename = content.filename,
|
||||||
|
caption = content.body,
|
||||||
|
formattedCaption = null,
|
||||||
mediaSource = content.source,
|
mediaSource = content.source,
|
||||||
thumbnailSource = content.info.thumbnailSource,
|
thumbnailSource = content.info.thumbnailSource,
|
||||||
mimeType = content.info.mimetype ?: MimeTypes.OctetStream,
|
mimeType = content.info.mimetype ?: MimeTypes.OctetStream,
|
||||||
|
|
@ -42,7 +44,7 @@ class TimelineItemContentStickerFactory @Inject constructor(
|
||||||
height = content.info.height?.toInt(),
|
height = content.info.height?.toInt(),
|
||||||
aspectRatio = aspectRatio,
|
aspectRatio = aspectRatio,
|
||||||
formattedFileSize = fileSizeFormatter.format(content.info.size ?: 0),
|
formattedFileSize = fileSizeFormatter.format(content.info.size ?: 0),
|
||||||
fileExtension = fileExtensionExtractor.extractFromName(content.body)
|
fileExtension = fileExtensionExtractor.extractFromName(content.filename)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,9 +85,9 @@ class TimelineItemEventFactory @AssistedInject constructor(
|
||||||
localSendState = currentTimelineItem.event.localSendState,
|
localSendState = currentTimelineItem.event.localSendState,
|
||||||
inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser),
|
inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser),
|
||||||
isThreaded = currentTimelineItem.event.isThreaded(),
|
isThreaded = currentTimelineItem.event.isThreaded(),
|
||||||
debugInfoProvider = currentTimelineItem.event.debugInfoProvider,
|
|
||||||
origin = currentTimelineItem.event.origin,
|
origin = currentTimelineItem.event.origin,
|
||||||
messageShield = currentTimelineItem.event.messageShieldProvider.getShield(strict = false)
|
timelineItemDebugInfoProvider = currentTimelineItem.event.timelineItemDebugInfoProvider,
|
||||||
|
messageShieldProvider = currentTimelineItem.event.messageShieldProvider,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,13 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventDebugInfoProvider
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShieldProvider
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemDebugInfoProvider
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||||
|
|
@ -74,9 +77,9 @@ sealed interface TimelineItem {
|
||||||
val localSendState: LocalEventSendState?,
|
val localSendState: LocalEventSendState?,
|
||||||
val inReplyTo: InReplyToDetails?,
|
val inReplyTo: InReplyToDetails?,
|
||||||
val isThreaded: Boolean,
|
val isThreaded: Boolean,
|
||||||
val debugInfoProvider: EventDebugInfoProvider,
|
|
||||||
val origin: TimelineItemEventOrigin?,
|
val origin: TimelineItemEventOrigin?,
|
||||||
val messageShield: MessageShield?,
|
val timelineItemDebugInfoProvider: TimelineItemDebugInfoProvider,
|
||||||
|
val messageShieldProvider: MessageShieldProvider,
|
||||||
) : TimelineItem {
|
) : TimelineItem {
|
||||||
val showSenderInformation = groupPosition.isNew() && !isMine
|
val showSenderInformation = groupPosition.isNew() && !isMine
|
||||||
|
|
||||||
|
|
@ -90,7 +93,14 @@ sealed interface TimelineItem {
|
||||||
|
|
||||||
val isRemote = eventId != null
|
val isRemote = eventId != null
|
||||||
|
|
||||||
val debugInfo = debugInfoProvider.get()
|
val eventOrTransactionId: EventOrTransactionId
|
||||||
|
get() = EventOrTransactionId.from(eventId = eventId, transactionId = transactionId)
|
||||||
|
|
||||||
|
// No need to be lazy here?
|
||||||
|
val messageShield: MessageShield? = messageShieldProvider(strict = false)
|
||||||
|
|
||||||
|
val debugInfo: TimelineItemDebugInfo
|
||||||
|
get() = timelineItemDebugInfoProvider()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,20 @@
|
||||||
package io.element.android.features.messages.impl.timeline.model.event
|
package io.element.android.features.messages.impl.timeline.model.event
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||||
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
|
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
data class TimelineItemAudioContent(
|
data class TimelineItemAudioContent(
|
||||||
val body: String,
|
override val filename: String,
|
||||||
|
override val caption: String?,
|
||||||
|
override val formattedCaption: FormattedBody?,
|
||||||
val duration: Duration,
|
val duration: Duration,
|
||||||
val mediaSource: MediaSource,
|
val mediaSource: MediaSource,
|
||||||
val mimeType: String,
|
val mimeType: String,
|
||||||
val formattedFileSize: String,
|
val formattedFileSize: String,
|
||||||
val fileExtension: String,
|
val fileExtension: String,
|
||||||
) : TimelineItemEventContent {
|
) : TimelineItemEventContentWithAttachment {
|
||||||
val fileExtensionAndSize =
|
val fileExtensionAndSize =
|
||||||
formatFileExtensionAndSize(
|
formatFileExtensionAndSize(
|
||||||
fileExtension,
|
fileExtension,
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ open class TimelineItemAudioContentProvider : PreviewParameterProvider<TimelineI
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aTimelineItemAudioContent(fileName: String = "A sound.mp3") = TimelineItemAudioContent(
|
fun aTimelineItemAudioContent(fileName: String = "A sound.mp3") = TimelineItemAudioContent(
|
||||||
body = fileName,
|
filename = fileName,
|
||||||
mimeType = MimeTypes.Pdf,
|
caption = null,
|
||||||
|
formattedCaption = null,
|
||||||
|
mimeType = MimeTypes.Mp3,
|
||||||
formattedFileSize = "100kB",
|
formattedFileSize = "100kB",
|
||||||
fileExtension = "mp3",
|
fileExtension = "mp3",
|
||||||
duration = 100.milliseconds,
|
duration = 100.milliseconds,
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,23 @@
|
||||||
package io.element.android.features.messages.impl.timeline.model.event
|
package io.element.android.features.messages.impl.timeline.model.event
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface TimelineItemEventContent {
|
sealed interface TimelineItemEventContent {
|
||||||
val type: String
|
val type: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface TimelineItemEventContentWithAttachment : TimelineItemEventContent {
|
||||||
|
val filename: String
|
||||||
|
val caption: String?
|
||||||
|
val formattedCaption: FormattedBody?
|
||||||
|
|
||||||
|
val bestDescription: String
|
||||||
|
get() = caption ?: filename
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only text based content can be copied.
|
* Only text based content can be copied.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,19 @@
|
||||||
package io.element.android.features.messages.impl.timeline.model.event
|
package io.element.android.features.messages.impl.timeline.model.event
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||||
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
|
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
|
||||||
|
|
||||||
data class TimelineItemFileContent(
|
data class TimelineItemFileContent(
|
||||||
val body: String,
|
override val filename: String,
|
||||||
|
override val caption: String?,
|
||||||
|
override val formattedCaption: FormattedBody?,
|
||||||
val fileSource: MediaSource,
|
val fileSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
val formattedFileSize: String,
|
val formattedFileSize: String,
|
||||||
val fileExtension: String,
|
val fileExtension: String,
|
||||||
val mimeType: String,
|
val mimeType: String,
|
||||||
) : TimelineItemEventContent {
|
) : TimelineItemEventContentWithAttachment {
|
||||||
override val type: String = "TimelineItemFileContent"
|
override val type: String = "TimelineItemFileContent"
|
||||||
|
|
||||||
val fileExtensionAndSize = formatFileExtensionAndSize(fileExtension, formattedFileSize)
|
val fileExtensionAndSize = formatFileExtensionAndSize(fileExtension, formattedFileSize)
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,12 @@ open class TimelineItemFileContentProvider : PreviewParameterProvider<TimelineIt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aTimelineItemFileContent(fileName: String = "A file.pdf") = TimelineItemFileContent(
|
fun aTimelineItemFileContent(
|
||||||
body = fileName,
|
fileName: String = "A file.pdf",
|
||||||
|
) = TimelineItemFileContent(
|
||||||
|
filename = fileName,
|
||||||
|
caption = null,
|
||||||
|
formattedCaption = null,
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
fileSource = MediaSource(url = ""),
|
fileSource = MediaSource(url = ""),
|
||||||
mimeType = MimeTypes.Pdf,
|
mimeType = MimeTypes.Pdf,
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||||
|
|
||||||
data class TimelineItemImageContent(
|
data class TimelineItemImageContent(
|
||||||
val body: String,
|
override val filename: String,
|
||||||
val formatted: FormattedBody?,
|
override val caption: String?,
|
||||||
val filename: String?,
|
override val formattedCaption: FormattedBody?,
|
||||||
val mediaSource: MediaSource,
|
val mediaSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
val formattedFileSize: String,
|
val formattedFileSize: String,
|
||||||
|
|
@ -24,11 +24,10 @@ data class TimelineItemImageContent(
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
val height: Int?,
|
val height: Int?,
|
||||||
val aspectRatio: Float?
|
val aspectRatio: Float?
|
||||||
) : TimelineItemEventContent {
|
) : TimelineItemEventContentWithAttachment {
|
||||||
override val type: String = "TimelineItemImageContent"
|
override val type: String = "TimelineItemImageContent"
|
||||||
|
|
||||||
val showCaption = filename != null && filename != body
|
val showCaption = caption != null
|
||||||
val caption = if (showCaption) body else ""
|
|
||||||
|
|
||||||
val preferredMediaSource = if (mimeType == MimeTypes.Gif) {
|
val preferredMediaSource = if (mimeType == MimeTypes.Gif) {
|
||||||
mediaSource
|
mediaSource
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,14 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineI
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aTimelineItemImageContent(
|
fun aTimelineItemImageContent(
|
||||||
aspectRatio: Float = 0.5f,
|
aspectRatio: Float? = 0.5f,
|
||||||
blurhash: String? = A_BLUR_HASH,
|
blurhash: String? = A_BLUR_HASH,
|
||||||
|
filename: String = "A picture.jpg",
|
||||||
|
caption: String? = null,
|
||||||
) = TimelineItemImageContent(
|
) = TimelineItemImageContent(
|
||||||
body = "a body",
|
filename = filename,
|
||||||
formatted = null,
|
caption = caption,
|
||||||
filename = null,
|
formattedCaption = null,
|
||||||
mediaSource = MediaSource(""),
|
mediaSource = MediaSource(""),
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
mimeType = MimeTypes.IMAGE_JPEG,
|
mimeType = MimeTypes.IMAGE_JPEG,
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,12 @@
|
||||||
package io.element.android.features.messages.impl.timeline.model.event
|
package io.element.android.features.messages.impl.timeline.model.event
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||||
|
|
||||||
data class TimelineItemStickerContent(
|
data class TimelineItemStickerContent(
|
||||||
val body: String,
|
override val filename: String,
|
||||||
|
override val caption: String?,
|
||||||
|
override val formattedCaption: FormattedBody?,
|
||||||
val mediaSource: MediaSource,
|
val mediaSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
val formattedFileSize: String,
|
val formattedFileSize: String,
|
||||||
|
|
@ -20,7 +23,7 @@ data class TimelineItemStickerContent(
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
val height: Int?,
|
val height: Int?,
|
||||||
val aspectRatio: Float?
|
val aspectRatio: Float?
|
||||||
) : TimelineItemEventContent {
|
) : TimelineItemEventContentWithAttachment {
|
||||||
override val type: String = "TimelineItemStickerContent"
|
override val type: String = "TimelineItemStickerContent"
|
||||||
|
|
||||||
/* Stickers are supposed to be small images so
|
/* Stickers are supposed to be small images so
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ fun aTimelineItemStickerContent(
|
||||||
aspectRatio: Float = 0.5f,
|
aspectRatio: Float = 0.5f,
|
||||||
blurhash: String? = A_BLUR_HASH,
|
blurhash: String? = A_BLUR_HASH,
|
||||||
) = TimelineItemStickerContent(
|
) = TimelineItemStickerContent(
|
||||||
body = "a body",
|
filename = "a sticker.gif",
|
||||||
|
caption = "a body",
|
||||||
|
formattedCaption = null,
|
||||||
mediaSource = MediaSource(""),
|
mediaSource = MediaSource(""),
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
mimeType = MimeTypes.IMAGE_JPEG,
|
mimeType = MimeTypes.IMAGE_JPEG,
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
data class TimelineItemVideoContent(
|
data class TimelineItemVideoContent(
|
||||||
val body: String,
|
override val filename: String,
|
||||||
val formatted: FormattedBody?,
|
override val caption: String?,
|
||||||
val filename: String?,
|
override val formattedCaption: FormattedBody?,
|
||||||
val duration: Duration,
|
val duration: Duration,
|
||||||
val videoSource: MediaSource,
|
val videoSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
|
|
@ -25,9 +25,8 @@ data class TimelineItemVideoContent(
|
||||||
val mimeType: String,
|
val mimeType: String,
|
||||||
val formattedFileSize: String,
|
val formattedFileSize: String,
|
||||||
val fileExtension: String,
|
val fileExtension: String,
|
||||||
) : TimelineItemEventContent {
|
) : TimelineItemEventContentWithAttachment {
|
||||||
override val type: String = "TimelineItemImageContent"
|
override val type: String = "TimelineItemImageContent"
|
||||||
|
|
||||||
val showCaption = filename != null && filename != body
|
val showCaption = caption != null
|
||||||
val caption = if (showCaption) body else ""
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,9 @@ fun aTimelineItemVideoContent(
|
||||||
aspectRatio: Float = 0.5f,
|
aspectRatio: Float = 0.5f,
|
||||||
blurhash: String? = A_BLUR_HASH,
|
blurhash: String? = A_BLUR_HASH,
|
||||||
) = TimelineItemVideoContent(
|
) = TimelineItemVideoContent(
|
||||||
body = "Video.mp4",
|
filename = "Video.mp4",
|
||||||
formatted = null,
|
caption = null,
|
||||||
filename = null,
|
formattedCaption = null,
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
blurHash = blurhash,
|
blurHash = blurhash,
|
||||||
aspectRatio = aspectRatio,
|
aspectRatio = aspectRatio,
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,19 @@ package io.element.android.features.messages.impl.timeline.model.event
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
data class TimelineItemVoiceContent(
|
data class TimelineItemVoiceContent(
|
||||||
val eventId: EventId?,
|
val eventId: EventId?,
|
||||||
val body: String,
|
override val filename: String,
|
||||||
|
override val caption: String?,
|
||||||
|
override val formattedCaption: FormattedBody?,
|
||||||
val duration: Duration,
|
val duration: Duration,
|
||||||
val mediaSource: MediaSource,
|
val mediaSource: MediaSource,
|
||||||
val mimeType: String,
|
val mimeType: String,
|
||||||
val waveform: ImmutableList<Float>,
|
val waveform: ImmutableList<Float>,
|
||||||
) : TimelineItemEventContent {
|
) : TimelineItemEventContentWithAttachment {
|
||||||
override val type: String = "TimelineItemAudioContent"
|
override val type: String = "TimelineItemAudioContent"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,17 +35,21 @@ open class TimelineItemVoiceContentProvider : PreviewParameterProvider<TimelineI
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aTimelineItemVoiceContent(
|
fun aTimelineItemVoiceContent(
|
||||||
eventId: String? = "\$anEventId",
|
eventId: EventId? = EventId("\$anEventId"),
|
||||||
body: String = "body doesn't really matter for a voice message",
|
filename: String = "filename doesn't really matter for a voice message",
|
||||||
|
caption: String? = "body doesn't really matter for a voice message",
|
||||||
duration: Duration = 61_000.milliseconds,
|
duration: Duration = 61_000.milliseconds,
|
||||||
contentUri: String = "mxc://matrix.org/1234567890abcdefg",
|
contentUri: String = "mxc://matrix.org/1234567890abcdefg",
|
||||||
mimeType: String = MimeTypes.Ogg,
|
mimeType: String = MimeTypes.Ogg,
|
||||||
|
mediaSource: MediaSource = MediaSource(contentUri),
|
||||||
waveform: List<Float> = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f),
|
waveform: List<Float> = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f),
|
||||||
) = TimelineItemVoiceContent(
|
) = TimelineItemVoiceContent(
|
||||||
eventId = eventId?.let { EventId(it) },
|
eventId = eventId,
|
||||||
body = body,
|
filename = filename,
|
||||||
|
caption = caption,
|
||||||
|
formattedCaption = null,
|
||||||
duration = duration,
|
duration = duration,
|
||||||
mediaSource = MediaSource(contentUri),
|
mediaSource = mediaSource,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
waveform = waveform.toPersistentList(),
|
waveform = waveform.toPersistentList(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,12 @@ interface VoiceMessageMediaRepo {
|
||||||
*
|
*
|
||||||
* @param mediaSource the media source of the voice message.
|
* @param mediaSource the media source of the voice message.
|
||||||
* @param mimeType the mime type of the voice message.
|
* @param mimeType the mime type of the voice message.
|
||||||
* @param body the body of the voice message.
|
* @param filename the filename of the voice message.
|
||||||
*/
|
*/
|
||||||
fun create(
|
fun create(
|
||||||
mediaSource: MediaSource,
|
mediaSource: MediaSource,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
body: String?,
|
filename: String?,
|
||||||
): VoiceMessageMediaRepo
|
): VoiceMessageMediaRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
|
||||||
private val matrixMediaLoader: MatrixMediaLoader,
|
private val matrixMediaLoader: MatrixMediaLoader,
|
||||||
@Assisted private val mediaSource: MediaSource,
|
@Assisted private val mediaSource: MediaSource,
|
||||||
@Assisted("mimeType") private val mimeType: String?,
|
@Assisted("mimeType") private val mimeType: String?,
|
||||||
@Assisted("body") private val body: String?,
|
@Assisted("filename") private val filename: String?,
|
||||||
) : VoiceMessageMediaRepo {
|
) : VoiceMessageMediaRepo {
|
||||||
@ContributesBinding(RoomScope::class)
|
@ContributesBinding(RoomScope::class)
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -69,7 +69,7 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
|
||||||
override fun create(
|
override fun create(
|
||||||
mediaSource: MediaSource,
|
mediaSource: MediaSource,
|
||||||
@Assisted("mimeType") mimeType: String?,
|
@Assisted("mimeType") mimeType: String?,
|
||||||
@Assisted("body") body: String?,
|
@Assisted("filename") filename: String?,
|
||||||
): DefaultVoiceMessageMediaRepo
|
): DefaultVoiceMessageMediaRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
|
||||||
else -> matrixMediaLoader.downloadMediaFile(
|
else -> matrixMediaLoader.downloadMediaFile(
|
||||||
source = mediaSource,
|
source = mediaSource,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
body = body,
|
filename = filename,
|
||||||
).mapCatching {
|
).mapCatching {
|
||||||
it.use { mediaFile ->
|
it.use { mediaFile ->
|
||||||
val dest = cachedFile.apply { parentFile?.mkdirs() }
|
val dest = cachedFile.apply { parentFile?.mkdirs() }
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,13 @@ interface VoiceMessagePlayer {
|
||||||
* @param eventId The eventId of the voice message event.
|
* @param eventId The eventId of the voice message event.
|
||||||
* @param mediaSource The media source of the voice message.
|
* @param mediaSource The media source of the voice message.
|
||||||
* @param mimeType The mime type of the voice message.
|
* @param mimeType The mime type of the voice message.
|
||||||
* @param body The body of the voice message.
|
* @param filename The filename of the voice message.
|
||||||
*/
|
*/
|
||||||
fun create(
|
fun create(
|
||||||
eventId: EventId?,
|
eventId: EventId?,
|
||||||
mediaSource: MediaSource,
|
mediaSource: MediaSource,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
body: String?,
|
filename: String?,
|
||||||
): VoiceMessagePlayer
|
): VoiceMessagePlayer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,7 +113,7 @@ class DefaultVoiceMessagePlayer(
|
||||||
private val eventId: EventId?,
|
private val eventId: EventId?,
|
||||||
mediaSource: MediaSource,
|
mediaSource: MediaSource,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
body: String?,
|
filename: String?,
|
||||||
) : VoiceMessagePlayer {
|
) : VoiceMessagePlayer {
|
||||||
@ContributesBinding(RoomScope::class) // Scoped types can't use @AssistedInject.
|
@ContributesBinding(RoomScope::class) // Scoped types can't use @AssistedInject.
|
||||||
class Factory @Inject constructor(
|
class Factory @Inject constructor(
|
||||||
|
|
@ -124,21 +124,21 @@ class DefaultVoiceMessagePlayer(
|
||||||
eventId: EventId?,
|
eventId: EventId?,
|
||||||
mediaSource: MediaSource,
|
mediaSource: MediaSource,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
body: String?,
|
filename: String?,
|
||||||
): DefaultVoiceMessagePlayer = DefaultVoiceMessagePlayer(
|
): DefaultVoiceMessagePlayer = DefaultVoiceMessagePlayer(
|
||||||
mediaPlayer = mediaPlayer,
|
mediaPlayer = mediaPlayer,
|
||||||
voiceMessageMediaRepoFactory = voiceMessageMediaRepoFactory,
|
voiceMessageMediaRepoFactory = voiceMessageMediaRepoFactory,
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
mediaSource = mediaSource,
|
mediaSource = mediaSource,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
body = body,
|
filename = filename,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val repo = voiceMessageMediaRepoFactory.create(
|
private val repo = voiceMessageMediaRepoFactory.create(
|
||||||
mediaSource = mediaSource,
|
mediaSource = mediaSource,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
body = body
|
filename = filename,
|
||||||
)
|
)
|
||||||
|
|
||||||
private var internalState = MutableStateFlow(
|
private var internalState = MutableStateFlow(
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class VoiceMessagePresenter @AssistedInject constructor(
|
||||||
eventId = content.eventId,
|
eventId = content.eventId,
|
||||||
mediaSource = content.mediaSource,
|
mediaSource = content.mediaSource,
|
||||||
mimeType = content.mimeType,
|
mimeType = content.mimeType,
|
||||||
body = content.body,
|
filename = content.filename,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue