Merge branch 'develop' into feature/fga/requests_to_join_list
This commit is contained in:
commit
fdd0445906
412 changed files with 4675 additions and 2105 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.6
|
- uses: mobile-dev-inc/action-maestro-cloud@v1.9.7
|
||||||
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 }}
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,5 @@ object ElementCallConfig {
|
||||||
/**
|
/**
|
||||||
* The default duration of a ringing call in seconds before it's automatically dismissed.
|
* The default duration of a ringing call in seconds before it's automatically dismissed.
|
||||||
*/
|
*/
|
||||||
const val RINGING_CALL_DURATION_SECONDS = 15
|
const val RINGING_CALL_DURATION_SECONDS = 90
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,20 @@ import android.graphics.Color
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
|
|
||||||
object NotificationConfig {
|
object NotificationConfig {
|
||||||
// TODO EAx Implement and set to true at some point
|
/**
|
||||||
const val SUPPORT_MARK_AS_READ_ACTION = false
|
* If set to true, the notification will have a "Mark as read" action.
|
||||||
|
*/
|
||||||
|
const val SHOW_MARK_AS_READ_ACTION = true
|
||||||
|
|
||||||
// TODO EAx Implement and set to true at some point
|
/**
|
||||||
const val SUPPORT_JOIN_DECLINE_INVITE = false
|
* If set to true, the notification for invitation will have two actions to accept or decline the invite.
|
||||||
|
*/
|
||||||
|
const val SHOW_ACCEPT_AND_DECLINE_INVITE_ACTIONS = true
|
||||||
|
|
||||||
// TODO EAx Implement and set to true at some point
|
/**
|
||||||
const val SUPPORT_QUICK_REPLY_ACTION = false
|
* If set to true, the notification will have a "Quick reply" action, allow to compose and send a message to the room.
|
||||||
|
*/
|
||||||
|
const val SHOW_QUICK_REPLY_ACTION = true
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
val NOTIFICATION_ACCENT_COLOR: Int = Color.parseColor("#FF0DBD8B")
|
val NOTIFICATION_ACCENT_COLOR: Int = Color.parseColor("#FF0DBD8B")
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,20 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.bumble.appyx.core.composable.PermanentChild
|
import com.bumble.appyx.core.composable.PermanentChild
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.navigation.NavElements
|
||||||
|
import com.bumble.appyx.core.navigation.NavKey
|
||||||
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStack.State.ACTIVE
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStack.State.CREATED
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStack.State.STASHED
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStackElement
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStackElements
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.BackStackOperation
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.Push
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
|
|
@ -312,7 +321,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
||||||
coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias()) }
|
coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias(), clearBackstack = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
|
override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
|
||||||
|
|
@ -400,6 +409,11 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
is NavTarget.SecureBackup -> {
|
is NavTarget.SecureBackup -> {
|
||||||
secureBackupEntryPoint.nodeBuilder(this, buildContext)
|
secureBackupEntryPoint.nodeBuilder(this, buildContext)
|
||||||
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
|
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
|
||||||
|
.callback(object : SecureBackupEntryPoint.Callback {
|
||||||
|
override fun onDone() {
|
||||||
|
backstack.pop()
|
||||||
|
}
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
NavTarget.Ftue -> {
|
NavTarget.Ftue -> {
|
||||||
|
|
@ -467,21 +481,21 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
serverNames: List<String> = emptyList(),
|
serverNames: List<String> = emptyList(),
|
||||||
trigger: JoinedRoom.Trigger? = null,
|
trigger: JoinedRoom.Trigger? = null,
|
||||||
eventId: EventId? = null,
|
eventId: EventId? = null,
|
||||||
|
clearBackstack: Boolean,
|
||||||
) {
|
) {
|
||||||
waitForNavTargetAttached { navTarget ->
|
waitForNavTargetAttached { navTarget ->
|
||||||
navTarget is NavTarget.RoomList
|
navTarget is NavTarget.RoomList
|
||||||
}
|
}
|
||||||
attachChild<RoomFlowNode> {
|
attachChild<RoomFlowNode> {
|
||||||
backstack.push(
|
val roomNavTarget = NavTarget.Room(
|
||||||
NavTarget.Room(
|
roomIdOrAlias = roomIdOrAlias,
|
||||||
roomIdOrAlias = roomIdOrAlias,
|
serverNames = serverNames,
|
||||||
serverNames = serverNames,
|
trigger = trigger,
|
||||||
trigger = trigger,
|
initialElement = RoomNavigationTarget.Messages(
|
||||||
initialElement = RoomNavigationTarget.Messages(
|
focusedEventId = eventId
|
||||||
focusedEventId = eventId
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
backstack.accept(AttachRoomOperation(roomNavTarget, clearBackstack))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -526,3 +540,31 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
) : Node(buildContext, plugins = plugins)
|
) : Node(buildContext, plugins = plugins)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
private class AttachRoomOperation(
|
||||||
|
val roomTarget: LoggedInFlowNode.NavTarget.Room,
|
||||||
|
val clearBackstack: Boolean,
|
||||||
|
) : BackStackOperation<LoggedInFlowNode.NavTarget> {
|
||||||
|
override fun isApplicable(elements: NavElements<LoggedInFlowNode.NavTarget, BackStack.State>) = true
|
||||||
|
|
||||||
|
override fun invoke(elements: BackStackElements<LoggedInFlowNode.NavTarget>): BackStackElements<LoggedInFlowNode.NavTarget> {
|
||||||
|
return if (clearBackstack) {
|
||||||
|
// Makes sure the room list target is alone in the backstack and stashed
|
||||||
|
elements.mapNotNull { element ->
|
||||||
|
if (element.key.navTarget == LoggedInFlowNode.NavTarget.RoomList) {
|
||||||
|
element.transitionTo(STASHED, this)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} + BackStackElement(
|
||||||
|
key = NavKey(roomTarget),
|
||||||
|
fromState = CREATED,
|
||||||
|
targetState = ACTIVE,
|
||||||
|
operation = this
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Push<LoggedInFlowNode.NavTarget>(roomTarget).invoke(elements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
trigger = JoinedRoom.Trigger.MobilePermalink,
|
trigger = JoinedRoom.Trigger.MobilePermalink,
|
||||||
serverNames = permalinkData.viaParameters,
|
serverNames = permalinkData.viaParameters,
|
||||||
eventId = permalinkData.eventId,
|
eventId = permalinkData.eventId,
|
||||||
|
clearBackstack = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is PermalinkData.UserLink -> {
|
is PermalinkData.UserLink -> {
|
||||||
|
|
@ -318,7 +319,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
.apply {
|
.apply {
|
||||||
when (deeplinkData) {
|
when (deeplinkData) {
|
||||||
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
|
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
|
||||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias())
|
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias(), clearBackstack = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ allprojects {
|
||||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.19")
|
detektPlugins("io.nlopez.compose.rules:detekt:0.4.19")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
|
||||||
|
exclude("io/element/android/tests/konsist/failures/**")
|
||||||
|
}
|
||||||
|
|
||||||
// KtLint
|
// KtLint
|
||||||
apply {
|
apply {
|
||||||
plugin("org.jlleitschuh.gradle.ktlint")
|
plugin("org.jlleitschuh.gradle.ktlint")
|
||||||
|
|
@ -75,6 +79,7 @@ allprojects {
|
||||||
val generatedPath = "${layout.buildDirectory.asFile.get()}/generated/"
|
val generatedPath = "${layout.buildDirectory.asFile.get()}/generated/"
|
||||||
filter {
|
filter {
|
||||||
exclude { element -> element.file.path.contains(generatedPath) }
|
exclude { element -> element.file.path.contains(generatedPath) }
|
||||||
|
exclude("io/element/android/tests/konsist/failures/**")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Dependency check
|
// Dependency check
|
||||||
|
|
|
||||||
|
|
@ -66,19 +66,34 @@ class WebViewWidgetMessageInterceptor(
|
||||||
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
|
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
|
||||||
// No network for instance, transmit the error
|
// No network for instance, transmit the error
|
||||||
Timber.e("onReceivedError error: ${error?.errorCode} ${error?.description}")
|
Timber.e("onReceivedError error: ${error?.errorCode} ${error?.description}")
|
||||||
onError(error?.description?.toString())
|
|
||||||
|
// Only propagate the error if it happens while loading the current page
|
||||||
|
if (view?.url == request?.url.toString()) {
|
||||||
|
onError(error?.description.toString())
|
||||||
|
}
|
||||||
|
|
||||||
super.onReceivedError(view, request, error)
|
super.onReceivedError(view, request, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceivedHttpError(view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse?) {
|
override fun onReceivedHttpError(view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse?) {
|
||||||
Timber.e("onReceivedHttpError error: ${errorResponse?.statusCode} ${errorResponse?.reasonPhrase}")
|
Timber.e("onReceivedHttpError error: ${errorResponse?.statusCode} ${errorResponse?.reasonPhrase}")
|
||||||
onError(errorResponse?.statusCode.toString())
|
|
||||||
|
// Only propagate the error if it happens while loading the current page
|
||||||
|
if (view?.url == request?.url.toString()) {
|
||||||
|
onError(errorResponse?.statusCode.toString())
|
||||||
|
}
|
||||||
|
|
||||||
super.onReceivedHttpError(view, request, errorResponse)
|
super.onReceivedHttpError(view, request, errorResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
|
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
|
||||||
Timber.e("onReceivedSslError error: ${error?.primaryError}")
|
Timber.e("onReceivedSslError error: ${error?.primaryError}")
|
||||||
onError(error?.primaryError?.toString())
|
|
||||||
|
// Only propagate the error if it happens while loading the current page
|
||||||
|
if (view?.url == error?.url.toString()) {
|
||||||
|
onError(error?.toString())
|
||||||
|
}
|
||||||
|
|
||||||
super.onReceivedSslError(view, handler, error)
|
super.onReceivedSslError(view, handler, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,10 @@ class CreateRoomDataStore @Inject constructor(
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
createRoomConfigFlow.getAndUpdate { config ->
|
||||||
config.copy(
|
config.copy(
|
||||||
roomVisibility = when (config.roomVisibility) {
|
roomVisibility = when (config.roomVisibility) {
|
||||||
is RoomVisibilityState.Public -> config.roomVisibility.copy(roomAddress = RoomAddress.Edited(address))
|
is RoomVisibilityState.Public -> {
|
||||||
|
val sanitizedAddress = address.lowercase()
|
||||||
|
config.roomVisibility.copy(roomAddress = RoomAddress.Edited(sanitizedAddress))
|
||||||
|
}
|
||||||
else -> config.roomVisibility
|
else -> config.roomVisibility
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -240,9 +240,9 @@ private fun RoomTopic(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
label = stringResource(R.string.screen_create_room_topic_label),
|
label = stringResource(R.string.screen_create_room_topic_label),
|
||||||
value = topic,
|
value = topic,
|
||||||
placeholder = stringResource(CommonStrings.common_topic_placeholder),
|
|
||||||
onValueChange = onTopicChange,
|
onValueChange = onTopicChange,
|
||||||
maxLines = 3,
|
maxLines = 3,
|
||||||
|
supportingText = stringResource(CommonStrings.common_topic_placeholder),
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
capitalization = KeyboardCapitalization.Sentences,
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,22 @@
|
||||||
<string name="screen_create_room_action_create_room">"Neuer Raum"</string>
|
<string name="screen_create_room_action_create_room">"Neuer Raum"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Personen einladen"</string>
|
<string name="screen_create_room_add_people_title">"Personen einladen"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Beim Erstellen des Chats ist ein Fehler aufgetreten"</string>
|
<string name="screen_create_room_error_creating_room">"Beim Erstellen des Chats ist ein Fehler aufgetreten"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Die Nachrichten in diesem Chat sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden."</string>
|
<string name="screen_create_room_private_option_description">"Nur eingeladene Personen haben Zutritt zu diesem Chatroom. Alle Nachrichten sind durchgehend verschlüsselt."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
|
<string name="screen_create_room_private_option_title">"Privater Chatroom"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden."</string>
|
<string name="screen_create_room_public_option_description">"Jeder kann diesen Chatroom finden.
|
||||||
<string name="screen_create_room_public_option_title">"Öffentlicher Raum (für alle)"</string>
|
Sie können dies aber jederzeit in den Chatroomeinstellungen ändern."</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"Öffentlicher Chatroom"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Jeder kann diesem Chatroom beitreten"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Jemand"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Chatroom Zugang"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Jeder kann darum bitten, dem Chatroom beizutreten, aber ein Administrator oder ein Moderator muss die Anfrage akzeptieren."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Beitritt beantragen"</string>
|
||||||
|
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Einige Zeichen sind nicht erlaubt. Es werden nur Buchstaben, Ziffern und die folgenden Symbole unterstützt: ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"</string>
|
||||||
|
<string name="screen_create_room_room_address_not_available_error_description">"Diese Chatroomadresse existiert bereits. Bitte versuchen Sie, das Adressenfeld des Chatrooms zu bearbeiten oder den Namen des Chatrooms zu ändern"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Damit dieser Chatroom im öffentlichen Chatroomverzeichnis sichtbar ist, benötigen Sie eine Chatroomadresse."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Chatroom Adresse"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Raumname"</string>
|
<string name="screen_create_room_room_name_label">"Raumname"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">" Sichtbarkeit des Chatrooms"</string>
|
||||||
<string name="screen_create_room_title">"Raum erstellen"</string>
|
<string name="screen_create_room_title">"Raum erstellen"</string>
|
||||||
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
|
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
|
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ You can change this anytime in room settings."</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Anyone can ask to join the room but an administrator or a moderator will have to accept the request"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Anyone can ask to join the room but an administrator or a moderator will have to accept the request"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Ask to join"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Ask to join"</string>
|
||||||
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"</string>
|
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"</string>
|
||||||
<string name="screen_create_room_room_address_not_available_error_description">"This room address already exists, please try editing the room address field or change the room name"</string>
|
<string name="screen_create_room_room_address_not_available_error_description">"This room address already exists. Please try editing the room address field or change the room name"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"In order for this room to be visible in the public room directory, you will need a room address."</string>
|
<string name="screen_create_room_room_address_section_footer">"In order for this room to be visible in the public room directory, you will need a room address."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Room address"</string>
|
<string name="screen_create_room_room_address_section_title">"Room address"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Room name"</string>
|
<string name="screen_create_room_room_name_label">"Room name"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_deactivate_account_confirmation_dialog_content">"Bitte bestätigen Sie, dass Sie Ihr Benutzerkonto deaktivieren möchten. Diese Aktion kann nicht rückgängig gemacht werden."</string>
|
||||||
|
<string name="screen_deactivate_account_delete_all_messages">"Lösche alle meine Nachrichten"</string>
|
||||||
|
<string name="screen_deactivate_account_delete_all_messages_notice">"Warnung: Benutzern werden möglicherweise unvollständige Konversationen angezeigt."</string>
|
||||||
|
<string name="screen_deactivate_account_description">"Wenn Sie Ihr Konto deaktivieren%1$s, wird es:"</string>
|
||||||
|
<string name="screen_deactivate_account_description_bold_part">"irreversibel"</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_1">"%1$s Ihr Konto (Sie können sich nicht erneut anmelden und Ihre ID kann nicht wiederverwendet werden)."</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_1_bold_part">"Dauerhaft deaktivieren"</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_2">"Sie werden aus allen Chatrooms entfernt."</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_3">"Löschen Sie Ihre Kontoinformationen von unserem Identitätsserver."</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_4">"Gelöschte Nachrichten werden für bereits registrierte Benutzer weiterhin sichtbar sein, wenn sie auch neuen oder nicht registrierten Benutzern nicht mehr zur Verfügung stehen."</string>
|
||||||
|
<string name="screen_deactivate_account_title">"Benutzerkonto deaktivieren"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -16,7 +16,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
class SharedPreferencesWelcomeScreenState @Inject constructor(
|
class SharedPreferencesWelcomeScreenStore @Inject constructor(
|
||||||
private val sharedPreferences: SharedPreferences,
|
private val sharedPreferences: SharedPreferences,
|
||||||
) : WelcomeScreenStore {
|
) : WelcomeScreenStore {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.features.ftue.impl.welcome.state
|
package io.element.android.features.ftue.impl.welcome.state
|
||||||
|
|
||||||
class InMemoryWelcomeScreenState : WelcomeScreenStore {
|
class InMemoryWelcomeScreenStore : WelcomeScreenStore {
|
||||||
private var isWelcomeScreenNeeded = true
|
private var isWelcomeScreenNeeded = true
|
||||||
|
|
||||||
override fun isWelcomeScreenNeeded(): Boolean {
|
override fun isWelcomeScreenNeeded(): Boolean {
|
||||||
|
|
@ -48,8 +48,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
private const val MAX_KNOCK_MESSAGE_LENGTH = 500
|
|
||||||
|
|
||||||
class JoinRoomPresenter @AssistedInject constructor(
|
class JoinRoomPresenter @AssistedInject constructor(
|
||||||
@Assisted private val roomId: RoomId,
|
@Assisted private val roomId: RoomId,
|
||||||
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
|
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.RoomType
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
|
|
||||||
|
internal const val MAX_KNOCK_MESSAGE_LENGTH = 500
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class JoinRoomState(
|
data class JoinRoomState(
|
||||||
val contentState: ContentState,
|
val contentState: ContentState,
|
||||||
|
|
|
||||||
|
|
@ -40,17 +40,6 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
|
||||||
),
|
),
|
||||||
aJoinRoomState(
|
|
||||||
contentState = aLoadedContentState(
|
|
||||||
joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock,
|
|
||||||
topic = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt" +
|
|
||||||
" ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco" +
|
|
||||||
" laboris nisi ut aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit in" +
|
|
||||||
" voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat" +
|
|
||||||
" non proident sunt in culpa qui officia deserunt mollit anim id est laborum",
|
|
||||||
numberOfMembers = 888,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null))
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null))
|
||||||
),
|
),
|
||||||
|
|
@ -81,6 +70,29 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
isDm = true,
|
isDm = true,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock,
|
||||||
|
topic = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt" +
|
||||||
|
" ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco" +
|
||||||
|
" laboris nisi ut aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit in" +
|
||||||
|
" voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat" +
|
||||||
|
" non proident sunt in culpa qui officia deserunt mollit anim id est laborum",
|
||||||
|
numberOfMembers = 888,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
knockMessage = "Let me in please!",
|
||||||
|
contentState = aLoadedContentState(
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock,
|
||||||
|
topic = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt" +
|
||||||
|
" ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco" +
|
||||||
|
" laboris nisi ut aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit in" +
|
||||||
|
" voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat" +
|
||||||
|
" non proident sunt in culpa qui officia deserunt mollit anim id est laborum",
|
||||||
|
numberOfMembers = 888,
|
||||||
|
)
|
||||||
|
),
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(
|
contentState = aLoadedContentState(
|
||||||
name = "A knocked Room",
|
name = "A knocked Room",
|
||||||
|
|
|
||||||
|
|
@ -390,13 +390,18 @@ private fun DefaultLoadedContent(
|
||||||
)
|
)
|
||||||
} else if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
|
} else if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
val supportingText = if (knockMessage.isNotEmpty()) {
|
||||||
|
"${knockMessage.length}/$MAX_KNOCK_MESSAGE_LENGTH"
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.screen_join_room_knock_message_description)
|
||||||
|
}
|
||||||
TextField(
|
TextField(
|
||||||
value = knockMessage,
|
value = knockMessage,
|
||||||
onValueChange = onKnockMessageUpdate,
|
onValueChange = onKnockMessageUpdate,
|
||||||
maxLines = 3,
|
maxLines = 3,
|
||||||
minLines = 3,
|
minLines = 3,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
supportingText = stringResource(R.string.screen_join_room_knock_message_description)
|
supportingText = supportingText
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +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_join_room_cancel_knock_action">"Anfrage abbrechen"</string>
|
||||||
|
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ja, abbrechen"</string>
|
||||||
|
<string name="screen_join_room_cancel_knock_alert_description">"Möchten Sie Ihre Beitrittsanfrage für diesen Chatroom wirklich stornieren?"</string>
|
||||||
|
<string name="screen_join_room_cancel_knock_alert_title">"Beitrittsanfrage stornieren"</string>
|
||||||
<string name="screen_join_room_join_action">"Raum beitreten"</string>
|
<string name="screen_join_room_join_action">"Raum beitreten"</string>
|
||||||
<string name="screen_join_room_knock_action">"Anklopfen"</string>
|
<string name="screen_join_room_knock_action">"Anklopfen"</string>
|
||||||
|
<string name="screen_join_room_knock_message_description">"Nachricht (optional)"</string>
|
||||||
|
<string name="screen_join_room_knock_sent_description">"Falls Ihre Anfrage, dem Raum beizutreten, akzeptiert wird, werden Sie eine Einladung erhalten."</string>
|
||||||
|
<string name="screen_join_room_knock_sent_title">"Beitrittsanfrage geschickt"</string>
|
||||||
<string name="screen_join_room_space_not_supported_description">"%1$s unterstützt noch keine Spaces. Du kannst auf Spaces im Web zugreifen."</string>
|
<string name="screen_join_room_space_not_supported_description">"%1$s unterstützt noch keine Spaces. Du kannst auf Spaces im Web zugreifen."</string>
|
||||||
<string name="screen_join_room_space_not_supported_title">"Spaces werden noch nicht unterstützt"</string>
|
<string name="screen_join_room_space_not_supported_title">"Spaces werden noch nicht unterstützt"</string>
|
||||||
<string name="screen_join_room_subtitle_knock">"Klopfe an um einen Administrator zu benachrichtigen. Nach der Freigabe kannst du dich an der Unterhaltung beteiligen."</string>
|
<string name="screen_join_room_subtitle_knock">"Klopfe an um einen Administrator zu benachrichtigen. Nach der Freigabe kannst du dich an der Unterhaltung beteiligen."</string>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<string name="screen_app_lock_biometric_authentication">"biometrische Authentifizierung"</string>
|
<string name="screen_app_lock_biometric_authentication">"biometrische Authentifizierung"</string>
|
||||||
<string name="screen_app_lock_biometric_unlock">"biometrisches Entsperren"</string>
|
<string name="screen_app_lock_biometric_unlock">"biometrisches Entsperren"</string>
|
||||||
<string name="screen_app_lock_biometric_unlock_title_android">"Mit Biometrie entsperren"</string>
|
<string name="screen_app_lock_biometric_unlock_title_android">"Mit Biometrie entsperren"</string>
|
||||||
|
<string name="screen_app_lock_confirm_biometric_authentication_android">"Biometrische Daten bestätigen"</string>
|
||||||
<string name="screen_app_lock_forgot_pin">"PIN vergessen?"</string>
|
<string name="screen_app_lock_forgot_pin">"PIN vergessen?"</string>
|
||||||
<string name="screen_app_lock_settings_change_pin">"PIN-Code ändern"</string>
|
<string name="screen_app_lock_settings_change_pin">"PIN-Code ändern"</string>
|
||||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Biometrisches Entsperren zulassen"</string>
|
<string name="screen_app_lock_settings_enable_biometric_unlock">"Biometrisches Entsperren zulassen"</string>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<string name="screen_app_lock_biometric_authentication">"l’authentification biométrique"</string>
|
<string name="screen_app_lock_biometric_authentication">"l’authentification biométrique"</string>
|
||||||
<string name="screen_app_lock_biometric_unlock">"déverrouillage biométrique"</string>
|
<string name="screen_app_lock_biometric_unlock">"déverrouillage biométrique"</string>
|
||||||
<string name="screen_app_lock_biometric_unlock_title_android">"Déverrouiller avec la biométrie"</string>
|
<string name="screen_app_lock_biometric_unlock_title_android">"Déverrouiller avec la biométrie"</string>
|
||||||
|
<string name="screen_app_lock_confirm_biometric_authentication_android">"Confirmer la biométrie"</string>
|
||||||
<string name="screen_app_lock_forgot_pin">"Code PIN oublié?"</string>
|
<string name="screen_app_lock_forgot_pin">"Code PIN oublié?"</string>
|
||||||
<string name="screen_app_lock_settings_change_pin">"Modifier le code PIN"</string>
|
<string name="screen_app_lock_settings_change_pin">"Modifier le code PIN"</string>
|
||||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Autoriser le déverrouillage biométrique"</string>
|
<string name="screen_app_lock_settings_enable_biometric_unlock">"Autoriser le déverrouillage biométrique"</string>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<string name="screen_app_lock_biometric_authentication">"биометрическая идентификация"</string>
|
<string name="screen_app_lock_biometric_authentication">"биометрическая идентификация"</string>
|
||||||
<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_confirm_biometric_authentication_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>
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Ger
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string>
|
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_4">"Scanne den QR-Code mit diesem Gerät"</string>
|
<string name="screen_qr_code_login_initial_state_item_4">"Scanne den QR-Code mit diesem Gerät"</string>
|
||||||
|
<string name="screen_qr_code_login_initial_state_subtitle">"Nur verfügbar für den Fall dass Ihr Kontoanbieter dies unterstützt."</string>
|
||||||
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
|
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import io.element.android.features.messages.impl.timeline.TimelineController
|
||||||
import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode
|
import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||||
|
|
@ -51,6 +52,7 @@ 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.overlay.Overlay
|
import io.element.android.libraries.architecture.overlay.Overlay
|
||||||
|
import io.element.android.libraries.architecture.overlay.operation.hide
|
||||||
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
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
@ -66,8 +68,8 @@ import io.element.android.libraries.matrix.api.room.joinedRoomMembers
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache
|
import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache
|
||||||
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
||||||
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
|
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||||
import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanTheme
|
import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanTheme
|
||||||
import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme
|
import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
@ -86,6 +88,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
private val showLocationEntryPoint: ShowLocationEntryPoint,
|
private val showLocationEntryPoint: ShowLocationEntryPoint,
|
||||||
private val createPollEntryPoint: CreatePollEntryPoint,
|
private val createPollEntryPoint: CreatePollEntryPoint,
|
||||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||||
|
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val roomMemberProfilesCache: RoomMemberProfilesCache,
|
private val roomMemberProfilesCache: RoomMemberProfilesCache,
|
||||||
|
|
@ -228,14 +231,22 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
createNode<MessagesNode>(buildContext, listOf(callback, inputs))
|
createNode<MessagesNode>(buildContext, listOf(callback, inputs))
|
||||||
}
|
}
|
||||||
is NavTarget.MediaViewer -> {
|
is NavTarget.MediaViewer -> {
|
||||||
val inputs = MediaViewerNode.Inputs(
|
val params = MediaViewerEntryPoint.Params(
|
||||||
mediaInfo = navTarget.mediaInfo,
|
mediaInfo = navTarget.mediaInfo,
|
||||||
mediaSource = navTarget.mediaSource,
|
mediaSource = navTarget.mediaSource,
|
||||||
thumbnailSource = navTarget.thumbnailSource,
|
thumbnailSource = navTarget.thumbnailSource,
|
||||||
canDownload = true,
|
canDownload = true,
|
||||||
canShare = true,
|
canShare = true,
|
||||||
)
|
)
|
||||||
createNode<MediaViewerNode>(buildContext, listOf(inputs))
|
val callback = object : MediaViewerEntryPoint.Callback {
|
||||||
|
override fun onDone() {
|
||||||
|
overlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.params(params)
|
||||||
|
.callback(callback)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
is NavTarget.AttachmentPreview -> {
|
is NavTarget.AttachmentPreview -> {
|
||||||
val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment)
|
val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment)
|
||||||
|
|
@ -319,100 +330,93 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processEventClick(event: TimelineItem.Event): Boolean {
|
private fun processEventClick(event: TimelineItem.Event): Boolean {
|
||||||
return when (event.content) {
|
val navTarget = when (event.content) {
|
||||||
is TimelineItemImageContent -> {
|
is TimelineItemImageContent -> {
|
||||||
val navTarget = NavTarget.MediaViewer(
|
buildMediaViewerNavTarget(
|
||||||
mediaInfo = MediaInfo(
|
event = event,
|
||||||
filename = event.content.filename,
|
content = event.content,
|
||||||
caption = event.content.caption,
|
|
||||||
mimeType = event.content.mimeType,
|
|
||||||
formattedFileSize = event.content.formattedFileSize,
|
|
||||||
fileExtension = event.content.fileExtension
|
|
||||||
),
|
|
||||||
mediaSource = event.content.mediaSource,
|
mediaSource = event.content.mediaSource,
|
||||||
thumbnailSource = event.content.thumbnailSource,
|
thumbnailSource = event.content.thumbnailSource,
|
||||||
)
|
)
|
||||||
overlay.show(navTarget)
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
is TimelineItemStickerContent -> {
|
is TimelineItemStickerContent -> {
|
||||||
/* Sticker may have an empty url and no thumbnail
|
/* Sticker may have an empty url and no thumbnail
|
||||||
if encrypted on certain bridges */
|
if encrypted on certain bridges */
|
||||||
if (event.content.preferredMediaSource != null) {
|
event.content.preferredMediaSource?.let { preferredMediaSource ->
|
||||||
val navTarget = NavTarget.MediaViewer(
|
buildMediaViewerNavTarget(
|
||||||
mediaInfo = MediaInfo(
|
event = event,
|
||||||
filename = event.content.filename,
|
content = event.content,
|
||||||
caption = event.content.caption,
|
mediaSource = preferredMediaSource,
|
||||||
mimeType = event.content.mimeType,
|
|
||||||
formattedFileSize = event.content.formattedFileSize,
|
|
||||||
fileExtension = event.content.fileExtension
|
|
||||||
),
|
|
||||||
mediaSource = event.content.preferredMediaSource,
|
|
||||||
thumbnailSource = event.content.thumbnailSource,
|
thumbnailSource = event.content.thumbnailSource,
|
||||||
)
|
)
|
||||||
overlay.show(navTarget)
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TimelineItemVideoContent -> {
|
is TimelineItemVideoContent -> {
|
||||||
val navTarget = NavTarget.MediaViewer(
|
buildMediaViewerNavTarget(
|
||||||
mediaInfo = MediaInfo(
|
event = event,
|
||||||
filename = event.content.filename,
|
content = event.content,
|
||||||
caption = event.content.caption,
|
mediaSource = event.content.mediaSource,
|
||||||
mimeType = event.content.mimeType,
|
|
||||||
formattedFileSize = event.content.formattedFileSize,
|
|
||||||
fileExtension = event.content.fileExtension
|
|
||||||
),
|
|
||||||
mediaSource = event.content.videoSource,
|
|
||||||
thumbnailSource = event.content.thumbnailSource,
|
thumbnailSource = event.content.thumbnailSource,
|
||||||
)
|
)
|
||||||
overlay.show(navTarget)
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
is TimelineItemFileContent -> {
|
is TimelineItemFileContent -> {
|
||||||
val navTarget = NavTarget.MediaViewer(
|
buildMediaViewerNavTarget(
|
||||||
mediaInfo = MediaInfo(
|
event = event,
|
||||||
filename = event.content.filename,
|
content = event.content,
|
||||||
caption = event.content.caption,
|
mediaSource = event.content.mediaSource,
|
||||||
mimeType = event.content.mimeType,
|
|
||||||
formattedFileSize = event.content.formattedFileSize,
|
|
||||||
fileExtension = event.content.fileExtension
|
|
||||||
),
|
|
||||||
mediaSource = event.content.fileSource,
|
|
||||||
thumbnailSource = event.content.thumbnailSource,
|
thumbnailSource = event.content.thumbnailSource,
|
||||||
)
|
)
|
||||||
overlay.show(navTarget)
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
is TimelineItemAudioContent -> {
|
is TimelineItemAudioContent -> {
|
||||||
val navTarget = NavTarget.MediaViewer(
|
buildMediaViewerNavTarget(
|
||||||
mediaInfo = MediaInfo(
|
event = event,
|
||||||
filename = event.content.filename,
|
content = event.content,
|
||||||
caption = event.content.caption,
|
|
||||||
mimeType = event.content.mimeType,
|
|
||||||
formattedFileSize = event.content.formattedFileSize,
|
|
||||||
fileExtension = event.content.fileExtension
|
|
||||||
),
|
|
||||||
mediaSource = event.content.mediaSource,
|
mediaSource = event.content.mediaSource,
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
)
|
)
|
||||||
overlay.show(navTarget)
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
is TimelineItemLocationContent -> {
|
is TimelineItemLocationContent -> {
|
||||||
val navTarget = NavTarget.LocationViewer(
|
NavTarget.LocationViewer(
|
||||||
location = event.content.location,
|
location = event.content.location,
|
||||||
description = event.content.description,
|
description = event.content.description,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
return when (navTarget) {
|
||||||
|
is NavTarget.MediaViewer -> {
|
||||||
overlay.show(navTarget)
|
overlay.show(navTarget)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
is NavTarget.LocationViewer -> {
|
||||||
|
backstack.push(navTarget)
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildMediaViewerNavTarget(
|
||||||
|
event: TimelineItem.Event,
|
||||||
|
content: TimelineItemEventContentWithAttachment,
|
||||||
|
mediaSource: MediaSource,
|
||||||
|
thumbnailSource: MediaSource?,
|
||||||
|
): NavTarget {
|
||||||
|
return NavTarget.MediaViewer(
|
||||||
|
mediaInfo = MediaInfo(
|
||||||
|
filename = content.filename,
|
||||||
|
caption = content.caption,
|
||||||
|
mimeType = content.mimeType,
|
||||||
|
formattedFileSize = content.formattedFileSize,
|
||||||
|
fileExtension = content.fileExtension,
|
||||||
|
senderName = event.safeSenderName,
|
||||||
|
dateSent = event.sentTime,
|
||||||
|
),
|
||||||
|
mediaSource = mediaSource,
|
||||||
|
thumbnailSource = thumbnailSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
mentionSpanTheme.updateStyles(currentUserId = room.sessionId)
|
mentionSpanTheme.updateStyles(currentUserId = room.sessionId)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||||
|
|
@ -65,6 +67,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
messageComposerPresenterFactory: MessageComposerPresenter.Factory,
|
messageComposerPresenterFactory: MessageComposerPresenter.Factory,
|
||||||
timelinePresenterFactory: TimelinePresenter.Factory,
|
timelinePresenterFactory: TimelinePresenter.Factory,
|
||||||
presenterFactory: MessagesPresenter.Factory,
|
presenterFactory: MessagesPresenter.Factory,
|
||||||
|
actionListPresenterFactory: ActionListPresenter.Factory,
|
||||||
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
||||||
private val mediaPlayer: MediaPlayer,
|
private val mediaPlayer: MediaPlayer,
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
|
|
@ -73,6 +76,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
navigator = this,
|
navigator = this,
|
||||||
composerPresenter = messageComposerPresenterFactory.create(this),
|
composerPresenter = messageComposerPresenterFactory.create(this),
|
||||||
timelinePresenter = timelinePresenterFactory.create(this),
|
timelinePresenter = timelinePresenterFactory.create(this),
|
||||||
|
actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
|
||||||
)
|
)
|
||||||
private val callbacks = plugins<Callback>()
|
private val callbacks = plugins<Callback>()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,8 @@ import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||||
import io.element.android.appconfig.MessageComposerConfig
|
import io.element.android.appconfig.MessageComposerConfig
|
||||||
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
|
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
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.actionlist.model.TimelineItemActionPostProcessor
|
|
||||||
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
|
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||||
|
|
@ -93,7 +92,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
@Assisted private val timelinePresenter: Presenter<TimelineState>,
|
@Assisted private val timelinePresenter: Presenter<TimelineState>,
|
||||||
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
||||||
private val identityChangeStatePresenter: Presenter<IdentityChangeState>,
|
private val identityChangeStatePresenter: Presenter<IdentityChangeState>,
|
||||||
actionListPresenterFactory: ActionListPresenter.Factory,
|
@Assisted private val actionListPresenter: Presenter<ActionListState>,
|
||||||
private val customReactionPresenter: Presenter<CustomReactionState>,
|
private val customReactionPresenter: Presenter<CustomReactionState>,
|
||||||
private val reactionSummaryPresenter: Presenter<ReactionSummaryState>,
|
private val reactionSummaryPresenter: Presenter<ReactionSummaryState>,
|
||||||
private val readReceiptBottomSheetPresenter: Presenter<ReadReceiptBottomSheetState>,
|
private val readReceiptBottomSheetPresenter: Presenter<ReadReceiptBottomSheetState>,
|
||||||
|
|
@ -110,14 +109,13 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
) : Presenter<MessagesState> {
|
) : Presenter<MessagesState> {
|
||||||
private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
|
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
navigator: MessagesNavigator,
|
navigator: MessagesNavigator,
|
||||||
composerPresenter: Presenter<MessageComposerState>,
|
composerPresenter: Presenter<MessageComposerState>,
|
||||||
timelinePresenter: Presenter<TimelineState>,
|
timelinePresenter: Presenter<TimelineState>,
|
||||||
|
actionListPresenter: Presenter<ActionListState>,
|
||||||
): MessagesPresenter
|
): MessagesPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,7 +270,8 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
timelineState: TimelineState,
|
timelineState: TimelineState,
|
||||||
) = launch {
|
) = launch {
|
||||||
when (action) {
|
when (action) {
|
||||||
TimelineItemAction.Copy -> handleCopyContents(targetEvent)
|
TimelineItemAction.CopyText -> handleCopyContents(targetEvent)
|
||||||
|
TimelineItemAction.CopyCaption -> handleCopyCaption(targetEvent)
|
||||||
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
|
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
|
||||||
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
||||||
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
||||||
|
|
@ -403,26 +402,28 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleActionAddCaption(
|
private suspend fun handleActionAddCaption(
|
||||||
targetEvent: TimelineItem.Event,
|
targetEvent: TimelineItem.Event,
|
||||||
composerState: MessageComposerState,
|
composerState: MessageComposerState,
|
||||||
) {
|
) {
|
||||||
val composerMode = MessageComposerMode.EditCaption(
|
val composerMode = MessageComposerMode.EditCaption(
|
||||||
eventOrTransactionId = targetEvent.eventOrTransactionId,
|
eventOrTransactionId = targetEvent.eventOrTransactionId,
|
||||||
content = "",
|
content = "",
|
||||||
|
showCaptionCompatibilityWarning = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaCaptionWarning),
|
||||||
)
|
)
|
||||||
composerState.eventSink(
|
composerState.eventSink(
|
||||||
MessageComposerEvents.SetMode(composerMode)
|
MessageComposerEvents.SetMode(composerMode)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleActionEditCaption(
|
private suspend fun handleActionEditCaption(
|
||||||
targetEvent: TimelineItem.Event,
|
targetEvent: TimelineItem.Event,
|
||||||
composerState: MessageComposerState,
|
composerState: MessageComposerState,
|
||||||
) {
|
) {
|
||||||
val composerMode = MessageComposerMode.EditCaption(
|
val composerMode = MessageComposerMode.EditCaption(
|
||||||
eventOrTransactionId = targetEvent.eventOrTransactionId,
|
eventOrTransactionId = targetEvent.eventOrTransactionId,
|
||||||
content = (targetEvent.content as? TimelineItemEventContentWithAttachment)?.caption.orEmpty(),
|
content = (targetEvent.content as? TimelineItemEventContentWithAttachment)?.caption.orEmpty(),
|
||||||
|
showCaptionCompatibilityWarning = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaCaptionWarning),
|
||||||
)
|
)
|
||||||
composerState.eventSink(
|
composerState.eventSink(
|
||||||
MessageComposerEvents.SetMode(composerMode)
|
MessageComposerEvents.SetMode(composerMode)
|
||||||
|
|
@ -488,11 +489,17 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
is TimelineItemStateContent -> event.content.body
|
is TimelineItemStateContent -> event.content.body
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
|
|
||||||
clipboardHelper.copyPlainText(content)
|
clipboardHelper.copyPlainText(content)
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
snackbarDispatcher.post(SnackbarMessage(R.string.screen_room_timeline_message_copied))
|
snackbarDispatcher.post(SnackbarMessage(R.string.screen_room_timeline_message_copied))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun handleCopyCaption(event: TimelineItem.Event) {
|
||||||
|
val content = (event.content as? TimelineItemEventContentWithAttachment)?.caption ?: return
|
||||||
|
clipboardHelper.copyPlainText(content)
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled
|
import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled
|
||||||
import io.element.android.features.messages.impl.UserEventPermissions
|
import io.element.android.features.messages.impl.UserEventPermissions
|
||||||
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.actionlist.model.TimelineItemActionComparator
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
|
||||||
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory
|
||||||
|
|
@ -37,6 +38,8 @@ import io.element.android.features.messages.impl.timeline.model.event.canBeForwa
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canReact
|
import io.element.android.features.messages.impl.timeline.model.event.canReact
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
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.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
|
@ -60,6 +63,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled,
|
private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
|
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
) : ActionListPresenter {
|
) : ActionListPresenter {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
@ContributesBinding(RoomScope::class)
|
@ContributesBinding(RoomScope::class)
|
||||||
|
|
@ -67,6 +71,8 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
override fun create(postProcessor: TimelineItemActionPostProcessor): DefaultActionListPresenter
|
override fun create(postProcessor: TimelineItemActionPostProcessor): DefaultActionListPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val comparator = TimelineItemActionComparator()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ActionListState {
|
override fun present(): ActionListState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
|
@ -134,7 +140,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildActions(
|
private suspend fun buildActions(
|
||||||
timelineItem: TimelineItem.Event,
|
timelineItem: TimelineItem.Event,
|
||||||
usersEventPermissions: UserEventPermissions,
|
usersEventPermissions: UserEventPermissions,
|
||||||
isDeveloperModeEnabled: Boolean,
|
isDeveloperModeEnabled: Boolean,
|
||||||
|
|
@ -142,7 +148,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
isEventPinned: Boolean,
|
isEventPinned: Boolean,
|
||||||
): List<TimelineItemAction> {
|
): List<TimelineItemAction> {
|
||||||
val canRedact = timelineItem.isMine && usersEventPermissions.canRedactOwn || !timelineItem.isMine && usersEventPermissions.canRedactOther
|
val canRedact = timelineItem.isMine && usersEventPermissions.canRedactOwn || !timelineItem.isMine && usersEventPermissions.canRedactOther
|
||||||
return buildList {
|
return buildSet {
|
||||||
if (timelineItem.canBeRepliedTo && usersEventPermissions.canSendMessage) {
|
if (timelineItem.canBeRepliedTo && usersEventPermissions.canSendMessage) {
|
||||||
if (timelineItem.isThreaded) {
|
if (timelineItem.isThreaded) {
|
||||||
add(TimelineItemAction.ReplyInThread)
|
add(TimelineItemAction.ReplyInThread)
|
||||||
|
|
@ -157,7 +163,9 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
if (timelineItem.content is TimelineItemEventContentWithAttachment) {
|
if (timelineItem.content is TimelineItemEventContentWithAttachment) {
|
||||||
// Caption
|
// Caption
|
||||||
if (timelineItem.content.caption == null) {
|
if (timelineItem.content.caption == null) {
|
||||||
add(TimelineItemAction.AddCaption)
|
if (featureFlagService.isFeatureEnabled(FeatureFlags.MediaCaptionCreation)) {
|
||||||
|
add(TimelineItemAction.AddCaption)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
add(TimelineItemAction.EditCaption)
|
add(TimelineItemAction.EditCaption)
|
||||||
add(TimelineItemAction.RemoveCaption)
|
add(TimelineItemAction.RemoveCaption)
|
||||||
|
|
@ -178,7 +186,9 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (timelineItem.content.canBeCopied()) {
|
if (timelineItem.content.canBeCopied()) {
|
||||||
add(TimelineItemAction.Copy)
|
add(TimelineItemAction.CopyText)
|
||||||
|
} else if ((timelineItem.content as? TimelineItemEventContentWithAttachment)?.caption.isNullOrBlank().not()) {
|
||||||
|
add(TimelineItemAction.CopyCaption)
|
||||||
}
|
}
|
||||||
if (timelineItem.isRemote) {
|
if (timelineItem.isRemote) {
|
||||||
add(TimelineItemAction.CopyLink)
|
add(TimelineItemAction.CopyLink)
|
||||||
|
|
@ -194,6 +204,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.postFilter(timelineItem.content)
|
.postFilter(timelineItem.content)
|
||||||
|
.sortedWith(comparator)
|
||||||
.let(postProcessor::process)
|
.let(postProcessor::process)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +212,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
/**
|
/**
|
||||||
* Post filter the actions based on the content of the event.
|
* Post filter the actions based on the content of the event.
|
||||||
*/
|
*/
|
||||||
private fun List<TimelineItemAction>.postFilter(content: TimelineItemEventContent): List<TimelineItemAction> {
|
private fun Iterable<TimelineItemAction>.postFilter(content: TimelineItemEventContent): Iterable<TimelineItemAction> {
|
||||||
return filter { action ->
|
return filter { action ->
|
||||||
when (content) {
|
when (content) {
|
||||||
is TimelineItemCallNotifyContent,
|
is TimelineItemCallNotifyContent,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.actionlist
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
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.actionlist.model.TimelineItemActionComparator
|
||||||
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
||||||
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.anUnsignedDeviceSendFailure
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.anUnsignedDeviceSendFailure
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
|
|
@ -22,7 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
override val values: Sequence<ActionListState>
|
override val values: Sequence<ActionListState>
|
||||||
|
|
@ -50,7 +51,9 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(
|
||||||
|
copyAction = TimelineItemAction.CopyCaption,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -61,7 +64,9 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(
|
||||||
|
copyAction = TimelineItemAction.CopyCaption,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -72,7 +77,9 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(
|
||||||
|
copyAction = null,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -83,18 +90,22 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(
|
||||||
|
copyAction = TimelineItemAction.CopyCaption,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(
|
event = aTimelineItemEvent(
|
||||||
content = aTimelineItemVoiceContent(),
|
content = aTimelineItemVoiceContent(caption = null),
|
||||||
timelineItemReactions = reactionsState
|
timelineItemReactions = reactionsState
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(
|
||||||
|
copyAction = null,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -161,27 +172,31 @@ fun anActionListState(
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> {
|
fun aTimelineItemActionList(
|
||||||
return persistentListOf(
|
copyAction: TimelineItemAction? = TimelineItemAction.CopyText
|
||||||
|
): ImmutableList<TimelineItemAction> {
|
||||||
|
return setOfNotNull(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Copy,
|
copyAction,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
)
|
||||||
|
.sortedWith(TimelineItemActionComparator())
|
||||||
|
.toPersistentList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
|
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
|
||||||
return persistentListOf(
|
return setOf(
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
|
||||||
TimelineItemAction.ReportContent,
|
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
.sortedWith(TimelineItemActionComparator())
|
||||||
|
.toPersistentList()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ sealed class TimelineItemAction(
|
||||||
) {
|
) {
|
||||||
data object ViewInTimeline : TimelineItemAction(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on)
|
data object ViewInTimeline : TimelineItemAction(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on)
|
||||||
data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward)
|
data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward)
|
||||||
data object Copy : TimelineItemAction(CommonStrings.action_copy, CompoundDrawables.ic_compound_copy)
|
data object CopyText : TimelineItemAction(CommonStrings.action_copy_text, CompoundDrawables.ic_compound_copy)
|
||||||
|
data object CopyCaption : TimelineItemAction(CommonStrings.action_copy_caption, CompoundDrawables.ic_compound_copy)
|
||||||
data object CopyLink : TimelineItemAction(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link)
|
data object CopyLink : TimelineItemAction(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link)
|
||||||
data object Redact : TimelineItemAction(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true)
|
data object Redact : TimelineItemAction(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true)
|
||||||
data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply)
|
data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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.messages.impl.actionlist.model
|
||||||
|
|
||||||
|
class TimelineItemActionComparator : Comparator<TimelineItemAction> {
|
||||||
|
// See order in https://www.figma.com/design/ux3tYoZV9WghC7hHT9Fhk0/Compound-iOS-Components?node-id=2946-2392
|
||||||
|
private val orderedList = listOf(
|
||||||
|
TimelineItemAction.EndPoll,
|
||||||
|
TimelineItemAction.ViewInTimeline,
|
||||||
|
TimelineItemAction.Reply,
|
||||||
|
TimelineItemAction.ReplyInThread,
|
||||||
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
|
TimelineItemAction.Unpin,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
|
TimelineItemAction.AddCaption,
|
||||||
|
TimelineItemAction.EditCaption,
|
||||||
|
TimelineItemAction.CopyCaption,
|
||||||
|
TimelineItemAction.RemoveCaption,
|
||||||
|
TimelineItemAction.ViewSource,
|
||||||
|
TimelineItemAction.ReportContent,
|
||||||
|
TimelineItemAction.Redact,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun compare(o1: TimelineItemAction, o2: TimelineItemAction): Int {
|
||||||
|
val index1 = orderedList.indexOf(o1)
|
||||||
|
val index2 = orderedList.indexOf(o2)
|
||||||
|
return index1.compareTo(index2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,12 +20,14 @@ import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
class AttachmentsPreviewNode @AssistedInject constructor(
|
class AttachmentsPreviewNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
presenterFactory: AttachmentsPreviewPresenter.Factory,
|
presenterFactory: AttachmentsPreviewPresenter.Factory,
|
||||||
|
private val localMediaRenderer: LocalMediaRenderer,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
data class Inputs(val attachment: Attachment) : NodeInputs
|
data class Inputs(val attachment: Attachment) : NodeInputs
|
||||||
|
|
||||||
|
|
@ -46,6 +48,7 @@ class AttachmentsPreviewNode @AssistedInject constructor(
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
AttachmentsPreviewView(
|
AttachmentsPreviewView(
|
||||||
state = state,
|
state = state,
|
||||||
|
localMediaRenderer = localMediaRenderer,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@
|
||||||
package io.element.android.features.messages.impl.attachments.preview
|
package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
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
|
||||||
|
|
@ -19,10 +21,16 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
|
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
|
||||||
|
import io.element.android.libraries.androidutils.file.safeDelete
|
||||||
|
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.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
|
import io.element.android.libraries.mediaupload.api.allFiles
|
||||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState
|
import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
@ -39,6 +47,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
private val mediaSender: MediaSender,
|
private val mediaSender: MediaSender,
|
||||||
private val permalinkBuilder: PermalinkBuilder,
|
private val permalinkBuilder: PermalinkBuilder,
|
||||||
private val temporaryUriDeleter: TemporaryUriDeleter,
|
private val temporaryUriDeleter: TemporaryUriDeleter,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
) : Presenter<AttachmentsPreviewState> {
|
) : Presenter<AttachmentsPreviewState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -63,19 +72,64 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
||||||
|
|
||||||
|
val userSentAttachment = remember { mutableStateOf(false) }
|
||||||
|
val allowCaption by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionCreation).collectAsState(initial = false)
|
||||||
|
val showCaptionCompatibilityWarning by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionWarning).collectAsState(initial = false)
|
||||||
|
|
||||||
|
val mediaUploadInfoState = remember { mutableStateOf<AsyncData<MediaUploadInfo>>(AsyncData.Uninitialized) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
preProcessAttachment(
|
||||||
|
attachment,
|
||||||
|
mediaUploadInfoState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(userSentAttachment.value, mediaUploadInfoState.value) {
|
||||||
|
if (userSentAttachment.value) {
|
||||||
|
// User confirmed sending the attachment
|
||||||
|
when (val mediaUploadInfo = mediaUploadInfoState.value) {
|
||||||
|
is AsyncData.Success -> {
|
||||||
|
// Pre-processing is done, send the attachment
|
||||||
|
val caption = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
ongoingSendAttachmentJob.value = coroutineScope.launch {
|
||||||
|
sendPreProcessedMedia(
|
||||||
|
mediaUploadInfo = mediaUploadInfo.data,
|
||||||
|
caption = caption,
|
||||||
|
sendActionState = sendActionState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is AsyncData.Failure -> {
|
||||||
|
// Pre-processing has failed, show the error
|
||||||
|
sendActionState.value = SendActionState.Failure(mediaUploadInfo.error)
|
||||||
|
}
|
||||||
|
AsyncData.Uninitialized,
|
||||||
|
is AsyncData.Loading -> {
|
||||||
|
// Pre-processing is still in progress, do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
||||||
when (attachmentsPreviewEvents) {
|
when (attachmentsPreviewEvents) {
|
||||||
is AttachmentsPreviewEvents.SendAttachment -> {
|
is AttachmentsPreviewEvents.SendAttachment -> coroutineScope.launch {
|
||||||
val caption = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
|
val useSendQueue = featureFlagService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
|
||||||
.takeIf { it.isNotEmpty() }
|
userSentAttachment.value = true
|
||||||
ongoingSendAttachmentJob.value = coroutineScope.sendAttachment(
|
val instantSending = mediaUploadInfoState.value.isReady() && useSendQueue
|
||||||
attachment = attachment,
|
sendActionState.value = if (instantSending) {
|
||||||
caption = caption,
|
SendActionState.Sending.InstantSending
|
||||||
sendActionState = sendActionState,
|
} else {
|
||||||
)
|
SendActionState.Sending.Processing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AttachmentsPreviewEvents.Cancel -> {
|
AttachmentsPreviewEvents.Cancel -> {
|
||||||
coroutineScope.cancel(attachment)
|
coroutineScope.cancel(
|
||||||
|
attachment,
|
||||||
|
mediaUploadInfoState.value,
|
||||||
|
sendActionState,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
AttachmentsPreviewEvents.ClearSendState -> {
|
AttachmentsPreviewEvents.ClearSendState -> {
|
||||||
ongoingSendAttachmentJob.value?.let {
|
ongoingSendAttachmentJob.value?.let {
|
||||||
|
|
@ -91,60 +145,101 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
attachment = attachment,
|
attachment = attachment,
|
||||||
sendActionState = sendActionState.value,
|
sendActionState = sendActionState.value,
|
||||||
textEditorState = textEditorState,
|
textEditorState = textEditorState,
|
||||||
|
allowCaption = allowCaption,
|
||||||
|
showCaptionCompatibilityWarning = showCaptionCompatibilityWarning,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.sendAttachment(
|
private fun CoroutineScope.preProcessAttachment(
|
||||||
attachment: Attachment,
|
attachment: Attachment,
|
||||||
caption: String?,
|
mediaUploadInfoState: MutableState<AsyncData<MediaUploadInfo>>,
|
||||||
sendActionState: MutableState<SendActionState>,
|
|
||||||
) = launch {
|
) = launch {
|
||||||
when (attachment) {
|
when (attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
sendMedia(
|
preProcessMedia(
|
||||||
mediaAttachment = attachment,
|
mediaAttachment = attachment,
|
||||||
caption = caption,
|
mediaUploadInfoState = mediaUploadInfoState,
|
||||||
sendActionState = sendActionState,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun preProcessMedia(
|
||||||
|
mediaAttachment: Attachment.Media,
|
||||||
|
mediaUploadInfoState: MutableState<AsyncData<MediaUploadInfo>>,
|
||||||
|
) {
|
||||||
|
mediaUploadInfoState.value = AsyncData.Loading()
|
||||||
|
mediaSender.preProcessMedia(
|
||||||
|
uri = mediaAttachment.localMedia.uri,
|
||||||
|
mimeType = mediaAttachment.localMedia.info.mimeType,
|
||||||
|
).fold(
|
||||||
|
onSuccess = { mediaUploadInfo ->
|
||||||
|
mediaUploadInfoState.value = AsyncData.Success(mediaUploadInfo)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
Timber.e(it, "Failed to pre-process media")
|
||||||
|
if (it is CancellationException) {
|
||||||
|
throw it
|
||||||
|
} else {
|
||||||
|
mediaUploadInfoState.value = AsyncData.Failure(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.cancel(
|
private fun CoroutineScope.cancel(
|
||||||
attachment: Attachment,
|
attachment: Attachment,
|
||||||
|
mediaUploadInfo: AsyncData<MediaUploadInfo>,
|
||||||
|
sendActionState: MutableState<SendActionState>,
|
||||||
) = launch {
|
) = launch {
|
||||||
// Delete the temporary file
|
// Delete the temporary file
|
||||||
when (attachment) {
|
when (attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
temporaryUriDeleter.delete(attachment.localMedia.uri)
|
temporaryUriDeleter.delete(attachment.localMedia.uri)
|
||||||
|
mediaUploadInfo.dataOrNull()?.let { data ->
|
||||||
|
cleanUp(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Reset the sendActionState to ensure that dialog is closed before the screen
|
||||||
|
sendActionState.value = SendActionState.Done
|
||||||
onDoneListener()
|
onDoneListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sendMedia(
|
private fun cleanUp(
|
||||||
mediaAttachment: Attachment.Media,
|
mediaUploadInfo: MediaUploadInfo,
|
||||||
|
) {
|
||||||
|
mediaUploadInfo.allFiles().forEach { file ->
|
||||||
|
file.safeDelete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun sendPreProcessedMedia(
|
||||||
|
mediaUploadInfo: MediaUploadInfo,
|
||||||
caption: String?,
|
caption: String?,
|
||||||
sendActionState: MutableState<SendActionState>,
|
sendActionState: MutableState<SendActionState>,
|
||||||
) = runCatching {
|
) = runCatching {
|
||||||
val context = coroutineContext
|
val context = coroutineContext
|
||||||
val progressCallback = object : ProgressCallback {
|
val progressCallback = object : ProgressCallback {
|
||||||
override fun onProgress(current: Long, total: Long) {
|
override fun onProgress(current: Long, total: Long) {
|
||||||
|
// Note will not happen if useSendQueue is true
|
||||||
if (context.isActive) {
|
if (context.isActive) {
|
||||||
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat())
|
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sendActionState.value = SendActionState.Sending.Processing
|
mediaSender.sendPreProcessedMedia(
|
||||||
mediaSender.sendMedia(
|
mediaUploadInfo = mediaUploadInfo,
|
||||||
uri = mediaAttachment.localMedia.uri,
|
|
||||||
mimeType = mediaAttachment.localMedia.info.mimeType,
|
|
||||||
caption = caption,
|
caption = caption,
|
||||||
|
formattedCaption = null,
|
||||||
progressCallback = progressCallback
|
progressCallback = progressCallback
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
}.fold(
|
}.fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
|
cleanUp(mediaUploadInfo)
|
||||||
|
// Reset the sendActionState to ensure that dialog is closed before the screen
|
||||||
|
sendActionState.value = SendActionState.Done
|
||||||
onDoneListener()
|
onDoneListener()
|
||||||
},
|
},
|
||||||
onFailure = { error ->
|
onFailure = { error ->
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,10 @@ data class AttachmentsPreviewState(
|
||||||
val attachment: Attachment,
|
val attachment: Attachment,
|
||||||
val sendActionState: SendActionState,
|
val sendActionState: SendActionState,
|
||||||
val textEditorState: TextEditorState,
|
val textEditorState: TextEditorState,
|
||||||
|
val allowCaption: Boolean,
|
||||||
|
val showCaptionCompatibilityWarning: Boolean,
|
||||||
val eventSink: (AttachmentsPreviewEvents) -> Unit
|
val eventSink: (AttachmentsPreviewEvents) -> Unit
|
||||||
) {
|
)
|
||||||
// Keep the val to eventually set to false for some mimetypes.
|
|
||||||
val allowCaption: Boolean = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface SendActionState {
|
sealed interface SendActionState {
|
||||||
|
|
@ -27,9 +26,11 @@ sealed interface SendActionState {
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface Sending : SendActionState {
|
sealed interface Sending : SendActionState {
|
||||||
|
data object InstantSending : Sending
|
||||||
data object Processing : Sending
|
data object Processing : Sending
|
||||||
data class Uploading(val progress: Float) : Sending
|
data class Uploading(val progress: Float) : Sending
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Failure(val error: Throwable) : SendActionState
|
data class Failure(val error: Throwable) : SendActionState
|
||||||
|
data object Done : SendActionState
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,9 @@ package io.element.android.features.messages.impl.attachments.preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
|
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
|
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||||
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.aVideoMediaInfo
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.anAudioMediaInfo
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo
|
|
||||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
|
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
|
||||||
|
|
||||||
|
|
@ -23,11 +20,11 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider<Attachment
|
||||||
override val values: Sequence<AttachmentsPreviewState>
|
override val values: Sequence<AttachmentsPreviewState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
anAttachmentsPreviewState(),
|
anAttachmentsPreviewState(),
|
||||||
anAttachmentsPreviewState(mediaInfo = aVideoMediaInfo()),
|
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing),
|
||||||
anAttachmentsPreviewState(mediaInfo = anAudioMediaInfo()),
|
|
||||||
anAttachmentsPreviewState(mediaInfo = anApkMediaInfo()),
|
|
||||||
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
|
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
|
||||||
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))),
|
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))),
|
||||||
|
anAttachmentsPreviewState(allowCaption = false),
|
||||||
|
anAttachmentsPreviewState(showCaptionCompatibilityWarning = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,11 +32,15 @@ fun anAttachmentsPreviewState(
|
||||||
mediaInfo: MediaInfo = anImageMediaInfo(),
|
mediaInfo: MediaInfo = anImageMediaInfo(),
|
||||||
textEditorState: TextEditorState = aTextEditorStateMarkdown(),
|
textEditorState: TextEditorState = aTextEditorStateMarkdown(),
|
||||||
sendActionState: SendActionState = SendActionState.Idle,
|
sendActionState: SendActionState = SendActionState.Idle,
|
||||||
|
allowCaption: Boolean = true,
|
||||||
|
showCaptionCompatibilityWarning: Boolean = true,
|
||||||
) = AttachmentsPreviewState(
|
) = AttachmentsPreviewState(
|
||||||
attachment = Attachment.Media(
|
attachment = Attachment.Media(
|
||||||
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
||||||
),
|
),
|
||||||
sendActionState = sendActionState,
|
sendActionState = sendActionState,
|
||||||
textEditorState = textEditorState,
|
textEditorState = textEditorState,
|
||||||
|
allowCaption = allowCaption,
|
||||||
|
showCaptionCompatibilityWarning = showCaptionCompatibilityWarning,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,22 @@
|
||||||
package io.element.android.features.messages.impl.attachments.preview
|
package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
|
@ -34,20 +38,20 @@ import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMediaView
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState
|
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||||
|
import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer
|
||||||
import io.element.android.libraries.textcomposer.TextComposer
|
import io.element.android.libraries.textcomposer.TextComposer
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.wysiwyg.display.TextDisplay
|
import io.element.android.wysiwyg.display.TextDisplay
|
||||||
import me.saket.telephoto.zoomable.ZoomSpec
|
|
||||||
import me.saket.telephoto.zoomable.rememberZoomableState
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AttachmentsPreviewView(
|
fun AttachmentsPreviewView(
|
||||||
state: AttachmentsPreviewState,
|
state: AttachmentsPreviewState,
|
||||||
|
localMediaRenderer: LocalMediaRenderer,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun postSendAttachment() {
|
fun postSendAttachment() {
|
||||||
|
|
@ -79,9 +83,11 @@ fun AttachmentsPreviewView(
|
||||||
title = {},
|
title = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) { paddingValues ->
|
||||||
AttachmentPreviewContent(
|
AttachmentPreviewContent(
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
state = state,
|
state = state,
|
||||||
|
localMediaRenderer = localMediaRenderer,
|
||||||
onSendClick = ::postSendAttachment,
|
onSendClick = ::postSendAttachment,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -99,12 +105,17 @@ private fun AttachmentSendStateView(
|
||||||
onRetryClick: () -> Unit
|
onRetryClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
when (sendActionState) {
|
when (sendActionState) {
|
||||||
is SendActionState.Sending -> {
|
is SendActionState.Sending.Processing -> {
|
||||||
ProgressDialog(
|
ProgressDialog(
|
||||||
type = when (sendActionState) {
|
type = ProgressDialogType.Indeterminate,
|
||||||
is SendActionState.Sending.Uploading -> ProgressDialogType.Determinate(sendActionState.progress)
|
text = stringResource(id = CommonStrings.common_sending),
|
||||||
SendActionState.Sending.Processing -> ProgressDialogType.Indeterminate
|
showCancelButton = true,
|
||||||
},
|
onDismissRequest = onDismissClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SendActionState.Sending.Uploading -> {
|
||||||
|
ProgressDialog(
|
||||||
|
type = ProgressDialogType.Determinate(sendActionState.progress),
|
||||||
text = stringResource(id = CommonStrings.common_sending),
|
text = stringResource(id = CommonStrings.common_sending),
|
||||||
showCancelButton = true,
|
showCancelButton = true,
|
||||||
onDismissRequest = onDismissClick,
|
onDismissRequest = onDismissClick,
|
||||||
|
|
@ -124,30 +135,23 @@ private fun AttachmentSendStateView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun AttachmentPreviewContent(
|
private fun AttachmentPreviewContent(
|
||||||
state: AttachmentsPreviewState,
|
state: AttachmentsPreviewState,
|
||||||
|
localMediaRenderer: LocalMediaRenderer,
|
||||||
onSendClick: () -> Unit,
|
onSendClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(
|
Column(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.navigationBarsPadding(),
|
.navigationBarsPadding(),
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.weight(1f),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
when (val attachment = state.attachment) {
|
when (val attachment = state.attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
val localMediaViewState = rememberLocalMediaViewState(
|
localMediaRenderer.Render(attachment.localMedia)
|
||||||
zoomableState = rememberZoomableState(
|
|
||||||
zoomSpec = ZoomSpec(maxZoomFactor = 4f, preventOverOrUnderZoom = false)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
LocalMediaView(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
localMedia = attachment.localMedia,
|
|
||||||
localMediaViewState = localMediaViewState,
|
|
||||||
onClick = {}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +162,6 @@ private fun AttachmentPreviewContent(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(ElementTheme.colors.bgCanvasDefault)
|
.background(ElementTheme.colors.bgCanvasDefault)
|
||||||
.height(IntrinsicSize.Min)
|
.height(IntrinsicSize.Min)
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.imePadding(),
|
.imePadding(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -174,7 +177,10 @@ private fun AttachmentsPreviewBottomActions(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
state = state.textEditorState,
|
state = state.textEditorState,
|
||||||
voiceMessageState = VoiceMessageState.Idle,
|
voiceMessageState = VoiceMessageState.Idle,
|
||||||
composerMode = MessageComposerMode.Attachment(state.allowCaption),
|
composerMode = MessageComposerMode.Attachment(
|
||||||
|
allowCaption = state.allowCaption,
|
||||||
|
showCaptionCompatibilityWarning = state.showCaptionCompatibilityWarning,
|
||||||
|
),
|
||||||
onRequestFocus = {},
|
onRequestFocus = {},
|
||||||
onSendMessage = onSendClick,
|
onSendMessage = onSendClick,
|
||||||
showTextFormatting = false,
|
showTextFormatting = false,
|
||||||
|
|
@ -200,5 +206,15 @@ private fun AttachmentsPreviewBottomActions(
|
||||||
internal fun AttachmentsPreviewViewPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = ElementPreviewDark {
|
internal fun AttachmentsPreviewViewPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = ElementPreviewDark {
|
||||||
AttachmentsPreviewView(
|
AttachmentsPreviewView(
|
||||||
state = state,
|
state = state,
|
||||||
|
localMediaRenderer = object : LocalMediaRenderer {
|
||||||
|
@Composable
|
||||||
|
override fun Render(localMedia: LocalMedia) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = CommonDrawables.sample_background),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
@ -35,6 +36,7 @@ class PinnedMessagesListNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
presenterFactory: PinnedMessagesListPresenter.Factory,
|
presenterFactory: PinnedMessagesListPresenter.Factory,
|
||||||
|
actionListPresenterFactory: ActionListPresenter.Factory,
|
||||||
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator {
|
) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator {
|
||||||
|
|
@ -47,7 +49,10 @@ class PinnedMessagesListNode @AssistedInject constructor(
|
||||||
fun onForwardEventClick(eventId: EventId)
|
fun onForwardEventClick(eventId: EventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val presenter = presenterFactory.create(this)
|
private val presenter = presenterFactory.create(
|
||||||
|
navigator = this,
|
||||||
|
actionListPresenter = actionListPresenterFactory.create(PinnedMessagesListTimelineActionPostProcessor())
|
||||||
|
)
|
||||||
private val callbacks = plugins<Callback>()
|
private val callbacks = plugins<Callback>()
|
||||||
|
|
||||||
private fun onEventClick(event: TimelineItem.Event) {
|
private fun onEventClick(event: TimelineItem.Event) {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.Interaction
|
import im.vector.app.features.analytics.plan.Interaction
|
||||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||||
import io.element.android.features.messages.impl.UserEventPermissions
|
import io.element.android.features.messages.impl.UserEventPermissions
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
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.pinned.PinnedEventsTimelineProvider
|
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||||
|
|
@ -64,13 +64,16 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
||||||
private val timelineProvider: PinnedEventsTimelineProvider,
|
private val timelineProvider: PinnedEventsTimelineProvider,
|
||||||
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
actionListPresenterFactory: ActionListPresenter.Factory,
|
@Assisted private val actionListPresenter: Presenter<ActionListState>,
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
) : Presenter<PinnedMessagesListState> {
|
) : Presenter<PinnedMessagesListState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(navigator: PinnedMessagesListNavigator): PinnedMessagesListPresenter
|
fun create(
|
||||||
|
navigator: PinnedMessagesListNavigator,
|
||||||
|
actionListPresenter: Presenter<ActionListState>,
|
||||||
|
): PinnedMessagesListPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val timelineItemsFactory: TimelineItemsFactory = timelineItemsFactoryCreator.create(
|
private val timelineItemsFactory: TimelineItemsFactory = timelineItemsFactoryCreator.create(
|
||||||
|
|
@ -79,7 +82,6 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
||||||
computeReactions = false,
|
computeReactions = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
private val actionListPresenter = actionListPresenterFactory.create(PinnedMessagesListTimelineActionPostProcessor())
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PinnedMessagesListState {
|
override fun present(): PinnedMessagesListState {
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ class DefaultHtmlConverterProvider @Inject constructor(
|
||||||
return TextDisplay.Custom(mentionSpan)
|
return TextDisplay.Custom(mentionSpan)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isEditor = false,
|
||||||
isMention = { _, url -> mentionDetector?.isMention(url).orFalse() }
|
isMention = { _, url -> mentionDetector?.isMention(url).orFalse() }
|
||||||
).apply {
|
).apply {
|
||||||
configureWith(editorStyle)
|
configureWith(editorStyle)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,15 @@ fun TimelineItemEncryptedView(
|
||||||
UtdCause.UnknownDevice -> {
|
UtdCause.UnknownDevice -> {
|
||||||
CommonStrings.common_unable_to_decrypt_insecure_device to CompoundDrawables.ic_compound_block
|
CommonStrings.common_unable_to_decrypt_insecure_device to CompoundDrawables.ic_compound_block
|
||||||
}
|
}
|
||||||
|
UtdCause.HistoricalMessage -> {
|
||||||
|
CommonStrings.timeline_decryption_failure_historical_event_no_key_backup to CompoundDrawables.ic_compound_block
|
||||||
|
}
|
||||||
|
UtdCause.WithheldUnverifiedOrInsecureDevice -> {
|
||||||
|
CommonStrings.timeline_decryption_failure_withheld_unverified to CompoundDrawables.ic_compound_block
|
||||||
|
}
|
||||||
|
UtdCause.WithheldBySender -> {
|
||||||
|
CommonStrings.timeline_decryption_failure_unable_to_decrypt to CompoundDrawables.ic_compound_error
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
CommonStrings.common_waiting_for_decryption_key to CompoundDrawables.ic_compound_time
|
CommonStrings.common_waiting_for_decryption_key to CompoundDrawables.ic_compound_time
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components.event
|
package io.element.android.features.messages.impl.timeline.components.event
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -44,14 +46,18 @@ fun TimelineItemInformativeView(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Box(
|
||||||
resourceId = iconResourceId,
|
contentAlignment = Alignment.Center,
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
modifier = Modifier.height(20.dp)
|
||||||
contentDescription = iconDescription,
|
) {
|
||||||
modifier = Modifier.size(16.dp)
|
Icon(
|
||||||
)
|
resourceId = iconResourceId,
|
||||||
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
|
contentDescription = iconDescription,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
fontStyle = FontStyle.Italic,
|
fontStyle = FontStyle.Italic,
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
open class ReactionSummaryStateProvider : PreviewParameterProvider<ReactionSummaryState> {
|
open class ReactionSummaryStateProvider : PreviewParameterProvider<ReactionSummaryState> {
|
||||||
override val values = sequenceOf(anActionListState())
|
override val values = sequenceOf(aReactionSummaryState())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun anActionListState(): ReactionSummaryState {
|
fun aReactionSummaryState(): ReactionSummaryState {
|
||||||
val reactions = aTimelineItemReactions(8, true).reactions
|
val reactions = aTimelineItemReactions(8, true).reactions
|
||||||
return ReactionSummaryState(
|
return ReactionSummaryState(
|
||||||
target = ReactionSummaryState.Summary(
|
target = ReactionSummaryState.Summary(
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
thumbnailSource = messageType.info?.thumbnailSource,
|
thumbnailSource = messageType.info?.thumbnailSource,
|
||||||
videoSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||||
width = messageType.info?.width?.toInt(),
|
width = messageType.info?.width?.toInt(),
|
||||||
height = messageType.info?.height?.toInt(),
|
height = messageType.info?.height?.toInt(),
|
||||||
|
|
@ -186,6 +186,8 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||||
waveform = messageType.details?.waveform?.toImmutableList() ?: persistentListOf(),
|
waveform = messageType.details?.waveform?.toImmutableList() ?: persistentListOf(),
|
||||||
|
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||||
|
fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
false -> {
|
false -> {
|
||||||
|
|
@ -211,7 +213,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
thumbnailSource = messageType.info?.thumbnailSource,
|
thumbnailSource = messageType.info?.thumbnailSource,
|
||||||
fileSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
|
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
|
||||||
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
|
||||||
fileExtension = fileExtension
|
fileExtension = fileExtension
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ data class TimelineItemAudioContent(
|
||||||
override val formattedCaption: CharSequence?,
|
override val formattedCaption: CharSequence?,
|
||||||
override val isEdited: Boolean,
|
override val isEdited: Boolean,
|
||||||
val duration: Duration,
|
val duration: Duration,
|
||||||
val mediaSource: MediaSource,
|
override val mediaSource: MediaSource,
|
||||||
val mimeType: String,
|
override val mimeType: String,
|
||||||
val formattedFileSize: String,
|
override val formattedFileSize: String,
|
||||||
val fileExtension: String,
|
override val fileExtension: String,
|
||||||
) : TimelineItemEventContentWithAttachment {
|
) : TimelineItemEventContentWithAttachment {
|
||||||
val fileExtensionAndSize =
|
val fileExtensionAndSize =
|
||||||
formatFileExtensionAndSize(
|
formatFileExtensionAndSize(
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,24 @@ open class TimelineItemEncryptedContentProvider : PreviewParameterProvider<Timel
|
||||||
utdCause = UtdCause.UnsignedDevice,
|
utdCause = UtdCause.UnsignedDevice,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
aTimelineItemEncryptedContent(
|
||||||
|
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||||
|
sessionId = "sessionId",
|
||||||
|
utdCause = UtdCause.HistoricalMessage,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aTimelineItemEncryptedContent(
|
||||||
|
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||||
|
sessionId = "sessionId",
|
||||||
|
utdCause = UtdCause.WithheldUnverifiedOrInsecureDevice,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aTimelineItemEncryptedContent(
|
||||||
|
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||||
|
sessionId = "sessionId",
|
||||||
|
utdCause = UtdCause.WithheldBySender,
|
||||||
|
)
|
||||||
|
),
|
||||||
aTimelineItemEncryptedContent(
|
aTimelineItemEncryptedContent(
|
||||||
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
data = UnableToDecryptContent.Data.MegolmV1AesSha2(
|
||||||
sessionId = "sessionId",
|
sessionId = "sessionId",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
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.media.MediaSource
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface TimelineItemEventContent {
|
sealed interface TimelineItemEventContent {
|
||||||
|
|
@ -26,6 +27,10 @@ sealed interface TimelineItemEventContentWithAttachment :
|
||||||
val filename: String
|
val filename: String
|
||||||
val caption: String?
|
val caption: String?
|
||||||
val formattedCaption: CharSequence?
|
val formattedCaption: CharSequence?
|
||||||
|
val mediaSource: MediaSource
|
||||||
|
val mimeType: String
|
||||||
|
val formattedFileSize: String
|
||||||
|
val fileExtension: String
|
||||||
|
|
||||||
val bestDescription: String
|
val bestDescription: String
|
||||||
get() = caption ?: filename
|
get() = caption ?: filename
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@ data class TimelineItemFileContent(
|
||||||
override val caption: String?,
|
override val caption: String?,
|
||||||
override val formattedCaption: CharSequence?,
|
override val formattedCaption: CharSequence?,
|
||||||
override val isEdited: Boolean,
|
override val isEdited: Boolean,
|
||||||
val fileSource: MediaSource,
|
override val mediaSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
val formattedFileSize: String,
|
override val formattedFileSize: String,
|
||||||
val fileExtension: String,
|
override val fileExtension: String,
|
||||||
val mimeType: String,
|
override val mimeType: String,
|
||||||
) : TimelineItemEventContentWithAttachment {
|
) : TimelineItemEventContentWithAttachment {
|
||||||
override val type: String = "TimelineItemFileContent"
|
override val type: String = "TimelineItemFileContent"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ fun aTimelineItemFileContent(
|
||||||
formattedCaption = null,
|
formattedCaption = null,
|
||||||
isEdited = false,
|
isEdited = false,
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
fileSource = MediaSource(url = ""),
|
mediaSource = MediaSource(url = ""),
|
||||||
mimeType = MimeTypes.Pdf,
|
mimeType = MimeTypes.Pdf,
|
||||||
formattedFileSize = "100kB",
|
formattedFileSize = "100kB",
|
||||||
fileExtension = "pdf"
|
fileExtension = "pdf"
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ data class TimelineItemImageContent(
|
||||||
override val caption: String?,
|
override val caption: String?,
|
||||||
override val formattedCaption: CharSequence?,
|
override val formattedCaption: CharSequence?,
|
||||||
override val isEdited: Boolean,
|
override val isEdited: Boolean,
|
||||||
val mediaSource: MediaSource,
|
override val mediaSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
val formattedFileSize: String,
|
override val formattedFileSize: String,
|
||||||
val fileExtension: String,
|
override val fileExtension: String,
|
||||||
val mimeType: String,
|
override val mimeType: String,
|
||||||
val blurhash: String?,
|
val blurhash: String?,
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
val height: Int?,
|
val height: Int?,
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,11 @@ data class TimelineItemStickerContent(
|
||||||
override val caption: String?,
|
override val caption: String?,
|
||||||
override val formattedCaption: CharSequence?,
|
override val formattedCaption: CharSequence?,
|
||||||
override val isEdited: Boolean,
|
override val isEdited: Boolean,
|
||||||
val mediaSource: MediaSource,
|
override val mediaSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
val formattedFileSize: String,
|
override val formattedFileSize: String,
|
||||||
val fileExtension: String,
|
override val fileExtension: String,
|
||||||
val mimeType: String,
|
override val mimeType: String,
|
||||||
val blurhash: String?,
|
val blurhash: String?,
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
val height: Int?,
|
val height: Int?,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ data class TimelineItemVideoContent(
|
||||||
override val formattedCaption: CharSequence?,
|
override val formattedCaption: CharSequence?,
|
||||||
override val isEdited: Boolean,
|
override val isEdited: Boolean,
|
||||||
val duration: Duration,
|
val duration: Duration,
|
||||||
val videoSource: MediaSource,
|
override val mediaSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
val aspectRatio: Float?,
|
val aspectRatio: Float?,
|
||||||
val blurHash: String?,
|
val blurHash: String?,
|
||||||
|
|
@ -24,9 +24,9 @@ data class TimelineItemVideoContent(
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
val thumbnailWidth: Int?,
|
val thumbnailWidth: Int?,
|
||||||
val thumbnailHeight: Int?,
|
val thumbnailHeight: Int?,
|
||||||
val mimeType: String,
|
override val mimeType: String,
|
||||||
val formattedFileSize: String,
|
override val formattedFileSize: String,
|
||||||
val fileExtension: String,
|
override val fileExtension: String,
|
||||||
) : TimelineItemEventContentWithAttachment {
|
) : TimelineItemEventContentWithAttachment {
|
||||||
override val type: String = "TimelineItemImageContent"
|
override val type: String = "TimelineItemImageContent"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ fun aTimelineItemVideoContent(
|
||||||
blurHash = blurhash,
|
blurHash = blurhash,
|
||||||
aspectRatio = aspectRatio,
|
aspectRatio = aspectRatio,
|
||||||
duration = 100.milliseconds,
|
duration = 100.milliseconds,
|
||||||
videoSource = MediaSource(""),
|
mediaSource = MediaSource(""),
|
||||||
width = 150,
|
width = 150,
|
||||||
height = 300,
|
height = 300,
|
||||||
thumbnailWidth = 150,
|
thumbnailWidth = 150,
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ data class TimelineItemVoiceContent(
|
||||||
override val formattedCaption: CharSequence?,
|
override val formattedCaption: CharSequence?,
|
||||||
override val isEdited: Boolean,
|
override val isEdited: Boolean,
|
||||||
val duration: Duration,
|
val duration: Duration,
|
||||||
val mediaSource: MediaSource,
|
override val mediaSource: MediaSource,
|
||||||
val mimeType: String,
|
override val formattedFileSize: String,
|
||||||
|
override val fileExtension: String,
|
||||||
|
override val mimeType: String,
|
||||||
val waveform: ImmutableList<Float>,
|
val waveform: ImmutableList<Float>,
|
||||||
) : TimelineItemEventContentWithAttachment {
|
) : TimelineItemEventContentWithAttachment {
|
||||||
override val type: String = "TimelineItemAudioContent"
|
override val type: String = "TimelineItemAudioContent"
|
||||||
|
|
|
||||||
|
|
@ -53,4 +53,6 @@ fun aTimelineItemVoiceContent(
|
||||||
mediaSource = mediaSource,
|
mediaSource = mediaSource,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
waveform = waveform.toPersistentList(),
|
waveform = waveform.toPersistentList(),
|
||||||
|
formattedFileSize = "1.0 MB",
|
||||||
|
fileExtension = "ogg",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
<string name="screen_room_timeline_add_reaction">"Emoji hinzufügen"</string>
|
<string name="screen_room_timeline_add_reaction">"Emoji hinzufügen"</string>
|
||||||
<string name="screen_room_timeline_beginning_of_room">"Dies ist der Anfang von %1$s."</string>
|
<string name="screen_room_timeline_beginning_of_room">"Dies ist der Anfang von %1$s."</string>
|
||||||
<string name="screen_room_timeline_beginning_of_room_no_name">"Dies ist der Anfang dieses Gesprächs."</string>
|
<string name="screen_room_timeline_beginning_of_room_no_name">"Dies ist der Anfang dieses Gesprächs."</string>
|
||||||
|
<string name="screen_room_timeline_legacy_call">"Anruftyp wird nicht unterstützt. Fragen Sie nach, ob der Anrufer die neue Element X-App verwenden kann."</string>
|
||||||
<string name="screen_room_timeline_less_reactions">"Weniger anzeigen"</string>
|
<string name="screen_room_timeline_less_reactions">"Weniger anzeigen"</string>
|
||||||
<string name="screen_room_timeline_message_copied">"Nachricht wurde kopiert"</string>
|
<string name="screen_room_timeline_message_copied">"Nachricht wurde kopiert"</string>
|
||||||
<string name="screen_room_timeline_no_permission_to_post">"Du bist nicht berechtigt, in diesem Raum zu schreiben"</string>
|
<string name="screen_room_timeline_no_permission_to_post">"Du bist nicht berechtigt, in diesem Raum zu schreiben"</string>
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
<string name="screen_room_timeline_add_reaction">"Ajouter un émoji"</string>
|
<string name="screen_room_timeline_add_reaction">"Ajouter un émoji"</string>
|
||||||
<string name="screen_room_timeline_beginning_of_room">"Ceci est le début de %1$s."</string>
|
<string name="screen_room_timeline_beginning_of_room">"Ceci est le début de %1$s."</string>
|
||||||
<string name="screen_room_timeline_beginning_of_room_no_name">"Ceci est le début de cette conversation."</string>
|
<string name="screen_room_timeline_beginning_of_room_no_name">"Ceci est le début de cette conversation."</string>
|
||||||
|
<string name="screen_room_timeline_legacy_call">"Appel non pris en charge. Demandez à l’appelant s’il peut utiliser la nouvelle application Element X pour vous appeler."</string>
|
||||||
<string name="screen_room_timeline_less_reactions">"Afficher moins"</string>
|
<string name="screen_room_timeline_less_reactions">"Afficher moins"</string>
|
||||||
<string name="screen_room_timeline_message_copied">"Message copié"</string>
|
<string name="screen_room_timeline_message_copied">"Message copié"</string>
|
||||||
<string name="screen_room_timeline_no_permission_to_post">"Vous n’êtes pas autorisé à publier dans ce salon"</string>
|
<string name="screen_room_timeline_no_permission_to_post">"Vous n’êtes pas autorisé à publier dans ce salon"</string>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||||
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.crypto.identity.anIdentityChangeState
|
import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState
|
||||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
|
|
@ -45,6 +45,8 @@ import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
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.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -242,7 +244,7 @@ class MessagesPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Copy, event))
|
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.CopyText, event))
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
assertThat(clipboardHelper.clipboardContents).isEqualTo((event.content as TimelineItemTextContent).body)
|
assertThat(clipboardHelper.clipboardContents).isEqualTo((event.content as TimelineItemTextContent).body)
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +371,7 @@ class MessagesPresenterTest {
|
||||||
formattedCaption = null,
|
formattedCaption = null,
|
||||||
isEdited = false,
|
isEdited = false,
|
||||||
duration = 10.milliseconds,
|
duration = 10.milliseconds,
|
||||||
videoSource = MediaSource(AN_AVATAR_URL),
|
mediaSource = MediaSource(AN_AVATAR_URL),
|
||||||
thumbnailSource = MediaSource(AN_AVATAR_URL),
|
thumbnailSource = MediaSource(AN_AVATAR_URL),
|
||||||
mimeType = MimeTypes.Mp4,
|
mimeType = MimeTypes.Mp4,
|
||||||
blurHash = null,
|
blurHash = null,
|
||||||
|
|
@ -411,7 +413,7 @@ class MessagesPresenterTest {
|
||||||
caption = null,
|
caption = null,
|
||||||
isEdited = false,
|
isEdited = false,
|
||||||
formattedCaption = null,
|
formattedCaption = null,
|
||||||
fileSource = MediaSource(AN_AVATAR_URL),
|
mediaSource = MediaSource(AN_AVATAR_URL),
|
||||||
thumbnailSource = MediaSource(AN_AVATAR_URL),
|
thumbnailSource = MediaSource(AN_AVATAR_URL),
|
||||||
formattedFileSize = "10 MB",
|
formattedFileSize = "10 MB",
|
||||||
mimeType = MimeTypes.Pdf,
|
mimeType = MimeTypes.Pdf,
|
||||||
|
|
@ -1007,6 +1009,37 @@ class MessagesPresenterTest {
|
||||||
composerMode = MessageComposerMode.EditCaption(
|
composerMode = MessageComposerMode.EditCaption(
|
||||||
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
||||||
content = A_CAPTION,
|
content = A_CAPTION,
|
||||||
|
showCaptionCompatibilityWarning = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action edit caption without warning`() = runTest {
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
content = aTimelineItemImageContent(
|
||||||
|
caption = A_CAPTION,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val composerRecorder = EventsRecorder<MessageComposerEvents>()
|
||||||
|
val presenter = createMessagesPresenter(
|
||||||
|
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.MediaCaptionWarning.key to false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EditCaption, messageEvent))
|
||||||
|
awaitItem()
|
||||||
|
composerRecorder.assertSingle(
|
||||||
|
MessageComposerEvents.SetMode(
|
||||||
|
composerMode = MessageComposerMode.EditCaption(
|
||||||
|
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
||||||
|
content = A_CAPTION,
|
||||||
|
showCaptionCompatibilityWarning = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -1033,6 +1066,37 @@ class MessagesPresenterTest {
|
||||||
composerMode = MessageComposerMode.EditCaption(
|
composerMode = MessageComposerMode.EditCaption(
|
||||||
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
||||||
content = "",
|
content = "",
|
||||||
|
showCaptionCompatibilityWarning = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action add caption without warning`() = runTest {
|
||||||
|
val composerRecorder = EventsRecorder<MessageComposerEvents>()
|
||||||
|
val presenter = createMessagesPresenter(
|
||||||
|
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.MediaCaptionWarning.key to false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
content = aTimelineItemImageContent(
|
||||||
|
caption = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.AddCaption, messageEvent))
|
||||||
|
awaitItem()
|
||||||
|
composerRecorder.assertSingle(
|
||||||
|
MessageComposerEvents.SetMode(
|
||||||
|
composerMode = MessageComposerMode.EditCaption(
|
||||||
|
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
||||||
|
content = "",
|
||||||
|
showCaptionCompatibilityWarning = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -1097,6 +1161,7 @@ class MessagesPresenterTest {
|
||||||
givenRoomInfo(aRoomInfo(id = roomId, name = ""))
|
givenRoomInfo(aRoomInfo(id = roomId, name = ""))
|
||||||
},
|
},
|
||||||
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
||||||
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
||||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||||
timelineEventSink: (TimelineEvents) -> Unit = {},
|
timelineEventSink: (TimelineEvents) -> Unit = {},
|
||||||
|
|
@ -1109,14 +1174,13 @@ class MessagesPresenterTest {
|
||||||
},
|
},
|
||||||
actionListEventSink: (ActionListEvents) -> Unit = {},
|
actionListEventSink: (ActionListEvents) -> Unit = {},
|
||||||
): MessagesPresenter {
|
): MessagesPresenter {
|
||||||
val featureFlagService = FakeFeatureFlagService()
|
|
||||||
return MessagesPresenter(
|
return MessagesPresenter(
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
composerPresenter = messageComposerPresenter,
|
composerPresenter = messageComposerPresenter,
|
||||||
voiceMessageComposerPresenter = { aVoiceMessageComposerState() },
|
voiceMessageComposerPresenter = { aVoiceMessageComposerState() },
|
||||||
timelinePresenter = { aTimelineState(eventSink = timelineEventSink) },
|
timelinePresenter = { aTimelineState(eventSink = timelineEventSink) },
|
||||||
timelineProtectionPresenter = { aTimelineProtectionState() },
|
timelineProtectionPresenter = { aTimelineProtectionState() },
|
||||||
actionListPresenterFactory = FakeActionListPresenter.Factory(actionListEventSink),
|
actionListPresenter = { anActionListState(eventSink = actionListEventSink) },
|
||||||
customReactionPresenter = { aCustomReactionState() },
|
customReactionPresenter = { aCustomReactionState() },
|
||||||
reactionSummaryPresenter = { aReactionSummaryState() },
|
reactionSummaryPresenter = { aReactionSummaryState() },
|
||||||
readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() },
|
readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() },
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
||||||
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
|
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
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.timeline.item.event.LocalEventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
|
@ -174,8 +176,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
)
|
)
|
||||||
|
|
@ -219,8 +221,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.ReplyInThread,
|
TimelineItemAction.ReplyInThread,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
)
|
)
|
||||||
|
|
@ -266,8 +268,8 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
)
|
)
|
||||||
|
|
@ -312,8 +314,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
|
|
@ -359,8 +361,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
|
|
@ -406,10 +408,10 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Edit,
|
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -451,10 +453,10 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.ReplyInThread,
|
TimelineItemAction.ReplyInThread,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Edit,
|
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -499,10 +501,10 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Edit,
|
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -545,7 +547,58 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.AddCaption,
|
TimelineItemAction.AddCaption,
|
||||||
|
TimelineItemAction.ViewSource,
|
||||||
|
TimelineItemAction.Redact,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - compute for a media item - caption disabled`() = runTest {
|
||||||
|
val presenter = createActionListPresenter(
|
||||||
|
isDeveloperModeEnabled = true,
|
||||||
|
isPinFeatureEnabled = true,
|
||||||
|
allowCaption = false,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
isMine = true,
|
||||||
|
isEditable = true,
|
||||||
|
content = aTimelineItemImageContent(),
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
userEventPermissions = aUserEventPermissions(
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.target).isEqualTo(
|
||||||
|
ActionListState.Target.Success(
|
||||||
|
event = messageEvent,
|
||||||
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
|
actions = persistentListOf(
|
||||||
|
TimelineItemAction.Reply,
|
||||||
|
TimelineItemAction.Forward,
|
||||||
|
// Not here
|
||||||
|
// TimelineItemAction.AddCaption,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
|
|
@ -593,10 +646,11 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.EditCaption,
|
|
||||||
TimelineItemAction.RemoveCaption,
|
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.EditCaption,
|
||||||
|
TimelineItemAction.CopyCaption,
|
||||||
|
TimelineItemAction.RemoveCaption,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -607,6 +661,54 @@ class ActionListPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - compute for a media with caption item - other user event`() = runTest {
|
||||||
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
isMine = false,
|
||||||
|
isEditable = false,
|
||||||
|
content = aTimelineItemImageContent(
|
||||||
|
caption = A_CAPTION,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = messageEvent,
|
||||||
|
userEventPermissions = aUserEventPermissions(
|
||||||
|
canRedactOwn = true,
|
||||||
|
canRedactOther = false,
|
||||||
|
canSendMessage = true,
|
||||||
|
canSendReaction = true,
|
||||||
|
canPinUnpin = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.target).isEqualTo(
|
||||||
|
ActionListState.Target.Success(
|
||||||
|
event = messageEvent,
|
||||||
|
displayEmojiReactions = true,
|
||||||
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
|
actions = persistentListOf(
|
||||||
|
TimelineItemAction.Reply,
|
||||||
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.CopyCaption,
|
||||||
|
TimelineItemAction.ViewSource,
|
||||||
|
TimelineItemAction.ReportContent,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for a state item in debug build`() = runTest {
|
fun `present - compute for a state item in debug build`() = runTest {
|
||||||
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
|
||||||
|
|
@ -711,10 +813,10 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Edit,
|
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -758,9 +860,9 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Edit,
|
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -812,10 +914,10 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Forward,
|
TimelineItemAction.Forward,
|
||||||
TimelineItemAction.Edit,
|
|
||||||
TimelineItemAction.Unpin,
|
TimelineItemAction.Unpin,
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
|
|
@ -908,7 +1010,7 @@ class ActionListPresenterTest {
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -947,11 +1049,11 @@ class ActionListPresenterTest {
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
|
||||||
TimelineItemAction.Edit,
|
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -990,8 +1092,8 @@ class ActionListPresenterTest {
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Reply,
|
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
|
|
@ -1052,7 +1154,9 @@ class ActionListPresenterTest {
|
||||||
val messageEvent = aMessageEvent(
|
val messageEvent = aMessageEvent(
|
||||||
isMine = true,
|
isMine = true,
|
||||||
isEditable = false,
|
isEditable = false,
|
||||||
content = aTimelineItemVoiceContent(),
|
content = aTimelineItemVoiceContent(
|
||||||
|
caption = null,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(
|
initialState.eventSink.invoke(
|
||||||
ActionListEvents.ComputeForMessage(
|
ActionListEvents.ComputeForMessage(
|
||||||
|
|
@ -1151,6 +1255,7 @@ private fun createActionListPresenter(
|
||||||
isDeveloperModeEnabled: Boolean,
|
isDeveloperModeEnabled: Boolean,
|
||||||
isPinFeatureEnabled: Boolean,
|
isPinFeatureEnabled: Boolean,
|
||||||
room: MatrixRoom = FakeMatrixRoom(),
|
room: MatrixRoom = FakeMatrixRoom(),
|
||||||
|
allowCaption: Boolean = true,
|
||||||
): ActionListPresenter {
|
): ActionListPresenter {
|
||||||
val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
|
val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
|
||||||
return DefaultActionListPresenter(
|
return DefaultActionListPresenter(
|
||||||
|
|
@ -1158,6 +1263,11 @@ private fun createActionListPresenter(
|
||||||
appPreferencesStore = preferencesStore,
|
appPreferencesStore = preferencesStore,
|
||||||
isPinnedMessagesFeatureEnabled = { isPinFeatureEnabled },
|
isPinnedMessagesFeatureEnabled = { isPinFeatureEnabled },
|
||||||
room = room,
|
room = room,
|
||||||
userSendFailureFactory = VerifiedUserSendFailureFactory(room)
|
userSendFailureFactory = VerifiedUserSendFailureFactory(room),
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(
|
||||||
|
FeatureFlags.MediaCaptionCreation.key to allowCaption,
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.messages.impl.actionlist
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
|
|
||||||
|
|
||||||
class FakeActionListPresenter(private val eventSink: (ActionListEvents) -> Unit = {}) : ActionListPresenter {
|
|
||||||
class Factory(private val eventSink: (ActionListEvents) -> Unit = {}) : ActionListPresenter.Factory {
|
|
||||||
override fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter {
|
|
||||||
return FakeActionListPresenter(eventSink)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun present(): ActionListState {
|
|
||||||
return anActionListState(eventSink = eventSink)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -20,6 +20,8 @@ import io.element.android.features.messages.impl.attachments.preview.OnDoneListe
|
||||||
import io.element.android.features.messages.impl.attachments.preview.SendActionState
|
import io.element.android.features.messages.impl.attachments.preview.SendActionState
|
||||||
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
|
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
|
||||||
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
|
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||||
|
|
@ -40,10 +42,12 @@ import io.element.android.libraries.preferences.test.InMemorySessionPreferencesS
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.fake.FakeTemporaryUriDeleter
|
import io.element.android.tests.testutils.fake.FakeTemporaryUriDeleter
|
||||||
import io.element.android.tests.testutils.lambda.any
|
import io.element.android.tests.testutils.lambda.any
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -60,6 +64,40 @@ class AttachmentsPreviewPresenterTest {
|
||||||
|
|
||||||
private val mockMediaUrl: Uri = mockk("localMediaUri")
|
private val mockMediaUrl: Uri = mockk("localMediaUri")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
createAttachmentsPreviewPresenter().test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(initialState.allowCaption).isTrue()
|
||||||
|
assertThat(initialState.showCaptionCompatibilityWarning).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state no caption warning`() = runTest {
|
||||||
|
createAttachmentsPreviewPresenter(
|
||||||
|
showCaptionCompatibilityWarning = false,
|
||||||
|
).test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.showCaptionCompatibilityWarning).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state - caption not allowed`() = runTest {
|
||||||
|
createAttachmentsPreviewPresenter(
|
||||||
|
allowCaption = false,
|
||||||
|
).test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(initialState.allowCaption).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - send media success scenario`() = runTest {
|
fun `present - send media success scenario`() = runTest {
|
||||||
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
||||||
|
|
@ -84,16 +122,142 @@ class AttachmentsPreviewPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0f))
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0f))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0.5f))
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0.5f))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f))
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f))
|
||||||
advanceUntilIdle()
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendFileResult.assertions().isCalledOnce()
|
sendFileResult.assertions().isCalledOnce()
|
||||||
onDoneListener.assertions().isCalledOnce()
|
onDoneListener.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send media after pre-processing success scenario`() = runTest {
|
||||||
|
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
||||||
|
Result.success(FakeMediaUploadHandler())
|
||||||
|
}
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
sendFileResult = sendFileResult,
|
||||||
|
)
|
||||||
|
val onDoneListener = lambdaRecorder<Unit> { }
|
||||||
|
val processLatch = CompletableDeferred<Unit>()
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(
|
||||||
|
room = room,
|
||||||
|
mediaPreProcessor = FakeMediaPreProcessor(
|
||||||
|
processLatch = processLatch,
|
||||||
|
),
|
||||||
|
onDoneListener = { onDoneListener() },
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
// Pre-processing finishes
|
||||||
|
processLatch.complete(Unit)
|
||||||
|
advanceUntilIdle()
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.InstantSending)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
|
sendFileResult.assertions().isCalledOnce()
|
||||||
|
onDoneListener.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send media before pre-processing success scenario`() = runTest {
|
||||||
|
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
||||||
|
Result.success(FakeMediaUploadHandler())
|
||||||
|
}
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
sendFileResult = sendFileResult,
|
||||||
|
)
|
||||||
|
val onDoneListener = lambdaRecorder<Unit> { }
|
||||||
|
val processLatch = CompletableDeferred<Unit>()
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(
|
||||||
|
room = room,
|
||||||
|
mediaPreProcessor = FakeMediaPreProcessor(
|
||||||
|
processLatch = processLatch,
|
||||||
|
),
|
||||||
|
onDoneListener = { onDoneListener() },
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
// Pre-processing finishes
|
||||||
|
processLatch.complete(Unit)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
|
sendFileResult.assertions().isCalledOnce()
|
||||||
|
onDoneListener.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send media with pre-processing failure after user sends media`() = runTest {
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val onDoneListener = lambdaRecorder<Unit> { }
|
||||||
|
val processLatch = CompletableDeferred<Unit>()
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(
|
||||||
|
room = room,
|
||||||
|
mediaPreProcessor = FakeMediaPreProcessor().apply {
|
||||||
|
givenResult(Result.failure(Exception()))
|
||||||
|
},
|
||||||
|
onDoneListener = { onDoneListener() },
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
// Pre-processing finishes
|
||||||
|
processLatch.complete(Unit)
|
||||||
|
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Failure::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send media with pre-processing failure before user sends media`() = runTest {
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val onDoneListener = lambdaRecorder<Unit> { }
|
||||||
|
val processLatch = CompletableDeferred<Unit>()
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(
|
||||||
|
room = room,
|
||||||
|
mediaPreProcessor = FakeMediaPreProcessor().apply {
|
||||||
|
givenResult(Result.failure(Exception()))
|
||||||
|
},
|
||||||
|
onDoneListener = { onDoneListener() },
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
// Pre-processing finishes
|
||||||
|
processLatch.complete(Unit)
|
||||||
|
advanceUntilIdle()
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.InstantSending)
|
||||||
|
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Failure::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - cancel scenario`() = runTest {
|
fun `present - cancel scenario`() = runTest {
|
||||||
val onDoneListener = lambdaRecorder<Unit> { }
|
val onDoneListener = lambdaRecorder<Unit> { }
|
||||||
|
|
@ -108,6 +272,8 @@ class AttachmentsPreviewPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.Cancel)
|
initialState.eventSink(AttachmentsPreviewEvents.Cancel)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
deleteCallback.assertions().isCalledOnce()
|
deleteCallback.assertions().isCalledOnce()
|
||||||
onDoneListener.assertions().isCalledOnce()
|
onDoneListener.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
@ -138,8 +304,10 @@ class AttachmentsPreviewPresenterTest {
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.textEditorState.setMarkdown(A_CAPTION)
|
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
advanceUntilIdle()
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendImageResult.assertions().isCalledOnce().with(
|
sendImageResult.assertions().isCalledOnce().with(
|
||||||
any(),
|
any(),
|
||||||
any(),
|
any(),
|
||||||
|
|
@ -177,8 +345,10 @@ class AttachmentsPreviewPresenterTest {
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.textEditorState.setMarkdown(A_CAPTION)
|
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
advanceUntilIdle()
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendVideoResult.assertions().isCalledOnce().with(
|
sendVideoResult.assertions().isCalledOnce().with(
|
||||||
any(),
|
any(),
|
||||||
any(),
|
any(),
|
||||||
|
|
@ -214,8 +384,10 @@ class AttachmentsPreviewPresenterTest {
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.textEditorState.setMarkdown(A_CAPTION)
|
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
advanceUntilIdle()
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendAudioResult.assertions().isCalledOnce().with(
|
sendAudioResult.assertions().isCalledOnce().with(
|
||||||
any(),
|
any(),
|
||||||
any(),
|
any(),
|
||||||
|
|
@ -243,8 +415,9 @@ class AttachmentsPreviewPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
val loadingState = awaitItem()
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(loadingState.sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
val failureState = awaitItem()
|
val failureState = awaitItem()
|
||||||
assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure))
|
assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure))
|
||||||
sendFileResult.assertions().isCalledOnce()
|
sendFileResult.assertions().isCalledOnce()
|
||||||
|
|
@ -263,6 +436,8 @@ class AttachmentsPreviewPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
initialState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
|
@ -277,7 +452,10 @@ class AttachmentsPreviewPresenterTest {
|
||||||
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
|
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
|
||||||
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||||
temporaryUriDeleter: TemporaryUriDeleter = FakeTemporaryUriDeleter(),
|
temporaryUriDeleter: TemporaryUriDeleter = FakeTemporaryUriDeleter(),
|
||||||
onDoneListener: OnDoneListener = OnDoneListener {},
|
onDoneListener: OnDoneListener = OnDoneListener { lambdaError() },
|
||||||
|
mediaUploadOnSendQueueEnabled: Boolean = true,
|
||||||
|
allowCaption: Boolean = true,
|
||||||
|
showCaptionCompatibilityWarning: Boolean = true,
|
||||||
): AttachmentsPreviewPresenter {
|
): AttachmentsPreviewPresenter {
|
||||||
return AttachmentsPreviewPresenter(
|
return AttachmentsPreviewPresenter(
|
||||||
attachment = aMediaAttachment(localMedia),
|
attachment = aMediaAttachment(localMedia),
|
||||||
|
|
@ -285,6 +463,13 @@ class AttachmentsPreviewPresenterTest {
|
||||||
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
|
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
|
||||||
permalinkBuilder = permalinkBuilder,
|
permalinkBuilder = permalinkBuilder,
|
||||||
temporaryUriDeleter = temporaryUriDeleter,
|
temporaryUriDeleter = temporaryUriDeleter,
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(
|
||||||
|
FeatureFlags.MediaUploadOnSendQueue.key to mediaUploadOnSendQueueEnabled,
|
||||||
|
FeatureFlags.MediaCaptionCreation.key to allowCaption,
|
||||||
|
FeatureFlags.MediaCaptionWarning.key to showCaptionCompatibilityWarning,
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
|
import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1586,7 +1586,12 @@ fun anEditMode(
|
||||||
fun anEditCaptionMode(
|
fun anEditCaptionMode(
|
||||||
eventOrTransactionId: EventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
eventOrTransactionId: EventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
||||||
caption: String = A_CAPTION,
|
caption: String = A_CAPTION,
|
||||||
) = MessageComposerMode.EditCaption(eventOrTransactionId, caption)
|
showCaptionCompatibilityWarning: Boolean = false,
|
||||||
|
) = MessageComposerMode.EditCaption(
|
||||||
|
eventOrTransactionId = eventOrTransactionId,
|
||||||
|
content = caption,
|
||||||
|
showCaptionCompatibilityWarning = showCaptionCompatibilityWarning,
|
||||||
|
)
|
||||||
|
|
||||||
fun aReplyMode() = MessageComposerMode.Reply(
|
fun aReplyMode() = MessageComposerMode.Reply(
|
||||||
replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
|
replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ package io.element.android.features.messages.impl.pinned.list
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||||
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||||
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.fixtures.aTimelineItemsFactoryCreator
|
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
|
||||||
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
|
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
|
||||||
|
|
@ -312,7 +312,7 @@ class PinnedMessagesListPresenterTest {
|
||||||
timelineProvider = timelineProvider,
|
timelineProvider = timelineProvider,
|
||||||
timelineProtectionPresenter = { aTimelineProtectionState() },
|
timelineProtectionPresenter = { aTimelineProtectionState() },
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
actionListPresenterFactory = FakeActionListPresenter.Factory(),
|
actionListPresenter = { anActionListState() },
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
appCoroutineScope = this,
|
appCoroutineScope = this,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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.messages.impl.pinned.list
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class PinnedMessagesListTimelineActionPostProcessorTest {
|
||||||
|
@Test
|
||||||
|
fun `ensure that ViewInTimeline is added`() {
|
||||||
|
val sut = PinnedMessagesListTimelineActionPostProcessor()
|
||||||
|
val result = sut.process(
|
||||||
|
listOf()
|
||||||
|
)
|
||||||
|
assertThat(result).isEqualTo(
|
||||||
|
listOf(TimelineItemAction.ViewInTimeline)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ensure that some actions are kept and some other are filtered out`() {
|
||||||
|
val sut = PinnedMessagesListTimelineActionPostProcessor()
|
||||||
|
val result = sut.process(
|
||||||
|
listOf(
|
||||||
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.CopyText,
|
||||||
|
TimelineItemAction.CopyCaption,
|
||||||
|
TimelineItemAction.CopyLink,
|
||||||
|
TimelineItemAction.Redact,
|
||||||
|
TimelineItemAction.Reply,
|
||||||
|
TimelineItemAction.ReplyInThread,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.EditCaption,
|
||||||
|
TimelineItemAction.AddCaption,
|
||||||
|
TimelineItemAction.RemoveCaption,
|
||||||
|
TimelineItemAction.ViewSource,
|
||||||
|
TimelineItemAction.ReportContent,
|
||||||
|
TimelineItemAction.EndPoll,
|
||||||
|
TimelineItemAction.Pin,
|
||||||
|
TimelineItemAction.Unpin,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(result).isEqualTo(
|
||||||
|
listOf(
|
||||||
|
TimelineItemAction.ViewInTimeline,
|
||||||
|
TimelineItemAction.Unpin,
|
||||||
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.ViewSource,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -64,7 +64,7 @@ import io.element.android.libraries.matrix.test.media.aMediaSource
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import io.element.android.libraries.matrix.test.timeline.aStickerContent
|
import io.element.android.libraries.matrix.test.timeline.aStickerContent
|
||||||
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
|
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
|
||||||
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
|
import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -239,7 +239,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
formattedCaption = null,
|
formattedCaption = null,
|
||||||
isEdited = false,
|
isEdited = false,
|
||||||
duration = Duration.ZERO,
|
duration = Duration.ZERO,
|
||||||
videoSource = MediaSource(url = "url", json = null),
|
mediaSource = MediaSource(url = "url", json = null),
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
aspectRatio = null,
|
aspectRatio = null,
|
||||||
blurHash = null,
|
blurHash = null,
|
||||||
|
|
@ -291,7 +291,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
formattedCaption = SpannedString("formatted"),
|
formattedCaption = SpannedString("formatted"),
|
||||||
isEdited = true,
|
isEdited = true,
|
||||||
duration = 1.minutes,
|
duration = 1.minutes,
|
||||||
videoSource = MediaSource(url = "url", json = null),
|
mediaSource = MediaSource(url = "url", json = null),
|
||||||
thumbnailSource = MediaSource("url_thumbnail"),
|
thumbnailSource = MediaSource("url_thumbnail"),
|
||||||
aspectRatio = 3f,
|
aspectRatio = 3f,
|
||||||
blurHash = A_BLUR_HASH,
|
blurHash = A_BLUR_HASH,
|
||||||
|
|
@ -380,7 +380,9 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
duration = Duration.ZERO,
|
duration = Duration.ZERO,
|
||||||
mediaSource = MediaSource(url = "url", json = null),
|
mediaSource = MediaSource(url = "url", json = null),
|
||||||
mimeType = MimeTypes.OctetStream,
|
mimeType = MimeTypes.OctetStream,
|
||||||
waveform = emptyList<Float>().toImmutableList()
|
waveform = emptyList<Float>().toImmutableList(),
|
||||||
|
fileExtension = "",
|
||||||
|
formattedFileSize = "0 Bytes",
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expected)
|
assertThat(result).isEqualTo(expected)
|
||||||
}
|
}
|
||||||
|
|
@ -419,7 +421,9 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
duration = 1.minutes,
|
duration = 1.minutes,
|
||||||
mediaSource = MediaSource(url = "url", json = null),
|
mediaSource = MediaSource(url = "url", json = null),
|
||||||
mimeType = MimeTypes.Ogg,
|
mimeType = MimeTypes.Ogg,
|
||||||
waveform = persistentListOf(1f, 2f)
|
waveform = persistentListOf(1f, 2f),
|
||||||
|
fileExtension = "ogg",
|
||||||
|
formattedFileSize = "123 Bytes",
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expected)
|
assertThat(result).isEqualTo(expected)
|
||||||
}
|
}
|
||||||
|
|
@ -571,7 +575,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
caption = null,
|
caption = null,
|
||||||
formattedCaption = null,
|
formattedCaption = null,
|
||||||
isEdited = false,
|
isEdited = false,
|
||||||
fileSource = MediaSource(url = "url", json = null),
|
mediaSource = MediaSource(url = "url", json = null),
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
formattedFileSize = "0 Bytes",
|
formattedFileSize = "0 Bytes",
|
||||||
fileExtension = "",
|
fileExtension = "",
|
||||||
|
|
@ -612,7 +616,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
caption = null,
|
caption = null,
|
||||||
formattedCaption = null,
|
formattedCaption = null,
|
||||||
isEdited = true,
|
isEdited = true,
|
||||||
fileSource = MediaSource(url = "url", json = null),
|
mediaSource = MediaSource(url = "url", json = null),
|
||||||
thumbnailSource = MediaSource("url_thumbnail"),
|
thumbnailSource = MediaSource("url_thumbnail"),
|
||||||
formattedFileSize = "123 Bytes",
|
formattedFileSize = "123 Bytes",
|
||||||
fileExtension = "pdf",
|
fileExtension = "pdf",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
<string name="screen_advanced_settings_element_call_base_url">"Benutzerdefinierte Element-Aufruf-Basis-URL"</string>
|
<string name="screen_advanced_settings_element_call_base_url">"Benutzerdefinierte Element-Aufruf-Basis-URL"</string>
|
||||||
<string name="screen_advanced_settings_element_call_base_url_description">"Lege eine eigene Basis-URL für Element Call fest."</string>
|
<string name="screen_advanced_settings_element_call_base_url_description">"Lege eine eigene Basis-URL für Element Call fest."</string>
|
||||||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Ungültige URL, bitte stelle sicher, dass du das Protokoll (http/https) und die richtige Adresse angibst."</string>
|
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Ungültige URL, bitte stelle sicher, dass du das Protokoll (http/https) und die richtige Adresse angibst."</string>
|
||||||
|
<string name="screen_advanced_settings_media_compression_description">"Laden Sie Fotos und Videos schneller hoch und reduzieren Sie die Datennutzung"</string>
|
||||||
|
<string name="screen_advanced_settings_media_compression_title">"Optimieren Sie die Medienqualität"</string>
|
||||||
<string name="screen_advanced_settings_push_provider_android">"Anbieter für Push-Benachrichtigungen"</string>
|
<string name="screen_advanced_settings_push_provider_android">"Anbieter für Push-Benachrichtigungen"</string>
|
||||||
<string name="screen_advanced_settings_rich_text_editor_description">"Deaktiviere den Rich-Text-Editor, um Markdown manuell einzugeben."</string>
|
<string name="screen_advanced_settings_rich_text_editor_description">"Deaktiviere den Rich-Text-Editor, um Markdown manuell einzugeben."</string>
|
||||||
<string name="screen_advanced_settings_send_read_receipts">"Lesebestätigungen"</string>
|
<string name="screen_advanced_settings_send_read_receipts">"Lesebestätigungen"</string>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
@ -33,20 +34,16 @@ import io.element.android.features.roomdetails.impl.members.details.RoomMemberDe
|
||||||
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode
|
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode
|
||||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode
|
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode
|
||||||
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
|
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
|
||||||
import io.element.android.features.userprofile.shared.avatar.AvatarPreviewNode
|
|
||||||
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.overlay.operation.show
|
import io.element.android.libraries.architecture.overlay.operation.show
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
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.media.MediaSource
|
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||||
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
|
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
@ -61,6 +58,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val messagesEntryPoint: MessagesEntryPoint,
|
private val messagesEntryPoint: MessagesEntryPoint,
|
||||||
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
||||||
|
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
|
||||||
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
|
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
|
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
|
||||||
|
|
@ -211,22 +209,18 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
createNode<RoomMemberDetailsNode>(buildContext, plugins)
|
createNode<RoomMemberDetailsNode>(buildContext, plugins)
|
||||||
}
|
}
|
||||||
is NavTarget.AvatarPreview -> {
|
is NavTarget.AvatarPreview -> {
|
||||||
// We need to fake the MimeType here for the viewer to work.
|
val callback = object : MediaViewerEntryPoint.Callback {
|
||||||
val mimeType = MimeTypes.Images
|
override fun onDone() {
|
||||||
val input = MediaViewerNode.Inputs(
|
backstack.pop()
|
||||||
mediaInfo = MediaInfo(
|
}
|
||||||
filename = navTarget.name,
|
}
|
||||||
caption = null,
|
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
|
||||||
mimeType = mimeType,
|
.avatar(
|
||||||
formattedFileSize = "",
|
navTarget.name,
|
||||||
fileExtension = ""
|
navTarget.avatarUrl,
|
||||||
),
|
)
|
||||||
mediaSource = MediaSource(url = navTarget.avatarUrl),
|
.callback(callback)
|
||||||
thumbnailSource = null,
|
.build()
|
||||||
canDownload = false,
|
|
||||||
canShare = false,
|
|
||||||
)
|
|
||||||
createNode<AvatarPreviewNode>(buildContext, listOf(input))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is NavTarget.PollHistory -> {
|
is NavTarget.PollHistory -> {
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@
|
||||||
<string name="banner_set_up_recovery_content">"Erstelle einen neuen Wiederherstellungsschlüssel, mit dem du deinen verschlüsselten Nachrichtenverlauf wiederherstellen kannst, wenn du dich an einem neuen Gerät anmeldest."</string>
|
<string name="banner_set_up_recovery_content">"Erstelle einen neuen Wiederherstellungsschlüssel, mit dem du deinen verschlüsselten Nachrichtenverlauf wiederherstellen kannst, wenn du dich an einem neuen Gerät anmeldest."</string>
|
||||||
<string name="banner_set_up_recovery_submit">"Wiederherstellung einrichten"</string>
|
<string name="banner_set_up_recovery_submit">"Wiederherstellung einrichten"</string>
|
||||||
<string name="banner_set_up_recovery_title">"Wiederherstellung einrichten"</string>
|
<string name="banner_set_up_recovery_title">"Wiederherstellung einrichten"</string>
|
||||||
<string name="confirm_recovery_key_banner_message">"Dein Chat-Backup ist derzeit nicht synchronisiert. Du musst deinen Wiederherstellungsschlüssel bestätigen, um Zugriff auf dein Chat-Backup zu erhalten."</string>
|
<string name="confirm_recovery_key_banner_message">"Bestätigen Sie die Validität Ihres Wiederherstellungsschlüssels, um weiterhin auf Ihren Schlüsselspeicher und den Nachrichtenverlauf zugreifen zu können."</string>
|
||||||
<string name="confirm_recovery_key_banner_title">"Wiederherstellungsschlüssel bestätigen."</string>
|
<string name="confirm_recovery_key_banner_primary_button_title">"Geben Sie Ihren Wiederherstellungsschlüssel ein"</string>
|
||||||
|
<string name="confirm_recovery_key_banner_secondary_button_title">"Haben Sie Ihren Wiederherstellungsschlüssel vergessen?"</string>
|
||||||
|
<string name="confirm_recovery_key_banner_title">"Ihr Schlüsselspeicher ist nicht synchronisiert"</string>
|
||||||
<string name="full_screen_intent_banner_message">"Damit du keinen wichtigen Anruf verpasst, ändere bitte deine Einstellungen so, dass du bei gesperrtem Telefon Benachrichtigungen im Vollbildmodus erhältst."</string>
|
<string name="full_screen_intent_banner_message">"Damit du keinen wichtigen Anruf verpasst, ändere bitte deine Einstellungen so, dass du bei gesperrtem Telefon Benachrichtigungen im Vollbildmodus erhältst."</string>
|
||||||
<string name="full_screen_intent_banner_title">"Verbessere dein Anruferlebnis"</string>
|
<string name="full_screen_intent_banner_title">"Verbessere dein Anruferlebnis"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?"</string>
|
<string name="screen_invites_decline_chat_message">"Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?"</string>
|
||||||
|
|
@ -17,6 +19,7 @@
|
||||||
<string name="screen_invites_decline_direct_chat_title">"Einladung ablehnen"</string>
|
<string name="screen_invites_decline_direct_chat_title">"Einladung ablehnen"</string>
|
||||||
<string name="screen_invites_empty_list">"Keine Einladungen"</string>
|
<string name="screen_invites_empty_list">"Keine Einladungen"</string>
|
||||||
<string name="screen_invites_invited_you">"%1$s (%2$s) hat dich eingeladen"</string>
|
<string name="screen_invites_invited_you">"%1$s (%2$s) hat dich eingeladen"</string>
|
||||||
|
<string name="screen_join_room_knock_sent_title">"Beitrittsanfrage geschickt"</string>
|
||||||
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
|
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
|
||||||
<string name="screen_migration_title">"Dein Konto wird eingerichtet."</string>
|
<string name="screen_migration_title">"Dein Konto wird eingerichtet."</string>
|
||||||
<string name="screen_roomlist_a11y_create_message">"Eine Unterthaltung oder Raum erstellen"</string>
|
<string name="screen_roomlist_a11y_create_message">"Eine Unterthaltung oder Raum erstellen"</string>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import io.element.android.features.securebackup.impl.root.SecureBackupRootNode
|
||||||
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode
|
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
|
import io.element.android.libraries.architecture.appyx.canPop
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
@ -111,10 +112,10 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||||
NavTarget.EnterRecoveryKey -> {
|
NavTarget.EnterRecoveryKey -> {
|
||||||
val callback = object : SecureBackupEnterRecoveryKeyNode.Callback {
|
val callback = object : SecureBackupEnterRecoveryKeyNode.Callback {
|
||||||
override fun onEnterRecoveryKeySuccess() {
|
override fun onEnterRecoveryKeySuccess() {
|
||||||
if (callbacks.isNotEmpty()) {
|
if (backstack.canPop()) {
|
||||||
callbacks.forEach { it.onDone() }
|
|
||||||
} else {
|
|
||||||
backstack.pop()
|
backstack.pop()
|
||||||
|
} else {
|
||||||
|
callbacks.forEach { it.onDone() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,15 @@
|
||||||
<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_chat_backup_key_backup_action_disable">"Backup deaktivieren"</string>
|
<string name="screen_chat_backup_key_backup_action_disable">"Backup deaktivieren"</string>
|
||||||
<string name="screen_chat_backup_key_backup_action_enable">"Backup aktivieren"</string>
|
<string name="screen_chat_backup_key_backup_action_enable">"Backup aktivieren"</string>
|
||||||
<string name="screen_chat_backup_key_backup_description">"Das Backup stellt sicher, dass du deinen Nachrichtenverlauf nicht verlierst. %1$s."</string>
|
<string name="screen_chat_backup_key_backup_description">"Speichern Sie Ihre verschlüsselte Identität und Ihre codierten Nachrichtenschlüssel auf dem Server. Auf diese Weise können Sie Ihren Nachrichtenverlauf auf allen neuen Geräten einsehen. %1$s."</string>
|
||||||
<string name="screen_chat_backup_key_backup_title">"Backup"</string>
|
<string name="screen_chat_backup_key_backup_title">"Schlüsselspeicher"</string>
|
||||||
|
<string name="screen_chat_backup_key_storage_disabled_error">"Der Schlüsselspeicher muss aktiviert sein, um Datenwiederherstellung zu ermöglichen."</string>
|
||||||
|
<string name="screen_chat_backup_key_storage_toggle_description">"Schlüssel von diesem Gerät hochladen"</string>
|
||||||
|
<string name="screen_chat_backup_key_storage_toggle_title">"Schlüsselspeicherung zulassen"</string>
|
||||||
<string name="screen_chat_backup_recovery_action_change">"Wiederherstellungsschlüssel ändern"</string>
|
<string name="screen_chat_backup_recovery_action_change">"Wiederherstellungsschlüssel ändern"</string>
|
||||||
|
<string name="screen_chat_backup_recovery_action_change_description">"Stellen Sie Ihre verschlüsselte Identität und Ihren Nachrichtenverlauf mit einem Wiederherstellungsschlüssel wieder her, falls Sie den Zugang zu allen Ihren Geräten verloren haben."</string>
|
||||||
<string name="screen_chat_backup_recovery_action_confirm">"Wiederherstellungsschlüssel eingeben"</string>
|
<string name="screen_chat_backup_recovery_action_confirm">"Wiederherstellungsschlüssel eingeben"</string>
|
||||||
<string name="screen_chat_backup_recovery_action_confirm_description">"Dein Chat-Backup ist derzeit nicht synchronisiert."</string>
|
<string name="screen_chat_backup_recovery_action_confirm_description">"Dein Schlüssel ist derzeit nicht synchronisiert."</string>
|
||||||
<string name="screen_chat_backup_recovery_action_setup">"Wiederherstellung einrichten"</string>
|
<string name="screen_chat_backup_recovery_action_setup">"Wiederherstellung einrichten"</string>
|
||||||
<string name="screen_chat_backup_recovery_action_setup_description">"Erhalte Zugriff auf deine verschlüsselten Nachrichten, wenn du alle deine Geräte verlierst oder von %1$s überall abgemeldet bist."</string>
|
<string name="screen_chat_backup_recovery_action_setup_description">"Erhalte Zugriff auf deine verschlüsselten Nachrichten, wenn du alle deine Geräte verlierst oder von %1$s überall abgemeldet bist."</string>
|
||||||
<string name="screen_create_new_recovery_key_list_item_1">
|
<string name="screen_create_new_recovery_key_list_item_1">
|
||||||
|
|
@ -33,6 +37,8 @@
|
||||||
<string name="screen_encryption_reset_bullet_1">"Deine Kontodaten, Kontakte, Einstellungen und die Liste der Chats bleiben erhalten"</string>
|
<string name="screen_encryption_reset_bullet_1">"Deine Kontodaten, Kontakte, Einstellungen und die Liste der Chats bleiben erhalten"</string>
|
||||||
<string name="screen_encryption_reset_bullet_2">"Du verlierst alle deine bisherigen Nachrichten sofern sie nicht auf einem anderen Gerät vorliegen"</string>
|
<string name="screen_encryption_reset_bullet_2">"Du verlierst alle deine bisherigen Nachrichten sofern sie nicht auf einem anderen Gerät vorliegen"</string>
|
||||||
<string name="screen_encryption_reset_bullet_3">"Du musst alle deine bestehenden Geräte und Kontakte erneut verifizieren."</string>
|
<string name="screen_encryption_reset_bullet_3">"Du musst alle deine bestehenden Geräte und Kontakte erneut verifizieren."</string>
|
||||||
|
<string name="screen_encryption_reset_footer">"Setzen Sie Ihre Identität nur dann zurück, wenn Sie keinen Zugriff auf ein anderes Ihrer angemeldeten Geräte und auch Ihren Wiederherstellungsschlüssel verloren haben."</string>
|
||||||
|
<string name="screen_encryption_reset_title">"Sie können es nicht bestätigen? Dann müssen Sie Ihre Identität zurücksetzen."</string>
|
||||||
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Ausschalten"</string>
|
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Ausschalten"</string>
|
||||||
<string name="screen_key_backup_disable_confirmation_description">"Du verlierst deine verschlüsselten Nachrichten, wenn du auf allen Geräten abgemeldet bist."</string>
|
<string name="screen_key_backup_disable_confirmation_description">"Du verlierst deine verschlüsselten Nachrichten, wenn du auf allen Geräten abgemeldet bist."</string>
|
||||||
<string name="screen_key_backup_disable_confirmation_title">"Bist du sicher, dass du das Backup deaktivieren willst?"</string>
|
<string name="screen_key_backup_disable_confirmation_title">"Bist du sicher, dass du das Backup deaktivieren willst?"</string>
|
||||||
|
|
@ -42,7 +48,7 @@
|
||||||
<string name="screen_key_backup_disable_title">"Bist du sicher, dass du das Backup deaktivieren willst?"</string>
|
<string name="screen_key_backup_disable_title">"Bist du sicher, dass du das Backup deaktivieren willst?"</string>
|
||||||
<string name="screen_recovery_key_change_description">"Hier kannst Du einen neuen Wiederherstellungsschlüssel erstellen. Nachdem Du einen neuen Wiederherstellungsschlüssel erstellt hast, funktioniert dein alter Schlüssel nicht mehr."</string>
|
<string name="screen_recovery_key_change_description">"Hier kannst Du einen neuen Wiederherstellungsschlüssel erstellen. Nachdem Du einen neuen Wiederherstellungsschlüssel erstellt hast, funktioniert dein alter Schlüssel nicht mehr."</string>
|
||||||
<string name="screen_recovery_key_change_generate_key">"Wiederherstellungsschlüssel erstellen"</string>
|
<string name="screen_recovery_key_change_generate_key">"Wiederherstellungsschlüssel erstellen"</string>
|
||||||
<string name="screen_recovery_key_change_generate_key_description">"Stelle sicher, dass du deinen Wiederherstellungsschlüssel an einem sicheren Ort aufbewahren kannst"</string>
|
<string name="screen_recovery_key_change_generate_key_description">"Geben Sie dies an niemanden weiter!"</string>
|
||||||
<string name="screen_recovery_key_change_success">"Wiederherstellungsschlüssel geändert"</string>
|
<string name="screen_recovery_key_change_success">"Wiederherstellungsschlüssel geändert"</string>
|
||||||
<string name="screen_recovery_key_change_title">"Wiederherstellungsschlüssel ändern?"</string>
|
<string name="screen_recovery_key_change_title">"Wiederherstellungsschlüssel ändern?"</string>
|
||||||
<string name="screen_recovery_key_confirm_create_new_recovery_key">
|
<string name="screen_recovery_key_confirm_create_new_recovery_key">
|
||||||
|
|
@ -51,23 +57,24 @@
|
||||||
" erstellen"
|
" erstellen"
|
||||||
</string>
|
</string>
|
||||||
<string name="screen_recovery_key_confirm_description">"Sorge dafür, dass niemand diesen Bildschirm sehen kann!"</string>
|
<string name="screen_recovery_key_confirm_description">"Sorge dafür, dass niemand diesen Bildschirm sehen kann!"</string>
|
||||||
<string name="screen_recovery_key_confirm_error_content">"Bitte versuche es noch einmal, um den Zugriff auf dein Chat-Backup zu bestätigen."</string>
|
<string name="screen_recovery_key_confirm_error_content">"Bitte versuchen Sie erneut, den Zugriff auf Ihren Schlüsselspeicher zu bestätigen."</string>
|
||||||
<string name="screen_recovery_key_confirm_error_title">"Falscher Wiederherstellungsschlüssel"</string>
|
<string name="screen_recovery_key_confirm_error_title">"Falscher Wiederherstellungsschlüssel"</string>
|
||||||
<string name="screen_recovery_key_confirm_key_description">"Dies funktioniert auch mit einem Sicherheitsschlüssel oder Sicherheitsphrase."</string>
|
<string name="screen_recovery_key_confirm_key_description">"Dies funktioniert auch mit einem Sicherheitsschlüssel oder Sicherheitsphrase."</string>
|
||||||
<string name="screen_recovery_key_confirm_key_placeholder">"Eingeben…"</string>
|
<string name="screen_recovery_key_confirm_key_placeholder">"Eingeben…"</string>
|
||||||
<string name="screen_recovery_key_confirm_lost_recovery_key">"Hast du deinen Wiederherstellungschlüssel vergessen?"</string>
|
<string name="screen_recovery_key_confirm_lost_recovery_key">"Hast du deinen Wiederherstellungschlüssel vergessen?"</string>
|
||||||
<string name="screen_recovery_key_confirm_success">"Wiederherstellungsschlüssel bestätigt"</string>
|
<string name="screen_recovery_key_confirm_success">"Wiederherstellungsschlüssel bestätigt"</string>
|
||||||
|
<string name="screen_recovery_key_confirm_title">"Geben Sie Ihren Wiederherstellungsschlüssel ein"</string>
|
||||||
<string name="screen_recovery_key_copied_to_clipboard">"Wiederherstellungsschlüssel kopiert"</string>
|
<string name="screen_recovery_key_copied_to_clipboard">"Wiederherstellungsschlüssel kopiert"</string>
|
||||||
<string name="screen_recovery_key_generating_key">"Generieren…"</string>
|
<string name="screen_recovery_key_generating_key">"Generieren…"</string>
|
||||||
<string name="screen_recovery_key_save_action">"Wiederherstellungsschlüssel speichern"</string>
|
<string name="screen_recovery_key_save_action">"Wiederherstellungsschlüssel speichern"</string>
|
||||||
<string name="screen_recovery_key_save_description">"Notiere dir deinen Wiederherstellungsschlüssel an einem sicheren Ort oder speichere ihn in einem Passwort-Manager."</string>
|
<string name="screen_recovery_key_save_description">"Schreiben Sie Ihren Wiederherstellungsschlüssel in eine verschlüsselte Datei, oder in einem Passwort-Manager oder in einem Safe. "</string>
|
||||||
<string name="screen_recovery_key_save_key_description">"Tippe, um den Wiederherstellungsschlüssel zu kopieren"</string>
|
<string name="screen_recovery_key_save_key_description">"Tippe, um den Wiederherstellungsschlüssel zu kopieren"</string>
|
||||||
<string name="screen_recovery_key_save_title">"Speichere deinen Wiederherstellungsschlüssel"</string>
|
<string name="screen_recovery_key_save_title">"Speichere deinen Wiederherstellungsschlüssel"</string>
|
||||||
<string name="screen_recovery_key_setup_confirmation_description">"Nach diesem Schritt kannst du nicht mehr auf deinen neuen Wiederherstellungsschlüssel zugreifen."</string>
|
<string name="screen_recovery_key_setup_confirmation_description">"Nach diesem Schritt kannst du nicht mehr auf deinen neuen Wiederherstellungsschlüssel zugreifen."</string>
|
||||||
<string name="screen_recovery_key_setup_confirmation_title">"Hast du deinen Wiederherstellungsschlüssel gespeichert?"</string>
|
<string name="screen_recovery_key_setup_confirmation_title">"Hast du deinen Wiederherstellungsschlüssel gespeichert?"</string>
|
||||||
<string name="screen_recovery_key_setup_description">"Dein Chat-Backup ist durch einen Wiederherstellungsschlüssel geschützt. Wenn du nach der Einrichtung einen neuen Wiederherstellungsschlüssel brauchst, kannst du ihn über die Option \"Wiederherstellungsschlüssel ändern\" neu erstellen."</string>
|
<string name="screen_recovery_key_setup_description">"Dein Chat-Backup ist durch einen Wiederherstellungsschlüssel geschützt. Wenn du nach der Einrichtung einen neuen Wiederherstellungsschlüssel brauchst, kannst du ihn über die Option \"Wiederherstellungsschlüssel ändern\" neu erstellen."</string>
|
||||||
<string name="screen_recovery_key_setup_generate_key">"Wiederherstellungsschlüssel erstellen"</string>
|
<string name="screen_recovery_key_setup_generate_key">"Wiederherstellungsschlüssel erstellen"</string>
|
||||||
<string name="screen_recovery_key_setup_generate_key_description">"Stelle sicher, dass du deinen Wiederherstellungsschlüssel an einem sicheren Ort aufbewahren kannst"</string>
|
<string name="screen_recovery_key_setup_generate_key_description">"Geben Sie dies an niemanden weiter!"</string>
|
||||||
<string name="screen_recovery_key_setup_success">"Einrichtung der Wiederherstellung erfolgreich"</string>
|
<string name="screen_recovery_key_setup_success">"Einrichtung der Wiederherstellung erfolgreich"</string>
|
||||||
<string name="screen_recovery_key_setup_title">"Wiederherstellung einrichten"</string>
|
<string name="screen_recovery_key_setup_title">"Wiederherstellung einrichten"</string>
|
||||||
<string name="screen_reset_encryption_confirmation_alert_action">"Ja, zurücksetzen"</string>
|
<string name="screen_reset_encryption_confirmation_alert_action">"Ja, zurücksetzen"</string>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
@ -24,18 +25,14 @@ import io.element.android.features.call.api.ElementCallEntryPoint
|
||||||
import io.element.android.features.userprofile.api.UserProfileEntryPoint
|
import io.element.android.features.userprofile.api.UserProfileEntryPoint
|
||||||
import io.element.android.features.userprofile.impl.root.UserProfileNode
|
import io.element.android.features.userprofile.impl.root.UserProfileNode
|
||||||
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
|
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
|
||||||
import io.element.android.features.userprofile.shared.avatar.AvatarPreviewNode
|
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
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.inputs
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
|
||||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||||
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||||
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
|
|
@ -44,6 +41,7 @@ class UserProfileFlowNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||||
private val sessionIdHolder: CurrentSessionIdHolder,
|
private val sessionIdHolder: CurrentSessionIdHolder,
|
||||||
|
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
|
||||||
) : BaseFlowNode<UserProfileFlowNode.NavTarget>(
|
) : BaseFlowNode<UserProfileFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Root,
|
initialElement = NavTarget.Root,
|
||||||
|
|
@ -80,22 +78,18 @@ class UserProfileFlowNode @AssistedInject constructor(
|
||||||
createNode<UserProfileNode>(buildContext, listOf(callback, params))
|
createNode<UserProfileNode>(buildContext, listOf(callback, params))
|
||||||
}
|
}
|
||||||
is NavTarget.AvatarPreview -> {
|
is NavTarget.AvatarPreview -> {
|
||||||
// We need to fake the MimeType here for the viewer to work.
|
val callback = object : MediaViewerEntryPoint.Callback {
|
||||||
val mimeType = MimeTypes.Images
|
override fun onDone() {
|
||||||
val input = MediaViewerNode.Inputs(
|
backstack.pop()
|
||||||
mediaInfo = MediaInfo(
|
}
|
||||||
|
}
|
||||||
|
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.avatar(
|
||||||
filename = navTarget.name,
|
filename = navTarget.name,
|
||||||
caption = null,
|
avatarUrl = navTarget.avatarUrl
|
||||||
mimeType = mimeType,
|
)
|
||||||
formattedFileSize = "",
|
.callback(callback)
|
||||||
fileExtension = "",
|
.build()
|
||||||
),
|
|
||||||
mediaSource = MediaSource(url = navTarget.avatarUrl),
|
|
||||||
thumbnailSource = null,
|
|
||||||
canDownload = false,
|
|
||||||
canShare = false,
|
|
||||||
)
|
|
||||||
createNode<AvatarPreviewNode>(buildContext, listOf(input))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.userprofile.shared.avatar
|
|
||||||
|
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
|
||||||
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
|
|
||||||
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter
|
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
|
||||||
class AvatarPreviewNode @AssistedInject constructor(
|
|
||||||
@Assisted buildContext: BuildContext,
|
|
||||||
@Assisted plugins: List<Plugin>,
|
|
||||||
presenterFactory: MediaViewerPresenter.Factory,
|
|
||||||
) : MediaViewerNode(buildContext, plugins, presenterFactory)
|
|
||||||
|
|
@ -13,5 +13,7 @@
|
||||||
<string name="screen_room_member_details_unblock_alert_action">"Blockierung aufheben"</string>
|
<string name="screen_room_member_details_unblock_alert_action">"Blockierung aufheben"</string>
|
||||||
<string name="screen_room_member_details_unblock_alert_description">"Der Nutzer kann dir wieder Nachrichten senden & alle Nachrichten des Nutzers werden wieder angezeigt."</string>
|
<string name="screen_room_member_details_unblock_alert_description">"Der Nutzer kann dir wieder Nachrichten senden & alle Nachrichten des Nutzers werden wieder angezeigt."</string>
|
||||||
<string name="screen_room_member_details_unblock_user">"Blockierung aufheben"</string>
|
<string name="screen_room_member_details_unblock_user">"Blockierung aufheben"</string>
|
||||||
|
<string name="screen_room_member_details_verify_button_subtitle">"Verwenden Sie die Web-App, um diesen Benutzer zu verifizieren."</string>
|
||||||
|
<string name="screen_room_member_details_verify_button_title">"Überprüfen Sie %1$s"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
|
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?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_identity_confirmation_cannot_confirm">"Sie können es nicht bestätigen?"</string>
|
||||||
<string name="screen_identity_confirmation_create_new_recovery_key">"Erstelle einen neuen Wiederherstellungsschlüssel"</string>
|
<string name="screen_identity_confirmation_create_new_recovery_key">"Erstelle einen neuen Wiederherstellungsschlüssel"</string>
|
||||||
<string name="screen_identity_confirmation_subtitle">"Verifiziere dieses Gerät, um sicheres Messaging einzurichten."</string>
|
<string name="screen_identity_confirmation_subtitle">"Verifiziere dieses Gerät, um sicheres Messaging einzurichten."</string>
|
||||||
<string name="screen_identity_confirmation_title">"Bestätige, dass du es bist"</string>
|
<string name="screen_identity_confirmation_title">"Bestätige, dass du es bist"</string>
|
||||||
|
|
@ -16,6 +17,7 @@
|
||||||
<string name="screen_session_verification_compare_numbers_title">"Vergleiche die Zahlen"</string>
|
<string name="screen_session_verification_compare_numbers_title">"Vergleiche die Zahlen"</string>
|
||||||
<string name="screen_session_verification_complete_subtitle">"Deine neue Session ist nun verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten und wird von anderen Benutzern als vertrauenswürdig eingestuft."</string>
|
<string name="screen_session_verification_complete_subtitle">"Deine neue Session ist nun verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten und wird von anderen Benutzern als vertrauenswürdig eingestuft."</string>
|
||||||
<string name="screen_session_verification_enter_recovery_key">"Wiederherstellungsschlüssel eingeben"</string>
|
<string name="screen_session_verification_enter_recovery_key">"Wiederherstellungsschlüssel eingeben"</string>
|
||||||
|
<string name="screen_session_verification_failed_subtitle">"Entweder ist bei der Anfrage ein Timeout aufgetreten, oder die Anfrage wurde abgelehnt, oder es gab eine Nichtübereinstimmung bei der Überprüfung."</string>
|
||||||
<string name="screen_session_verification_open_existing_session_subtitle">"Beweise deine Identität, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen."</string>
|
<string name="screen_session_verification_open_existing_session_subtitle">"Beweise deine Identität, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen."</string>
|
||||||
<string name="screen_session_verification_open_existing_session_title">"Öffne eine bestehende Session"</string>
|
<string name="screen_session_verification_open_existing_session_title">"Öffne eine bestehende Session"</string>
|
||||||
<string name="screen_session_verification_positive_button_canceled">"Verifizierung wiederholen"</string>
|
<string name="screen_session_verification_positive_button_canceled">"Verifizierung wiederholen"</string>
|
||||||
|
|
@ -23,8 +25,20 @@
|
||||||
<string name="screen_session_verification_positive_button_verifying_ongoing">"Warten auf eine Übereinstimmung"</string>
|
<string name="screen_session_verification_positive_button_verifying_ongoing">"Warten auf eine Übereinstimmung"</string>
|
||||||
<string name="screen_session_verification_ready_subtitle">"Vergleiche eine spezielle Reihe von Emojis."</string>
|
<string name="screen_session_verification_ready_subtitle">"Vergleiche eine spezielle Reihe von Emojis."</string>
|
||||||
<string name="screen_session_verification_request_accepted_subtitle">"Vergleiche die einzelnen Emojis und stelle sicher, dass sie in der gleichen Reihenfolge erscheinen."</string>
|
<string name="screen_session_verification_request_accepted_subtitle">"Vergleiche die einzelnen Emojis und stelle sicher, dass sie in der gleichen Reihenfolge erscheinen."</string>
|
||||||
|
<string name="screen_session_verification_request_details_timestamp">"Angemeldet"</string>
|
||||||
|
<string name="screen_session_verification_request_failure_subtitle">"Entweder ist bei der Anfrage ein Timeout aufgetreten, oder die Anfrage wurde abgelehnt, oder es gab eine Nichtübereinstimmung bei der Überprüfung."</string>
|
||||||
|
<string name="screen_session_verification_request_failure_title">"Überprüfung fehlgeschlagen"</string>
|
||||||
|
<string name="screen_session_verification_request_footer">"Fahren Sie nur fort, falls Sie für diese Überprüfung verantwortlich sind.."</string>
|
||||||
|
<string name="screen_session_verification_request_subtitle">"Verifizieren Sie das andere Gerät, um die Sicherheit Ihres Nachrichtenverlaufs zu gewährleisten."</string>
|
||||||
|
<string name="screen_session_verification_request_success_subtitle">"Jetzt können Sie gesichert Nachrichten auf Ihrem anderen Gerät lesen oder senden."</string>
|
||||||
|
<string name="screen_session_verification_request_success_title">"Gerät verifiziert"</string>
|
||||||
|
<string name="screen_session_verification_request_title">"Verifizierung angefordert"</string>
|
||||||
<string name="screen_session_verification_they_dont_match">"Sie stimmen nicht überein"</string>
|
<string name="screen_session_verification_they_dont_match">"Sie stimmen nicht überein"</string>
|
||||||
<string name="screen_session_verification_they_match">"Sie stimmen überein"</string>
|
<string name="screen_session_verification_they_match">"Sie stimmen überein"</string>
|
||||||
|
<string name="screen_session_verification_use_another_device_subtitle">"Stellen Sie sicher, dass die App auf dem anderen Gerät geöffnet ist, bevor Sie die Überprüfung auf diesem Gerät aus starten."</string>
|
||||||
|
<string name="screen_session_verification_use_another_device_title">"Öffnen Sie die App auf einem anderen verifizierten Gerät"</string>
|
||||||
|
<string name="screen_session_verification_waiting_another_device_subtitle">"Sie sollten ein Popup-Fenster auf dem anderen Gerät sehen. Starten Sie die Überprüfung von dort aus."</string>
|
||||||
|
<string name="screen_session_verification_waiting_another_device_title">"Starten Sie die Überprüfung auf dem anderen Gerät"</string>
|
||||||
<string name="screen_session_verification_waiting_to_accept_subtitle">"Akzeptiere die Anfrage, um den Verifizierungsprozess in deiner anderen Session zu starten, um fortzufahren."</string>
|
<string name="screen_session_verification_waiting_to_accept_subtitle">"Akzeptiere die Anfrage, um den Verifizierungsprozess in deiner anderen Session zu starten, um fortzufahren."</string>
|
||||||
<string name="screen_session_verification_waiting_to_accept_title">"Warten auf die Annahme der Anfrage"</string>
|
<string name="screen_session_verification_waiting_to_accept_title">"Warten auf die Annahme der Anfrage"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Abmelden…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Abmelden…"</string>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
<string name="screen_session_verification_compare_numbers_title">"Porównaj liczby"</string>
|
<string name="screen_session_verification_compare_numbers_title">"Porównaj liczby"</string>
|
||||||
<string name="screen_session_verification_complete_subtitle">"Twoja nowa sesja jest teraz zweryfikowana. Ma ona dostęp do Twoich zaszyfrowanych wiadomości, a inni użytkownicy będą widzieć ją jako zaufaną."</string>
|
<string name="screen_session_verification_complete_subtitle">"Twoja nowa sesja jest teraz zweryfikowana. Ma ona dostęp do Twoich zaszyfrowanych wiadomości, a inni użytkownicy będą widzieć ją jako zaufaną."</string>
|
||||||
<string name="screen_session_verification_enter_recovery_key">"Wprowadź klucz przywracania"</string>
|
<string name="screen_session_verification_enter_recovery_key">"Wprowadź klucz przywracania"</string>
|
||||||
|
<string name="screen_session_verification_failed_subtitle">"Albo upłynął limit czasu żądania, albo żądanie zostało odrzucone, albo wystąpił błąd weryfikacji."</string>
|
||||||
<string name="screen_session_verification_open_existing_session_subtitle">"Udowodnij, że to ty, aby uzyskać dostęp do historii zaszyfrowanych wiadomości."</string>
|
<string name="screen_session_verification_open_existing_session_subtitle">"Udowodnij, że to ty, aby uzyskać dostęp do historii zaszyfrowanych wiadomości."</string>
|
||||||
<string name="screen_session_verification_open_existing_session_title">"Otwórz istniejącą sesję"</string>
|
<string name="screen_session_verification_open_existing_session_title">"Otwórz istniejącą sesję"</string>
|
||||||
<string name="screen_session_verification_positive_button_canceled">"Ponów weryfikację"</string>
|
<string name="screen_session_verification_positive_button_canceled">"Ponów weryfikację"</string>
|
||||||
|
|
@ -25,9 +26,11 @@
|
||||||
<string name="screen_session_verification_ready_subtitle">"Porównaj unikalny zestaw emoji."</string>
|
<string name="screen_session_verification_ready_subtitle">"Porównaj unikalny zestaw emoji."</string>
|
||||||
<string name="screen_session_verification_request_accepted_subtitle">"Porównaj unikalne emoji, upewniając się, że pojawiły się w tej samej kolejności."</string>
|
<string name="screen_session_verification_request_accepted_subtitle">"Porównaj unikalne emoji, upewniając się, że pojawiły się w tej samej kolejności."</string>
|
||||||
<string name="screen_session_verification_request_details_timestamp">"Zalogowano"</string>
|
<string name="screen_session_verification_request_details_timestamp">"Zalogowano"</string>
|
||||||
|
<string name="screen_session_verification_request_failure_subtitle">"Albo upłynął limit czasu żądania, albo żądanie zostało odrzucone, albo wystąpił błąd weryfikacji."</string>
|
||||||
<string name="screen_session_verification_request_failure_title">"Weryfikacja nie powiodła się"</string>
|
<string name="screen_session_verification_request_failure_title">"Weryfikacja nie powiodła się"</string>
|
||||||
<string name="screen_session_verification_request_footer">"Kontynuuj tylko, jeśli to Ty zainicjowałeś tę weryfikację."</string>
|
<string name="screen_session_verification_request_footer">"Kontynuuj tylko, jeśli to Ty zainicjowałeś tę weryfikację."</string>
|
||||||
<string name="screen_session_verification_request_subtitle">"Zweryfikuj drugie urządzenie, aby zabezpieczyć historię wiadomości."</string>
|
<string name="screen_session_verification_request_subtitle">"Zweryfikuj drugie urządzenie, aby zabezpieczyć historię wiadomości."</string>
|
||||||
|
<string name="screen_session_verification_request_success_subtitle">"Już możesz bezpiecznie czytać lub wysyłać wiadomości na drugim urządzeniu."</string>
|
||||||
<string name="screen_session_verification_request_success_title">"Urządzenie zweryfikowane"</string>
|
<string name="screen_session_verification_request_success_title">"Urządzenie zweryfikowane"</string>
|
||||||
<string name="screen_session_verification_request_title">"Zażądano weryfikacji"</string>
|
<string name="screen_session_verification_request_title">"Zażądano weryfikacji"</string>
|
||||||
<string name="screen_session_verification_they_dont_match">"Nie pasują do siebie"</string>
|
<string name="screen_session_verification_they_dont_match">"Nie pasują do siebie"</string>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ constraintlayout = "2.2.0"
|
||||||
constraintlayout_compose = "1.1.0"
|
constraintlayout_compose = "1.1.0"
|
||||||
lifecycle = "2.8.7"
|
lifecycle = "2.8.7"
|
||||||
activity = "1.9.3"
|
activity = "1.9.3"
|
||||||
media3 = "1.4.1"
|
media3 = "1.5.0"
|
||||||
camera = "1.4.0"
|
camera = "1.4.0"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
|
|
@ -46,14 +46,14 @@ coil = "2.7.0"
|
||||||
showkase = "1.0.3"
|
showkase = "1.0.3"
|
||||||
appyx = "1.5.1"
|
appyx = "1.5.1"
|
||||||
sqldelight = "2.0.2"
|
sqldelight = "2.0.2"
|
||||||
wysiwyg = "2.37.13"
|
wysiwyg = "2.37.14"
|
||||||
telephoto = "0.14.0"
|
telephoto = "0.14.0"
|
||||||
|
|
||||||
# Dependency analysis
|
# Dependency analysis
|
||||||
dependencyAnalysis = "2.5.0"
|
dependencyAnalysis = "2.5.0"
|
||||||
|
|
||||||
# DI
|
# DI
|
||||||
dagger = "2.52"
|
dagger = "2.53"
|
||||||
anvil = "0.4.0"
|
anvil = "0.4.0"
|
||||||
|
|
||||||
# Auto service
|
# Auto service
|
||||||
|
|
@ -150,7 +150,7 @@ test_arch_core = "androidx.arch.core:core-testing:2.2.0"
|
||||||
test_junit = "junit:junit:4.13.2"
|
test_junit = "junit:junit:4.13.2"
|
||||||
test_runner = "androidx.test:runner:1.6.2"
|
test_runner = "androidx.test:runner:1.6.2"
|
||||||
test_mockk = "io.mockk:mockk:1.13.13"
|
test_mockk = "io.mockk:mockk:1.13.13"
|
||||||
test_konsist = "com.lemonappdev:konsist:0.16.1"
|
test_konsist = "com.lemonappdev:konsist:0.17.1"
|
||||||
test_turbine = "app.cash.turbine:turbine:1.2.0"
|
test_turbine = "app.cash.turbine:turbine:1.2.0"
|
||||||
test_truth = "com.google.truth:truth:1.4.4"
|
test_truth = "com.google.truth:truth:1.4.4"
|
||||||
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.18"
|
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.18"
|
||||||
|
|
@ -173,7 +173,7 @@ jsoup = "org.jsoup:jsoup:1.18.1"
|
||||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||||
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
|
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
|
||||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.65"
|
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.68"
|
||||||
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
||||||
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
||||||
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||||
|
|
@ -201,7 +201,7 @@ matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
|
||||||
|
|
||||||
# Emojibase
|
# Emojibase
|
||||||
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.3.3"
|
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.3.3"
|
||||||
sigpwned_emoji4j = "com.sigpwned:emoji4j-core:15.1.2"
|
sigpwned_emoji4j = "com.sigpwned:emoji4j-core:16.0.0"
|
||||||
|
|
||||||
# Di
|
# Di
|
||||||
inject = "javax.inject:javax.inject:1"
|
inject = "javax.inject:javax.inject:1"
|
||||||
|
|
@ -233,7 +233,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
|
||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" }
|
anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" }
|
||||||
detekt = "io.gitlab.arturbosch.detekt:1.23.7"
|
detekt = "io.gitlab.arturbosch.detekt:1.23.7"
|
||||||
ktlint = "org.jlleitschuh.gradle.ktlint:12.1.1"
|
ktlint = "org.jlleitschuh.gradle.ktlint:12.1.2"
|
||||||
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
|
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
|
||||||
dependencycheck = "org.owasp.dependencycheck:11.1.0"
|
dependencycheck = "org.owasp.dependencycheck:11.1.0"
|
||||||
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
|
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
|
||||||
|
|
@ -241,6 +241,6 @@ paparazzi = "app.cash.paparazzi:1.3.5"
|
||||||
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
|
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
|
||||||
firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" }
|
firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" }
|
||||||
knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" }
|
knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" }
|
||||||
sonarqube = "org.sonarqube:6.0.0.5145"
|
sonarqube = "org.sonarqube:6.0.1.5171"
|
||||||
licensee = "app.cash.licensee:1.12.0"
|
licensee = "app.cash.licensee:1.12.0"
|
||||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,9 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.dateformatter.api"
|
namespace = "io.element.android.libraries.dateformatter.api"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.dateformatter.api
|
||||||
|
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert milliseconds to human readable duration.
|
||||||
|
* Hours in 1 digit or more.
|
||||||
|
* Minutes in 2 digits when hours are available.
|
||||||
|
* Seconds always on 2 digits.
|
||||||
|
* Example:
|
||||||
|
* - when the duration is longer than 1 hour:
|
||||||
|
* - "10:23:34"
|
||||||
|
* - "1:23:34"
|
||||||
|
* - "1:03:04"
|
||||||
|
* - when the duration is shorter:
|
||||||
|
* - "4:56"
|
||||||
|
* - "14:06"
|
||||||
|
* - Less than one minute:
|
||||||
|
* - "0:00"
|
||||||
|
* - "0:01"
|
||||||
|
* - "0:59"
|
||||||
|
*/
|
||||||
|
fun Long.toHumanReadableDuration(): String {
|
||||||
|
val inSeconds = this / 1_000
|
||||||
|
val hours = inSeconds / 3_600
|
||||||
|
val minutes = inSeconds % 3_600 / 60
|
||||||
|
val seconds = inSeconds % 60
|
||||||
|
return if (hours > 0) {
|
||||||
|
String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds)
|
||||||
|
} else {
|
||||||
|
String.format(Locale.US, "%d:%02d", minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.dateformatter.api
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DurationFormatterTest {
|
||||||
|
@Test
|
||||||
|
fun `format seconds only`() {
|
||||||
|
assertThat(buildDuration().toHumanReadableDuration()).isEqualTo("0:00")
|
||||||
|
assertThat(buildDuration(seconds = 1).toHumanReadableDuration()).isEqualTo("0:01")
|
||||||
|
assertThat(buildDuration(seconds = 59).toHumanReadableDuration()).isEqualTo("0:59")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `format minutes and seconds`() {
|
||||||
|
assertThat(buildDuration(minutes = 1).toHumanReadableDuration()).isEqualTo("1:00")
|
||||||
|
assertThat(buildDuration(minutes = 1, seconds = 30).toHumanReadableDuration()).isEqualTo("1:30")
|
||||||
|
assertThat(buildDuration(minutes = 59, seconds = 59).toHumanReadableDuration()).isEqualTo("59:59")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `format hours, minutes and seconds`() {
|
||||||
|
assertThat(buildDuration(hours = 1).toHumanReadableDuration()).isEqualTo("1:00:00")
|
||||||
|
assertThat(buildDuration(hours = 1, minutes = 1, seconds = 1).toHumanReadableDuration()).isEqualTo("1:01:01")
|
||||||
|
assertThat(buildDuration(hours = 24, minutes = 59, seconds = 59).toHumanReadableDuration()).isEqualTo("24:59:59")
|
||||||
|
assertThat(buildDuration(hours = 25, minutes = 0, seconds = 0).toHumanReadableDuration()).isEqualTo("25:00:00")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildDuration(
|
||||||
|
hours: Int = 0,
|
||||||
|
minutes: Int = 0,
|
||||||
|
seconds: Int = 0
|
||||||
|
): Long {
|
||||||
|
return (hours * 60 * 60 + minutes * 60 + seconds) * 1000L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,19 +5,32 @@
|
||||||
* Please see LICENSE in the repository root for full details.
|
* Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.theme.components
|
package io.element.android.libraries.designsystem.theme.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.DragInteraction
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.PressInteraction
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.SliderColors
|
import androidx.compose.material3.SliderColors
|
||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
|
|
||||||
|
|
@ -32,8 +45,20 @@ fun Slider(
|
||||||
steps: Int = 0,
|
steps: Int = 0,
|
||||||
onValueChangeFinish: (() -> Unit)? = null,
|
onValueChangeFinish: (() -> Unit)? = null,
|
||||||
colors: SliderColors = SliderDefaults.colors(),
|
colors: SliderColors = SliderDefaults.colors(),
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
useCustomLayout: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
val thumbColor = ElementTheme.colors.iconOnSolidPrimary
|
||||||
|
var isUserInteracting by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(interactionSource) {
|
||||||
|
interactionSource.interactions.collect { interaction ->
|
||||||
|
isUserInteracting = when (interaction) {
|
||||||
|
is DragInteraction.Start,
|
||||||
|
is PressInteraction.Press -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
androidx.compose.material3.Slider(
|
androidx.compose.material3.Slider(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
|
|
@ -43,6 +68,54 @@ fun Slider(
|
||||||
steps = steps,
|
steps = steps,
|
||||||
onValueChangeFinished = onValueChangeFinish,
|
onValueChangeFinished = onValueChangeFinish,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
|
thumb = {
|
||||||
|
if (useCustomLayout) {
|
||||||
|
SliderDefaults.Thumb(
|
||||||
|
modifier = Modifier.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
if (isUserInteracting.not()) {
|
||||||
|
drawCircle(thumbColor, radius = 8.dp.toPx())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
colors = colors.copy(
|
||||||
|
thumbColor = ElementTheme.colors.iconPrimary,
|
||||||
|
),
|
||||||
|
enabled = enabled,
|
||||||
|
thumbSize = DpSize(
|
||||||
|
if (isUserInteracting) 44.dp else 22.dp,
|
||||||
|
22.dp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SliderDefaults.Thumb(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
colors = colors,
|
||||||
|
enabled = enabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
track = { sliderState ->
|
||||||
|
if (useCustomLayout) {
|
||||||
|
SliderDefaults.Track(
|
||||||
|
modifier = Modifier.height(8.dp),
|
||||||
|
colors = colors.copy(
|
||||||
|
activeTrackColor = Color(0x66E0EDFF),
|
||||||
|
inactiveTrackColor = Color(0x66E0EDFF),
|
||||||
|
),
|
||||||
|
enabled = enabled,
|
||||||
|
sliderState = sliderState,
|
||||||
|
thumbTrackGapSize = 0.dp,
|
||||||
|
drawStopIndicator = { },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SliderDefaults.Track(
|
||||||
|
colors = colors,
|
||||||
|
enabled = enabled,
|
||||||
|
sliderState = sliderState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -55,5 +128,6 @@ internal fun SlidersPreview() = ElementThemedPreview {
|
||||||
Slider(onValueChange = { value = it }, value = value, enabled = true)
|
Slider(onValueChange = { value = it }, value = value, enabled = true)
|
||||||
Slider(steps = 10, onValueChange = { value = it }, value = value, enabled = true)
|
Slider(steps = 10, onValueChange = { value = it }, value = value, enabled = true)
|
||||||
Slider(onValueChange = { value = it }, value = value, enabled = false)
|
Slider(onValueChange = { value = it }, value = value, enabled = false)
|
||||||
|
Slider(onValueChange = { value = it }, value = value, enabled = true, useCustomLayout = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.designsystem.utils
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HideKeyboardWhenDisposed() {
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
<string name="state_event_room_invite_you">"%1$s hat dich eingeladen"</string>
|
<string name="state_event_room_invite_you">"%1$s hat dich eingeladen"</string>
|
||||||
<string name="state_event_room_join">"%1$s hat den Raum betreten"</string>
|
<string name="state_event_room_join">"%1$s hat den Raum betreten"</string>
|
||||||
<string name="state_event_room_join_by_you">"Du hast den Raum betreten"</string>
|
<string name="state_event_room_join_by_you">"Du hast den Raum betreten"</string>
|
||||||
<string name="state_event_room_knock">"%1$s hat angefragt beizutreten"</string>
|
<string name="state_event_room_knock">"%1$s beantragt den Beitritt"</string>
|
||||||
<string name="state_event_room_knock_accepted">"%1$s hat %2$s den Beitritt erlaubt"</string>
|
<string name="state_event_room_knock_accepted">"%1$s hat %2$s den Beitritt erlaubt"</string>
|
||||||
<string name="state_event_room_knock_accepted_by_you">"Du hast %1$s den Beitritt erlaubt."</string>
|
<string name="state_event_room_knock_accepted_by_you">"Du hast %1$s den Beitritt erlaubt."</string>
|
||||||
<string name="state_event_room_knock_by_you">"Du hast angefragt beizutreten"</string>
|
<string name="state_event_room_knock_by_you">"Du hast angefragt beizutreten"</string>
|
||||||
|
|
|
||||||
|
|
@ -140,4 +140,18 @@ enum class FeatureFlags(
|
||||||
defaultValue = { buildMeta -> buildMeta.buildType != BuildType.RELEASE },
|
defaultValue = { buildMeta -> buildMeta.buildType != BuildType.RELEASE },
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
),
|
),
|
||||||
|
MediaCaptionCreation(
|
||||||
|
key = "feature.media_caption_creation",
|
||||||
|
title = "Allow creation of media captions",
|
||||||
|
description = null,
|
||||||
|
defaultValue = { true },
|
||||||
|
isFinished = false,
|
||||||
|
),
|
||||||
|
MediaCaptionWarning(
|
||||||
|
key = "feature.media_caption_creation_warning",
|
||||||
|
title = "Show a compatibility warning on media captions creation",
|
||||||
|
description = null,
|
||||||
|
defaultValue = { true },
|
||||||
|
isFinished = false,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,22 @@ enum class UtdCause {
|
||||||
SentBeforeWeJoined,
|
SentBeforeWeJoined,
|
||||||
VerificationViolation,
|
VerificationViolation,
|
||||||
UnsignedDevice,
|
UnsignedDevice,
|
||||||
UnknownDevice
|
UnknownDevice,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expected utd because this is a device-historical message and
|
||||||
|
* key storage is not setup or not configured correctly.
|
||||||
|
*/
|
||||||
|
HistoricalMessage,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key was withheld on purpose because your device is insecure and/or the
|
||||||
|
* sender trust requirement settings are not met for your device.
|
||||||
|
*/
|
||||||
|
WithheldUnverifiedOrInsecureDevice,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key is withheld by sender.
|
||||||
|
*/
|
||||||
|
WithheldBySender,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ class UtdTracker(
|
||||||
UtdCause.UNKNOWN_DEVICE -> {
|
UtdCause.UNKNOWN_DEVICE -> {
|
||||||
Error.Name.ExpectedSentByInsecureDevice
|
Error.Name.ExpectedSentByInsecureDevice
|
||||||
}
|
}
|
||||||
|
UtdCause.HISTORICAL_MESSAGE -> Error.Name.HistoricalMessage
|
||||||
}
|
}
|
||||||
val event = Error(
|
val event = Error(
|
||||||
context = null,
|
context = null,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.Client
|
import org.matrix.rustcomponents.sdk.Client
|
||||||
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
|
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
|
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
|
||||||
|
|
@ -86,7 +85,7 @@ class RustMediaLoader(
|
||||||
return if (json != null) {
|
return if (json != null) {
|
||||||
RustMediaSource.fromJson(json)
|
RustMediaSource.fromJson(json)
|
||||||
} else {
|
} else {
|
||||||
mediaSourceFromUrl(url)
|
RustMediaSource.fromUrl(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ class RustRoomFactory(
|
||||||
}
|
}
|
||||||
val innerRoom = try {
|
val innerRoom = try {
|
||||||
roomListItem.previewRoom(via = emptyList())
|
roomListItem.previewRoom(via = emptyList())
|
||||||
} catch (e: RoomListException) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Failed to get pending room for $roomId")
|
Timber.e(e, "Failed to get pending room for $roomId")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.room.preview
|
package io.element.android.libraries.matrix.impl.room.preview
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
|
||||||
|
|
@ -25,7 +26,7 @@ object RoomPreviewInfoMapper {
|
||||||
avatarUrl = info.avatarUrl,
|
avatarUrl = info.avatarUrl,
|
||||||
numberOfJoinedMembers = info.numJoinedMembers.toLong(),
|
numberOfJoinedMembers = info.numJoinedMembers.toLong(),
|
||||||
roomType = info.roomType.map(),
|
roomType = info.roomType.map(),
|
||||||
isHistoryWorldReadable = info.isHistoryWorldReadable,
|
isHistoryWorldReadable = info.isHistoryWorldReadable.orFalse(),
|
||||||
isJoined = info.membership == Membership.JOINED,
|
isJoined = info.membership == Membership.JOINED,
|
||||||
isInvited = info.membership == Membership.INVITED,
|
isInvited = info.membership == Membership.INVITED,
|
||||||
isPublic = info.joinRule == JoinRule.Public,
|
isPublic = info.joinRule == JoinRule.Public,
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ private fun RustUtdCause.map(): UtdCause {
|
||||||
RustUtdCause.VERIFICATION_VIOLATION -> UtdCause.VerificationViolation
|
RustUtdCause.VERIFICATION_VIOLATION -> UtdCause.VerificationViolation
|
||||||
RustUtdCause.UNSIGNED_DEVICE -> UtdCause.UnsignedDevice
|
RustUtdCause.UNSIGNED_DEVICE -> UtdCause.UnsignedDevice
|
||||||
RustUtdCause.UNKNOWN_DEVICE -> UtdCause.UnknownDevice
|
RustUtdCause.UNKNOWN_DEVICE -> UtdCause.UnknownDevice
|
||||||
|
RustUtdCause.HISTORICAL_MESSAGE -> UtdCause.HistoricalMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,35 @@ class MediaSender @Inject constructor(
|
||||||
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
|
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
|
||||||
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
|
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
|
||||||
|
|
||||||
|
suspend fun preProcessMedia(
|
||||||
|
uri: Uri,
|
||||||
|
mimeType: String,
|
||||||
|
): Result<MediaUploadInfo> {
|
||||||
|
val compressIfPossible = sessionPreferencesStore.doesCompressMedia().first()
|
||||||
|
return preProcessor
|
||||||
|
.process(
|
||||||
|
uri = uri,
|
||||||
|
mimeType = mimeType,
|
||||||
|
deleteOriginal = false,
|
||||||
|
compressIfPossible = compressIfPossible,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendPreProcessedMedia(
|
||||||
|
mediaUploadInfo: MediaUploadInfo,
|
||||||
|
caption: String?,
|
||||||
|
formattedCaption: String?,
|
||||||
|
progressCallback: ProgressCallback?,
|
||||||
|
): Result<Unit> {
|
||||||
|
return room.sendMedia(
|
||||||
|
uploadInfo = mediaUploadInfo,
|
||||||
|
progressCallback = progressCallback,
|
||||||
|
caption = caption,
|
||||||
|
formattedCaption = formattedCaption
|
||||||
|
)
|
||||||
|
.handleSendResult()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun sendMedia(
|
suspend fun sendMedia(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,11 @@ sealed interface MediaUploadInfo {
|
||||||
data class VoiceMessage(override val file: File, val audioInfo: AudioInfo, val waveform: List<Float>) : MediaUploadInfo
|
data class VoiceMessage(override val file: File, val audioInfo: AudioInfo, val waveform: List<Float>) : MediaUploadInfo
|
||||||
data class AnyFile(override val file: File, val fileInfo: FileInfo) : MediaUploadInfo
|
data class AnyFile(override val file: File, val fileInfo: FileInfo) : MediaUploadInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MediaUploadInfo.allFiles(): List<File> {
|
||||||
|
return listOfNotNull(
|
||||||
|
file,
|
||||||
|
(this@allFiles as? MediaUploadInfo.Image)?.thumbnailFile,
|
||||||
|
(this@allFiles as? MediaUploadInfo.Video)?.thumbnailFile,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,13 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class FakeMediaPreProcessor : MediaPreProcessor {
|
class FakeMediaPreProcessor(
|
||||||
|
private val processLatch: CompletableDeferred<Unit>? = null,
|
||||||
|
) : MediaPreProcessor {
|
||||||
var processCallCount = 0
|
var processCallCount = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
@ -41,6 +44,7 @@ class FakeMediaPreProcessor : MediaPreProcessor {
|
||||||
deleteOriginal: Boolean,
|
deleteOriginal: Boolean,
|
||||||
compressIfPossible: Boolean
|
compressIfPossible: Boolean
|
||||||
): Result<MediaUploadInfo> = simulateLongTask {
|
): Result<MediaUploadInfo> = simulateLongTask {
|
||||||
|
processLatch?.await()
|
||||||
processCallCount++
|
processCallCount++
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,44 +13,12 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.mediaviewer.api"
|
namespace = "io.element.android.libraries.mediaviewer.api"
|
||||||
testOptions {
|
|
||||||
unitTests {
|
|
||||||
isIncludeAndroidResources = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAnvil()
|
setupAnvil()
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.coil.compose)
|
|
||||||
implementation(libs.androidx.media3.exoplayer)
|
|
||||||
implementation(libs.androidx.media3.ui)
|
|
||||||
implementation(libs.coroutines.core)
|
|
||||||
implementation(libs.dagger)
|
|
||||||
implementation(libs.telephoto.zoomableimage)
|
|
||||||
implementation(libs.vanniktech.blurhash)
|
|
||||||
implementation(libs.telephoto.flick)
|
|
||||||
|
|
||||||
implementation(projects.libraries.androidutils)
|
|
||||||
implementation(projects.libraries.architecture)
|
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.designsystem)
|
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
|
||||||
implementation(projects.libraries.uiStrings)
|
|
||||||
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
|
||||||
testImplementation(projects.libraries.mediaviewer.test)
|
|
||||||
testImplementation(projects.tests.testutils)
|
|
||||||
testImplementation(libs.test.junit)
|
|
||||||
testImplementation(libs.test.truth)
|
|
||||||
testImplementation(libs.test.mockk)
|
|
||||||
testImplementation(libs.test.robolectric)
|
|
||||||
testImplementation(libs.test.turbine)
|
|
||||||
testImplementation(libs.coroutines.core)
|
|
||||||
testImplementation(libs.coroutines.test)
|
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
* Copyright 2024 New Vector Ltd.
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
* Please see LICENSE in the repository root for full details.
|
* Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.mediaviewer.api.local
|
package io.element.android.libraries.mediaviewer.api
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
|
|
@ -18,44 +18,73 @@ data class MediaInfo(
|
||||||
val mimeType: String,
|
val mimeType: String,
|
||||||
val formattedFileSize: String,
|
val formattedFileSize: String,
|
||||||
val fileExtension: String,
|
val fileExtension: String,
|
||||||
|
val senderName: String?,
|
||||||
|
val dateSent: String?,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
fun anImageMediaInfo(): MediaInfo = MediaInfo(
|
fun anImageMediaInfo(
|
||||||
|
caption: String? = null,
|
||||||
|
senderName: String? = null,
|
||||||
|
dateSent: String? = null,
|
||||||
|
): MediaInfo = MediaInfo(
|
||||||
filename = "an image file.jpg",
|
filename = "an image file.jpg",
|
||||||
caption = null,
|
caption = caption,
|
||||||
mimeType = MimeTypes.Jpeg,
|
mimeType = MimeTypes.Jpeg,
|
||||||
formattedFileSize = "4MB",
|
formattedFileSize = "4MB",
|
||||||
fileExtension = "jpg",
|
fileExtension = "jpg",
|
||||||
|
senderName = senderName,
|
||||||
|
dateSent = dateSent,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aVideoMediaInfo(): MediaInfo = MediaInfo(
|
fun aVideoMediaInfo(
|
||||||
|
caption: String? = null,
|
||||||
|
senderName: String? = null,
|
||||||
|
dateSent: String? = null,
|
||||||
|
): MediaInfo = MediaInfo(
|
||||||
filename = "a video file.mp4",
|
filename = "a video file.mp4",
|
||||||
caption = null,
|
caption = caption,
|
||||||
mimeType = MimeTypes.Mp4,
|
mimeType = MimeTypes.Mp4,
|
||||||
formattedFileSize = "14MB",
|
formattedFileSize = "14MB",
|
||||||
fileExtension = "mp4",
|
fileExtension = "mp4",
|
||||||
|
senderName = senderName,
|
||||||
|
dateSent = dateSent,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aPdfMediaInfo(): MediaInfo = MediaInfo(
|
fun aPdfMediaInfo(
|
||||||
|
senderName: String? = null,
|
||||||
|
dateSent: String? = null,
|
||||||
|
): MediaInfo = MediaInfo(
|
||||||
filename = "a pdf file.pdf",
|
filename = "a pdf file.pdf",
|
||||||
caption = null,
|
caption = null,
|
||||||
mimeType = MimeTypes.Pdf,
|
mimeType = MimeTypes.Pdf,
|
||||||
formattedFileSize = "23MB",
|
formattedFileSize = "23MB",
|
||||||
fileExtension = "pdf",
|
fileExtension = "pdf",
|
||||||
|
senderName = senderName,
|
||||||
|
dateSent = dateSent,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun anApkMediaInfo(): MediaInfo = MediaInfo(
|
fun anApkMediaInfo(
|
||||||
|
senderName: String? = null,
|
||||||
|
dateSent: String? = null,
|
||||||
|
): MediaInfo = MediaInfo(
|
||||||
filename = "an apk file.apk",
|
filename = "an apk file.apk",
|
||||||
caption = null,
|
caption = null,
|
||||||
mimeType = MimeTypes.Apk,
|
mimeType = MimeTypes.Apk,
|
||||||
formattedFileSize = "50MB",
|
formattedFileSize = "50MB",
|
||||||
fileExtension = "apk",
|
fileExtension = "apk",
|
||||||
|
senderName = senderName,
|
||||||
|
dateSent = dateSent,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun anAudioMediaInfo(): MediaInfo = MediaInfo(
|
fun anAudioMediaInfo(
|
||||||
|
senderName: String? = null,
|
||||||
|
dateSent: String? = null,
|
||||||
|
): MediaInfo = MediaInfo(
|
||||||
filename = "an audio file.mp3",
|
filename = "an audio file.mp3",
|
||||||
caption = null,
|
caption = null,
|
||||||
mimeType = MimeTypes.Mp3,
|
mimeType = MimeTypes.Mp3,
|
||||||
formattedFileSize = "7MB",
|
formattedFileSize = "7MB",
|
||||||
fileExtension = "mp3",
|
fileExtension = "mp3",
|
||||||
|
senderName = senderName,
|
||||||
|
dateSent = dateSent,
|
||||||
)
|
)
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.mediaviewer.api
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
|
||||||
|
interface MediaViewerEntryPoint : FeatureEntryPoint {
|
||||||
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||||
|
|
||||||
|
interface NodeBuilder {
|
||||||
|
fun callback(callback: Callback): NodeBuilder
|
||||||
|
fun params(params: Params): NodeBuilder
|
||||||
|
fun avatar(filename: String, avatarUrl: String): NodeBuilder
|
||||||
|
fun build(): Node
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val mediaInfo: MediaInfo,
|
||||||
|
val mediaSource: MediaSource,
|
||||||
|
val thumbnailSource: MediaSource?,
|
||||||
|
val canDownload: Boolean,
|
||||||
|
val canShare: Boolean,
|
||||||
|
) : NodeInputs
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.mediaviewer.api.local
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.libraries.mediaviewer.api.local
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import io.element.android.libraries.matrix.api.media.MediaFile
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
|
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
|
|
||||||
interface LocalMediaFactory {
|
interface LocalMediaFactory {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.mediaviewer.api.local
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
interface LocalMediaRenderer {
|
||||||
|
@Composable
|
||||||
|
fun Render(localMedia: LocalMedia)
|
||||||
|
}
|
||||||
|
|
@ -1,324 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 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.libraries.mediaviewer.api.local
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.net.Uri
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.GraphicEq
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.draw.rotate
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.media3.common.MediaItem
|
|
||||||
import androidx.media3.common.Player
|
|
||||||
import androidx.media3.ui.AspectRatioFrameLayout
|
|
||||||
import androidx.media3.ui.PlayerView
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio
|
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
|
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
|
||||||
import io.element.android.libraries.designsystem.utils.KeepScreenOn
|
|
||||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
|
||||||
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.exoplayer.ExoPlayerWrapper
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.pdf.PdfViewer
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.pdf.rememberPdfViewerState
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
|
|
||||||
import me.saket.telephoto.zoomable.rememberZoomableImageState
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LocalMediaView(
|
|
||||||
localMedia: LocalMedia?,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(),
|
|
||||||
mediaInfo: MediaInfo? = localMedia?.info,
|
|
||||||
) {
|
|
||||||
val mimeType = mediaInfo?.mimeType
|
|
||||||
when {
|
|
||||||
mimeType.isMimeTypeImage() -> MediaImageView(
|
|
||||||
localMediaViewState = localMediaViewState,
|
|
||||||
localMedia = localMedia,
|
|
||||||
modifier = modifier,
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
mimeType.isMimeTypeVideo() -> MediaVideoView(
|
|
||||||
localMediaViewState = localMediaViewState,
|
|
||||||
localMedia = localMedia,
|
|
||||||
modifier = modifier,
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
mimeType == MimeTypes.Pdf -> MediaPDFView(
|
|
||||||
localMediaViewState = localMediaViewState,
|
|
||||||
localMedia = localMedia,
|
|
||||||
modifier = modifier,
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
// TODO handle audio with exoplayer
|
|
||||||
else -> MediaFileView(
|
|
||||||
localMediaViewState = localMediaViewState,
|
|
||||||
uri = localMedia?.uri,
|
|
||||||
info = mediaInfo,
|
|
||||||
modifier = modifier,
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MediaImageView(
|
|
||||||
localMediaViewState: LocalMediaViewState,
|
|
||||||
localMedia: LocalMedia?,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
if (LocalInspectionMode.current) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = CommonDrawables.sample_background),
|
|
||||||
modifier = modifier,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val zoomableImageState = rememberZoomableImageState(localMediaViewState.zoomableState)
|
|
||||||
localMediaViewState.isReady = zoomableImageState.isImageDisplayed
|
|
||||||
ZoomableAsyncImage(
|
|
||||||
modifier = modifier,
|
|
||||||
state = zoomableImageState,
|
|
||||||
model = localMedia?.uri,
|
|
||||||
contentDescription = stringResource(id = CommonStrings.common_image),
|
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
onClick = { onClick() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MediaVideoView(
|
|
||||||
localMediaViewState: LocalMediaViewState,
|
|
||||||
localMedia: LocalMedia?,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
if (LocalInspectionMode.current) {
|
|
||||||
Text(
|
|
||||||
modifier = modifier
|
|
||||||
.background(ElementTheme.colors.bgSubtlePrimary)
|
|
||||||
.wrapContentSize(),
|
|
||||||
text = "A Video Player will render here",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ExoPlayerMediaVideoView(
|
|
||||||
localMediaViewState = localMediaViewState,
|
|
||||||
localMedia = localMedia,
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
@Composable
|
|
||||||
private fun ExoPlayerMediaVideoView(
|
|
||||||
localMediaViewState: LocalMediaViewState,
|
|
||||||
localMedia: LocalMedia?,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
var playableState: PlayableState.Playable by remember {
|
|
||||||
mutableStateOf(PlayableState.Playable(isPlaying = false, isShowingControls = false))
|
|
||||||
}
|
|
||||||
localMediaViewState.playableState = playableState
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val playerListener = object : Player.Listener {
|
|
||||||
override fun onRenderedFirstFrame() {
|
|
||||||
localMediaViewState.isReady = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
||||||
playableState = playableState.copy(isPlaying = isPlaying)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val exoPlayer = remember {
|
|
||||||
ExoPlayerWrapper.create(context)
|
|
||||||
.apply {
|
|
||||||
addListener(playerListener)
|
|
||||||
this.prepare()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (localMedia?.uri != null) {
|
|
||||||
LaunchedEffect(localMedia.uri) {
|
|
||||||
val mediaItem = MediaItem.fromUri(localMedia.uri)
|
|
||||||
exoPlayer.setMediaItem(mediaItem)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
exoPlayer.setMediaItems(emptyList())
|
|
||||||
}
|
|
||||||
KeepScreenOn(playableState.isPlaying)
|
|
||||||
AndroidView(
|
|
||||||
factory = {
|
|
||||||
PlayerView(context).apply {
|
|
||||||
player = exoPlayer
|
|
||||||
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
|
||||||
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
|
||||||
setOnClickListener {
|
|
||||||
onClick()
|
|
||||||
}
|
|
||||||
setControllerVisibilityListener(PlayerView.ControllerVisibilityListener { visibility ->
|
|
||||||
val isShowingControls = visibility == View.VISIBLE
|
|
||||||
playableState = playableState.copy(isShowingControls = isShowingControls)
|
|
||||||
})
|
|
||||||
controllerShowTimeoutMs = 1500
|
|
||||||
setShowPreviousButton(false)
|
|
||||||
setShowFastForwardButton(false)
|
|
||||||
setShowRewindButton(false)
|
|
||||||
setShowNextButton(false)
|
|
||||||
showController()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRelease = { playerView ->
|
|
||||||
playerView.setOnClickListener(null)
|
|
||||||
playerView.setControllerVisibilityListener(null as PlayerView.ControllerVisibilityListener?)
|
|
||||||
playerView.player = null
|
|
||||||
},
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
|
|
||||||
OnLifecycleEvent { _, event ->
|
|
||||||
when (event) {
|
|
||||||
Lifecycle.Event.ON_RESUME -> exoPlayer.play()
|
|
||||||
Lifecycle.Event.ON_PAUSE -> exoPlayer.pause()
|
|
||||||
Lifecycle.Event.ON_DESTROY -> {
|
|
||||||
exoPlayer.release()
|
|
||||||
exoPlayer.removeListener(playerListener)
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MediaPDFView(
|
|
||||||
localMediaViewState: LocalMediaViewState,
|
|
||||||
localMedia: LocalMedia?,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val pdfViewerState = rememberPdfViewerState(
|
|
||||||
model = localMedia?.uri,
|
|
||||||
zoomableState = localMediaViewState.zoomableState,
|
|
||||||
)
|
|
||||||
localMediaViewState.isReady = pdfViewerState.isLoaded
|
|
||||||
PdfViewer(
|
|
||||||
pdfViewerState = pdfViewerState,
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MediaFileView(
|
|
||||||
localMediaViewState: LocalMediaViewState,
|
|
||||||
uri: Uri?,
|
|
||||||
info: MediaInfo?,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val isAudio = info?.mimeType.isMimeTypeAudio().orFalse()
|
|
||||||
localMediaViewState.isReady = uri != null
|
|
||||||
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.clickable(
|
|
||||||
onClick = onClick,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(72.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.onBackground),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (isAudio) Icons.Outlined.GraphicEq else CompoundIcons.Attachment(),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.background,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(32.dp)
|
|
||||||
.rotate(if (isAudio) 0f else -45f),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (info != null) {
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
|
||||||
Text(
|
|
||||||
text = info.filename,
|
|
||||||
maxLines = 2,
|
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = formatFileExtensionAndSize(info.fileExtension, info.formattedFileSize),
|
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,30 +7,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.mediaviewer.api.util
|
package io.element.android.libraries.mediaviewer.api.util
|
||||||
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
|
||||||
import io.element.android.libraries.di.AppScope
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
interface FileExtensionExtractor {
|
interface FileExtensionExtractor {
|
||||||
fun extractFromName(name: String): String
|
fun extractFromName(name: String): String
|
||||||
}
|
}
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
class FileExtensionExtractorWithValidation @Inject constructor() : FileExtensionExtractor {
|
|
||||||
override fun extractFromName(name: String): String {
|
|
||||||
val fileExtension = name.substringAfterLast('.', "")
|
|
||||||
// Makes sure the extension is known by the system, otherwise default to binary extension.
|
|
||||||
return if (MimeTypeMap.getSingleton().hasExtension(fileExtension)) {
|
|
||||||
fileExtension
|
|
||||||
} else {
|
|
||||||
"bin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FileExtensionExtractorWithoutValidation : FileExtensionExtractor {
|
|
||||||
override fun extractFromName(name: String): String {
|
|
||||||
return name.substringAfterLast('.', "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 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.libraries.mediaviewer.api.viewer
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.aPdfMediaInfo
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.aVideoMediaInfo
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.anAudioMediaInfo
|
|
||||||
import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo
|
|
||||||
|
|
||||||
open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState> {
|
|
||||||
override val values: Sequence<MediaViewerState>
|
|
||||||
get() = sequenceOf(
|
|
||||||
aMediaViewerState(),
|
|
||||||
aMediaViewerState(AsyncData.Loading()),
|
|
||||||
aMediaViewerState(AsyncData.Failure(IllegalStateException("error"))),
|
|
||||||
aMediaViewerState(
|
|
||||||
AsyncData.Success(
|
|
||||||
LocalMedia(Uri.EMPTY, anImageMediaInfo())
|
|
||||||
),
|
|
||||||
anImageMediaInfo(),
|
|
||||||
),
|
|
||||||
aMediaViewerState(
|
|
||||||
AsyncData.Success(
|
|
||||||
LocalMedia(Uri.EMPTY, aVideoMediaInfo())
|
|
||||||
),
|
|
||||||
aVideoMediaInfo(),
|
|
||||||
),
|
|
||||||
aMediaViewerState(
|
|
||||||
AsyncData.Success(
|
|
||||||
LocalMedia(Uri.EMPTY, aPdfMediaInfo())
|
|
||||||
),
|
|
||||||
aPdfMediaInfo(),
|
|
||||||
),
|
|
||||||
aMediaViewerState(
|
|
||||||
AsyncData.Loading(),
|
|
||||||
anApkMediaInfo(),
|
|
||||||
),
|
|
||||||
aMediaViewerState(
|
|
||||||
AsyncData.Success(
|
|
||||||
LocalMedia(Uri.EMPTY, anApkMediaInfo())
|
|
||||||
),
|
|
||||||
anApkMediaInfo(),
|
|
||||||
),
|
|
||||||
aMediaViewerState(
|
|
||||||
AsyncData.Loading(),
|
|
||||||
anAudioMediaInfo(),
|
|
||||||
),
|
|
||||||
aMediaViewerState(
|
|
||||||
AsyncData.Success(
|
|
||||||
LocalMedia(Uri.EMPTY, anAudioMediaInfo())
|
|
||||||
),
|
|
||||||
anAudioMediaInfo(),
|
|
||||||
),
|
|
||||||
aMediaViewerState(
|
|
||||||
AsyncData.Success(
|
|
||||||
LocalMedia(Uri.EMPTY, anImageMediaInfo())
|
|
||||||
),
|
|
||||||
anImageMediaInfo(),
|
|
||||||
canDownload = false,
|
|
||||||
canShare = false,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun aMediaViewerState(
|
|
||||||
downloadedMedia: AsyncData<LocalMedia> = AsyncData.Uninitialized,
|
|
||||||
mediaInfo: MediaInfo = anImageMediaInfo(),
|
|
||||||
canDownload: Boolean = true,
|
|
||||||
canShare: Boolean = true,
|
|
||||||
eventSink: (MediaViewerEvents) -> Unit = {},
|
|
||||||
) = MediaViewerState(
|
|
||||||
mediaInfo = mediaInfo,
|
|
||||||
thumbnailSource = null,
|
|
||||||
downloadedMedia = downloadedMedia,
|
|
||||||
snackbarMessage = null,
|
|
||||||
canDownload = canDownload,
|
|
||||||
canShare = canShare,
|
|
||||||
eventSink = eventSink,
|
|
||||||
)
|
|
||||||
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